您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tools to track leechers and group rules
// ==UserScript== // @name SteamGifts Group Tools // @namespace SG-Group-Tools // @version 1.0.4 // @description Tools to track leechers and group rules // @author CapnJ // @license MIT // @match https://www.steamgifts.com/group/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect steamgifts.com // ==/UserScript== (function () { const style = document.createElement('style'); style.textContent = ` .sggt-btn { background: #4a90e2; color: white; border: none; padding: 8px 12px; margin-top: 6px; margin-right: 6px; border-radius: 6px; font-size: 13px; font-weight: bold; cursor: pointer; transition: background 0.2s ease-in-out; } .sggt-btn:hover { background: #357ab8; } `; document.head.appendChild(style); function injectSettingsButton() { const leftContainer = document.querySelector('body > header > nav > div.nav__left-container'); if (!leftContainer) return; const esgstContainer = document.querySelector('#esgst'); if (!esgstContainer) return; const settingsBtn = document.createElement('a'); settingsBtn.className = 'nav__button'; settingsBtn.href = '#'; settingsBtn.innerHTML = ` <i class="fa fa-fw fa-cogs icon-grey grey"></i> <span style="margin-left: 4px;">Group Settings</span> `; settingsBtn.style.marginLeft = '8px'; settingsBtn.addEventListener('click', e => { e.preventDefault(); openSettingsModal(); }); esgstContainer.insertAdjacentElement('afterend', settingsBtn); } function waitForESGSTAndInjectSettings(retries = 10) { const esgstContainer = document.querySelector('#esgst'); const leftContainer = document.querySelector('body > header > nav > div.nav__left-container'); if (esgstContainer && leftContainer) { injectSettingsButton(); } else if (retries > 0) { setTimeout(() => waitForESGSTAndInjectSettings(retries - 1), 500); } } const groupKey = window.location.pathname.split('/')[2]; const lastCheckKey = `lastCheckTime_${groupKey}`; const lastEntryKey = `lastEntryTimes_${groupKey}`; const successKey = `creatorSuccessFlags_${groupKey}`; const defaultFeatures = { kickBuffer: { label: 'Kick Buffer Column' }, offsetInput: { label: 'Offset Column' }, lastEntry: { label: 'Last Entry Function' }, successTags: { label: 'Monthly Giveaway and Tags' } }; function getGroupFeatureSettings() { const stored = localStorage.getItem(`groupFeatureSettings_${groupKey}`); const parsed = stored ? JSON.parse(stored) : {}; return Object.assign({}, ...Object.keys(defaultFeatures).map(k => ({ [k]: parsed[k] ?? true }))); } const settings = getGroupFeatureSettings(); // Ensure all expected keys are present Object.keys(defaultFeatures).forEach(key => { if (typeof settings[key] === 'undefined') { settings[key] = true; } }); function openSettingsModal() { const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 2px solid #333; z-index: 10000; box-shadow: 0 0 10px rgba(0,0,0,0.5); border-radius: 8px; `; const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; `; overlay.addEventListener('click', () => { document.body.removeChild(modal); document.body.removeChild(overlay); }); const settings = getGroupFeatureSettings(); modal.innerHTML = ` <h3>Feature Visibility Settings</h3> ${Object.keys(defaultFeatures).map(key => ` <label style='display:block;margin-bottom:8px;'> ${defaultFeatures[key].label} <input type='checkbox' data-feature='${key}' ${settings[key] ? 'checked' : ''}/> </label> `).join('')} <button id='saveSettingsBtn' class='sggt-btn'>Save</button> `; modal.querySelector('#saveSettingsBtn').addEventListener('click', () => { const checkboxes = modal.querySelectorAll('input[type="checkbox"]'); const newSettings = {}; checkboxes.forEach(cb => { newSettings[cb.dataset.feature] = cb.checked; }); localStorage.setItem(`groupFeatureSettings_${groupKey}`, JSON.stringify(newSettings)); document.body.removeChild(modal); document.body.removeChild(overlay); location.reload(); }); document.body.appendChild(overlay); document.body.appendChild(modal); } function getMonthKey(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; } function getMonthName(date) { return date.toLocaleString('default', { month: 'long' }); } async function rolloverSuccessFlags() { const flags = await GM_getValue(successKey, {}); const now = new Date(); const thisMonth = getMonthKey(now); const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1); const lastMonth = getMonthKey(lastMonthDate); const updated = {}; for (const [user, record] of Object.entries(flags)) { updated[user] = { lastMonth: record.thisMonth || false, thisMonth: false }; } await GM_setValue(successKey, updated); } async function scanSuccessfulGiveawaysOnPage() { let flags = await GM_getValue(successKey, {}); let updatedUsers = 0; document.querySelectorAll('.giveaway__row-outer-wrap').forEach(g => { const creator = g.querySelector('.giveaway__username'); const winner = g.querySelector('[data-draggable-id="winners"] .fa-check-circle'); const tsEl = g.querySelector('span[data-timestamp]'); if (!creator || !winner || !tsEl) return; const username = creator.href.split('/user/')[1]; const ts = parseInt(tsEl.dataset.timestamp) * 1000; const ended = new Date(ts); const monthKey = getMonthKey(ended); // Determine if we are in lastMonth or thisMonth const now = new Date(); const currentKey = getMonthKey(now); const prevMonthKey = getMonthKey(new Date(now.getFullYear(), now.getMonth() - 1, 1)); if (!flags[username]) flags[username] = { thisMonth: false, lastMonth: false }; let wasUpdated = false; if (monthKey === currentKey && !flags[username].thisMonth) { flags[username].thisMonth = true; wasUpdated = true; } if (monthKey === prevMonthKey && !flags[username].lastMonth) { flags[username].lastMonth = true; wasUpdated = true; } if (wasUpdated) updatedUsers++; }); await GM_setValue(successKey, flags); alert(`Successful creator flags updated. ${updatedUsers} user(s) modified.`); } async function getStoredEntryTimes() { return await GM_getValue(lastEntryKey, {}); } async function getConfirmedEntryTime() { return await GM_getValue(lastCheckKey); } async function setConfirmedEntryTime(timestamp) { return await GM_setValue(lastCheckKey, timestamp); } function getSettings() { const base = parseFloat(localStorage.getItem(`kickBufferBaseLimit_${groupKey}`) || '200'); const pct = parseFloat(localStorage.getItem(`kickBufferPercentage_${groupKey}`) || '10'); return { baseLimit: isNaN(base) ? 200 : base, percentage: isNaN(pct) ? 10 : pct }; } function getOffset(username) { return parseInt(localStorage.getItem(`kickBufferOffset_${groupKey}_${username}`) || '0') || 0; } function rerun() { document.querySelectorAll('.kick-buffer-cell, .offset-cell, .last-entry-cell').forEach(e => e.remove()); document.querySelectorAll('.table__row-inner-wrap').forEach(processRow); } async function processRow(row) { const link = row.querySelector('a[href*="/user/"]'); if (!link) return; const username = link.href.split('/user/')[1]; const s = getSettings(); const offset = getOffset(username); const cells = row.querySelectorAll('.table__column--width-small.text-center'); const sent = parseFloat(cells[0]?.textContent.match(/\(([^)]+)\)/)?.[1]?.replace(/[^\d.-]/g, '') || '0'); const valueDiffCell = cells[3]; const valueDiff = parseFloat(valueDiffCell?.textContent.replace(/[^\d.-]/g, '') || '0'); const minBuffer = parseInt(localStorage.getItem(`kickBufferMinLimit_${groupKey}`) ?? '0'); const calculatedBuffer = s.baseLimit + sent * (s.percentage / 100) + offset; const buffer = -Math.max(calculatedBuffer, minBuffer); if (valueDiff < buffer) { valueDiffCell.style.color = 'red'; valueDiffCell.style.fontWeight = 'bold'; } else { valueDiffCell.style.color = ''; valueDiffCell.style.fontWeight = ''; } const lastEntryCell = document.createElement('div'); lastEntryCell.className = 'table__column table__column--width-small text-center last-entry-cell'; const [storedEntries, confirmedTime] = await Promise.all([getStoredEntryTimes(), getConfirmedEntryTime()]); const lastEntryTime = storedEntries[username] || 0; lastEntryCell.textContent = lastEntryTime ? new Date(lastEntryTime * 1000).toLocaleString() : '-'; if (confirmedTime && lastEntryTime > confirmedTime / 1000) { lastEntryCell.style.fontWeight = 'bold'; if (valueDiff < buffer) { lastEntryCell.style.color = 'red'; } else { lastEntryCell.style.color = 'green'; } } const offsetCell = document.createElement('div'); offsetCell.className = 'table__column table__column--width-small text-center offset-cell'; const input = document.createElement('input'); input.type = 'number'; input.value = Math.round(offset); input.step = '1'; input.style.cssText = 'width:60px;text-align:center;font-size:11px;'; input.addEventListener('change', () => { localStorage.setItem(`kickBufferOffset_${groupKey}_${username}`, input.value); rerun(); }); if (settings.offsetInput){ offsetCell.appendChild(input); row.appendChild(offsetCell); }; const bCell = document.createElement('div'); bCell.className = 'table__column table__column--width-small text-center kick-buffer-cell'; bCell.textContent = buffer.toFixed(2); if (settings.kickBuffer) row.appendChild(bCell); if (settings.lastEntry) row.appendChild(lastEntryCell); } function injectHeader() { const head = document.querySelector('.table__heading'); if (!head || head.querySelector('.kick-buffer-header')) return; if (settings.offsetInput){ const offset = document.createElement('div'); offset.className = 'table__column--width-small text-center'; offset.textContent = 'Offset (-)'; head.appendChild(offset); } if (settings.kickBuffer){ const buffer = document.createElement('div'); buffer.className = 'table__column--width-small text-center kick-buffer-header'; buffer.textContent = 'Kick Buffer'; head.appendChild(buffer); } if (settings.lastEntry){ const lastEntry = document.createElement('div'); lastEntry.className = 'table__column--width-small text-center'; lastEntry.textContent = 'Last Entry'; head.appendChild(lastEntry); } } async function injectControls() { const nav = document.querySelector('.sidebar__navigation'); if (!nav || nav.nextElementSibling?.classList.contains('kick-buffer-config-panel')) return; const settings = getGroupFeatureSettings(); const s = getSettings(); const confirmedTime = await getConfirmedEntryTime(); const wrap = document.createElement('div'); wrap.className = 'kick-buffer-config-panel'; wrap.style.cssText = 'margin-top:15px;padding:10px;border:1px solid #ccc;background:#f4f4f4;'; let html = ` <strong style="display:block;margin-bottom:8px;">Kick Buffer Settings</strong> <label style="margin-right:15px;"> Base Limit: <input type="number" id="baseLimitInput" value="${s.baseLimit}" step="1" style="width:60px;"> </label> <label> Percentage: <input type="number" id="percentageInput" value="${s.percentage}" step="1" style="width:60px;"> </label> <label> Min Buffer: <input type="number" id="minBufferInput" value="${localStorage.getItem(`kickBufferMinLimit_${groupKey}`) ?? '0'}" step="1" style="width:60px;"> </label> `; if (settings.lastEntry) { html += ` <div style="margin: 8px 0;"> <strong>Last Check:</strong> <span id="confirmedTimeDisplay">${confirmedTime ? new Date(confirmedTime).toLocaleString() : 'Not set'}</span> </div> <button id="confirmNowBtn" class="sggt-btn">Set timecheck to now</button> `; } wrap.innerHTML = html; nav.insertAdjacentElement('afterend', wrap); // Attach input listeners document.getElementById('baseLimitInput').addEventListener('input', () => { localStorage.setItem(`kickBufferBaseLimit_${groupKey}`, document.getElementById('baseLimitInput').value); rerun(); }); document.getElementById('percentageInput').addEventListener('input', () => { localStorage.setItem(`kickBufferPercentage_${groupKey}`, document.getElementById('percentageInput').value); rerun(); }); document.getElementById('minBufferInput').addEventListener('input', () => { localStorage.setItem(`kickBufferMinLimit_${groupKey}`, document.getElementById('minBufferInput').value); rerun(); }); // Optional button listener if (settings.lastEntry) { document.getElementById('confirmNowBtn').addEventListener('click', async () => { const now = Date.now(); await setConfirmedEntryTime(now); document.getElementById('confirmedTimeDisplay').textContent = new Date(now).toLocaleString(); rerun(); }); } // Export/import buttons (if present elsewhere in DOM) const exportBtn = document.getElementById('exportKickBuffer'); const importBtn = document.getElementById('importKickBuffer'); if (exportBtn) exportBtn.addEventListener('click', exportData); if (importBtn) importBtn.addEventListener('click', importData); } function autoScrollUntilLoaded(cb) { const status = document.createElement('div'); status.id = 'kick-buffer-status'; status.style.cssText = 'position:fixed;bottom:0;left:0;right:0;padding:8px;text-align:center;background:#fffae5;color:#222;z-index:1000;font-weight:bold;box-shadow:0 -2px 4px rgba(0,0,0,0.1);'; status.textContent = 'Kick Buffer: Loading users…'; document.body.appendChild(status); let prev = 0, idle = 0; const interval = setInterval(() => { window.scrollTo(0, document.body.scrollHeight); const count = document.querySelectorAll('.table__row-inner-wrap').length; if (count === prev) idle++; else { idle = 0; prev = count; } if (idle > 10) { clearInterval(interval); status.textContent = 'Kick Buffer: Loaded and calculated.'; cb(); setTimeout(() => status.remove(), 5000); } }, 200); } function delay(ms) { return new Promise(res => setTimeout(res, ms)); } async function scanAllGiveaways() { const entryLinks = Array.from( document.querySelectorAll('.giveaway__links a[href$="/entries"]') ).filter(link => { const text = link.textContent.trim(); return !text.startsWith("0 "); }); console.log(`[Entry Check] Found ${entryLinks.length} giveaways to check.`); if (entryLinks.length === 0) { alert("No giveaways with entries found on this page."); return; } const entryTimes = await getStoredEntryTimes(); for (const link of entryLinks) { const giveawayUrl = 'https://www.steamgifts.com' + link.getAttribute('href').replace(/\/entries$/, ''); updateGiveawayStatusText(`Checking: ${giveawayUrl}`); await processGiveawayEntries(giveawayUrl, entryTimes); await delay(550); } await GM_setValue(lastEntryKey, entryTimes); updateGiveawayStatusText("Giveaway check complete."); alert("Entrant timestamps saved successfully."); } async function processGiveawayEntries(giveawayUrl, entryTimes) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url: giveawayUrl + "/entries", onload: response => { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); console.log(`[Entry Check] Checking giveaway: ${giveawayUrl}`); const rows = doc.querySelectorAll('.table__row-inner-wrap'); let newEntries = 0; rows.forEach(row => { const userLink = row.querySelector('a[href*="/user/"]'); const span = row.querySelector('span[data-timestamp]'); if (!userLink || !span) return; const username = userLink.href.split('/user/')[1]; const timestamp = parseInt(span.dataset.timestamp); if (!entryTimes[username] || timestamp > entryTimes[username]) { entryTimes[username] = timestamp; newEntries++; } }); console.log(`[Entry Check] Updated ${newEntries} entries from ${giveawayUrl}`); resolve(); }, onerror: err => { console.error("Error fetching entries for", giveawayUrl, err); resolve(); } }); }); } function createGiveawayStatusPanel() { const nav = document.querySelector('.sidebar__navigation'); if (!nav || document.querySelector('.kick-buffer-status-panel')) return; const settings = getGroupFeatureSettings(); const wrap = document.createElement('div'); wrap.className = 'kick-buffer-status-panel'; wrap.style.cssText = 'margin-top:15px;padding:10px;border:1px solid #ccc;background:#f4f4f4;'; let title = ''; const buttons = []; if (settings.successTags && settings.lastEntry) { title = 'Giveaway Scanner'; buttons.push({ id: 'startGiveawayCheck', label: 'Scan for Last Entries', handler: scanAllGiveaways }); buttons.push({ id: 'scanCreatorFlags', label: 'Scan For Monthly', handler: scanSuccessfulGiveawaysOnPage }); } else if (settings.successTags) { title = 'Monhtly Scanner'; buttons.push({ id: 'scanCreatorFlags', label: 'Scan For Monthly', handler: scanSuccessfulGiveawaysOnPage }); } else { title = 'Entry Date Sniffer'; buttons.push({ id: 'startGiveawayCheck', label: 'Scan for Last Entries', handler: scanAllGiveaways }); } wrap.innerHTML = ` <strong style="display:block;margin-bottom:8px;">${title}</strong> <div id="kickBufferGiveawayStatus">Ready to scan giveaways.</div> ${buttons.map(btn => `<button id="${btn.id}" class="sggt-btn">${btn.label}</button>`).join('')} `; nav.insertAdjacentElement('afterend', wrap); buttons.forEach(btn => { document.getElementById(btn.id).addEventListener('click', btn.handler); }); } function updateGiveawayStatusText(text) { const statusEl = document.getElementById('kickBufferGiveawayStatus'); if (statusEl) statusEl.textContent = text; } async function injectSuccessTags() { const flags = await GM_getValue(successKey, {}); const now = new Date(); const thisMonthKey = getMonthKey(now); const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1); const lastMonthKey = getMonthKey(lastMonthDate); document.querySelectorAll('.table__row-inner-wrap').forEach(row => { const usernameEl = row.querySelector('a[href^="/user/"]'); if (!usernameEl) return; const username = usernameEl.href.split('/user/')[1]; const data = flags[username]; if (!data) return; const createTag = (text, color) => { const span = document.createElement('span'); span.textContent = text; span.style.cssText = `background-color:${color};color:white;padding:2px 6px;margin-left:5px;border-radius:10px;font-size:10px;font-weight:bold;`; return span; }; const tagContainer = row.querySelector('.esgst-tag-button'); if (tagContainer) { const thisMonthName = getMonthName(now); const lastMonthName = getMonthName(lastMonthDate); tagContainer.insertAdjacentElement('afterend', createTag(thisMonthName, data.thisMonth ? 'green' : 'red') ); tagContainer.insertAdjacentElement('afterend', createTag(lastMonthName, data.lastMonth ? 'green' : 'red') ); } }); } // Run rollover once per new month (async () => { const monthKey = `lastRolloverMonth_${groupKey}`; const stored = await GM_getValue(monthKey, null); const now = new Date(); const thisMonth = getMonthKey(now); if (stored !== thisMonth) { await rolloverSuccessFlags(); await GM_setValue(monthKey, thisMonth); } })(); async function injectGiveawayCreatorFlags() { const flags = await GM_getValue(successKey, {}); const now = new Date(); const thisMonthKey = getMonthKey(now); const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1); const lastMonthKey = getMonthKey(lastMonthDate); document.querySelectorAll('.giveaway__row-outer-wrap').forEach(g => { const usernameEl = g.querySelector('.giveaway__username'); if (!usernameEl) return; const username = usernameEl.href.split('/user/')[1]; const data = flags[username]; if (!data) return; const createTag = (text, color) => { const span = document.createElement('span'); span.textContent = text; span.style.cssText = `background-color:${color};color:white;padding:2px 6px;margin-left:5px;border-radius:10px;font-size:10px;font-weight:bold;`; return span; }; usernameEl.insertAdjacentElement('afterend', createTag(lastMonthKey.split('-')[1], data.lastMonth ? 'green' : 'red') ); usernameEl.insertAdjacentElement('afterend', createTag(thisMonthKey.split('-')[1], data.thisMonth ? 'green' : 'red') ); }); } waitForESGSTAndInjectSettings(); if (location.pathname.includes("/users")) { injectHeader(); if (settings.kickBuffer || settings.offsetInput || settings.LastEntry) injectControls(); autoScrollUntilLoaded(() => { rerun(); if (settings.successTags) injectSuccessTags(); }); } else { if (settings.successTags || settings.lastEntry) createGiveawayStatusPanel(); } })();