SteamGifts Group Tools

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();
  }
})();