gamescom Epix Tools

Automatically adds all the Epix friends links from the gamescom discord server

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        gamescom Epix Tools
// @namespace   Violentmonkey Scripts
// @match       https://discord.com/channels/574865170694799400/1259933715409145966*
// @match       https://www.gamescom.global/*/epix/cards
// @inject-into content
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @version     2.1
// @author      UpDownLeftDie

// @license MIT
// @description Automatically adds all the Epix friends links from the gamescom discord server

// @contributionURL https://www.patreon.com/camkitties
// @supportURL https://discord.gg/hWvWGUDf
// @homepageURL https://greasyfork.org/en/scripts/503478%20gamescom%20epix%20tools

// ==/UserScript==

let EPIX_IDS = [];
let DISCORD_TOKEN = '';
let EPIX_FRIENDS = [];
let EPIX_BUTTON;


main();
function main() {
  if(location.href.includes('discord.com')) {
    function check(changes, observer) {
      if(document.querySelector('h1')) {
        observer.disconnect();
        discordMain();
      }
    }
  } else {
    function check(changes, observer) {
      if(document.querySelector('.card-list--list-item')) {
        observer.disconnect();
        epixMain();
      }
    }
  }
  (new MutationObserver(check)).observe(document, {childList: true, subtree: true});
}


function epixMain() {
  const mainSection = document.querySelector('section > div');
  const cardsButton = document.createElement('button');
  cardsButton.setAttribute('id', 'loadCardsButton');
  cardsButton.setAttribute('type', 'button');
  cardsButton.textContent = 'Load have/need card lists';

  const screenshotButton = document.createElement('button');
  screenshotButton.setAttribute('id', 'screenshotButton');
  screenshotButton.setAttribute('type', 'button');
  screenshotButton.textContent = 'Show only extra cards';

  const buttonContainer = document.createElement('div');
  buttonContainer.setAttribute('style', 'margin-bottom: 20px; display: flex; justify-content: space-between;');

  buttonContainer.append(cardsButton, screenshotButton)

  mainSection.prepend(buttonContainer);

  document.getElementById("loadCardsButton").addEventListener("click", () => {
    cardsButton.remove();
    epixLoadCards();
  }, false);

  document.getElementById("screenshotButton").addEventListener("click", () => {
    cardsButton.remove();
    screenshotButton.remove();
    document.querySelectorAll('.card-list--list-item').forEach(node => {
      const text = node.textContent;
      if (text === "x1" || !text) {
          node.style.display = "none";
      }
    });
  }, false);
}

async function epixLoadCards() {
  window.scrollTo({
    top: document.body.scrollHeight,
    behavior:'instant',
  });

  const scrollPercent = 15
  // > -scrollPercent ensure we scroll all the way back to the top
  for(let p = 100; p > -scrollPercent; p -= scrollPercent) {
    await wait(200);
    if (p < 0) p = 0;
    window.scrollTo({
      top: document.body.scrollHeight * (p/100),
      behavior:'smooth',
    });
  }

  const lockedCardSrc = 'KdneecaZTKWwd7aCAkOT';
  let cardsHave = [];
  let cardsNeed = [];
  document.querySelectorAll('.card-list--list-item').forEach(node => {
    const img = node.querySelector('img');
    img.removeAttribute('loading');
  });
  await wait(500);
  document.querySelectorAll('.card-list--list-item').forEach(node => {
    const img = node.querySelector('img');
    const name = img.title;
    if (img.src.includes(lockedCardSrc)) {
      cardsNeed.push(name);
    } else {
      const count = Number(node.textContent.slice(1));
      cardsHave.push({name, count});
    }
  });
  cardsHave.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
  cardsNeed.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));

  const mainSection = document.querySelector('section > div');

  const cardsLists = document.createElement('div');
  cardsLists.setAttribute('class', 'row gx-3 gy-4 g-sm-4');
  cardsLists.setAttribute('style', 'margin-bottom: 25px;')
  cardsLists.innerHTML = `
    <div class="col-6" role="button" tabindex="99">
      Extra Cards:<br />
      <div style="margin-left: 20px;">
        \`${cardsHave.filter(card => card.count > 1).map(card => `${card.name}\` - x${card.count - 1}`).join('<br />`')}
        <p>&nbsp;</p>
      </div>
    </div>
    <div class="col-6" role="button" tabindex="0">
      Cards Need:<br />
      <div style="margin-left: 20px;">
        \`${cardsNeed.join('`<br />`')}\`
      </div>
    </div>
  `;

  mainSection.prepend(cardsLists)
}

function discordMain() {
  EPIX_FRIENDS = [...new Set(GM_getValue('epixFriends') || [])];
  EPIX_IDS = [...new Set(GM_getValue('epixIds') || [])];
  DISCORD_TOKEN = getDiscordToken();
  console.log({EPIX_FRIENDS, EPIX_IDS, DISCORD_TOKEN: !!DISCORD_TOKEN});

  EPIX_BUTTON = document.createElement('button');
  EPIX_BUTTON.setAttribute('id', 'epixButton');
  EPIX_BUTTON.setAttribute('type', 'button');
  EPIX_BUTTON.innerHTML = 'Run Epix Friend Adder';
  document.querySelector('h1').appendChild(EPIX_BUTTON);

  document.getElementById("epixButton").addEventListener("click", handleButton, false);
}

async function handleButton(event) {
  await getEpixIds();

  let count = 0;
  disableButton('Running: added 0');

  let messages = [];
  do {
    const minId = GM_getValue('discordMinId');
    console.log({minId});
    messages = await getDiscordMessages(minId);
    const codes = getEpixCodes(messages);
    console.log({messages, codes});

    for(let i in codes) {
      const promises = EPIX_IDS.map(epixId => connectRequest(epixId, codes[i].code));
      let responses = await Promise.all(promises).catch(err => {
        disableButton("ERROR");
        throw err;
      })
      let json = await responses[0].json();
      let status = json?.data?.status.toUpperCase();

      for(let i in responses) {
        const resp = responses[i];
        console.log({resp})
        if (!resp.ok) {
          if (resp.status === 400 || json?.message?.toLowerCase() === "user not found") {
            status = "USER_NOT_FOUND";
          } else {
            disableButton("ERROR");
            throw resp;
          }
        }
      }

      count++;
      disableButton(`Running: added ${count}`);
      updateFriends(status, codes[i]);
    }
    if (messages.length > 0) {
       GM_setValue('discordMinId', messages[messages.length - 1][0].id);
    }
  } while(messages.length >= 25);


  disableButton("Done!");
}

async function getEpixIds() {
  return new Promise((resolve, reject) => {
    const inputValue = EPIX_IDS?.length ? EPIX_IDS.join(',') : '';
    const dialog = document.createElement('dialog');
    dialog.setAttribute('open', true);
    dialog.setAttribute('id', 'epixIdsDialog')
    dialog.innerHTML = `
      <p>Enter your Epix user id(s) (<strong>NOT the same as your invite id!</strong>)</p>
      To get this go to your <a href="https://www.gamescom.global/en/epix" target="_blank">profile</a>:
        <ol>
          <li>open dev tools</li>
          <li>refresh the page</li>
          <li>look at network requests for "user?userId=XXXXXXX"</li>
        </ol>
      <form method="dialog">
        <label for="epixIds">Epix Id(s):</label>
        <input required id="epixIds" placeholder="b5629b160f555ab4b08ef8e49568b7dd, a49f9b160f555vd4b08ef8e49568b7a2" value="${inputValue}" />
        <label for="discordToken">Discord token:</label>
        <span style="display:flex;">
          <input required id="discordToken" style="flex: 1 0 0;" placeholder="MTMkaJ9HshK2dAx.Fna4Jk.qsKrjSk42jKns9Js32-G3pnH_qcnIskQzy" value="${DISCORD_TOKEN ? DISCORD_TOKEN : ''}" />
          <button id="getDiscordToken" ${!!DISCORD_TOKEN ? "disabled" : null}>Get</button>
        </span>
        <button id="epixIdAddButton" type="submit">START</button>
        <span id="epixError" style="color: red; font-weight: bold;"></span>
      </form>
    `;
    document.body.appendChild(dialog);

    document.getElementById("getDiscordToken").addEventListener("click", (e) => {
      e.preventDefault();

      DISCORD_TOKEN = getDiscordToken();
      document.getElementById("discordToken").value = DISCORD_TOKEN;
    });

    document.getElementById("epixIdAddButton").addEventListener("click", (e) => {
      e.preventDefault();
      const idStr = document.getElementById("epixIds").value;
      const ids = idStr.split(',').reduce((acc, curr) => {
        const id = curr.replace(/\W/gi, '');
        if (!id) return acc;
        acc.push(id);
        return acc;
      }, []);
      EPIX_IDS = ids;

      if(!DISCORD_TOKEN || !EPIX_IDS?.length) {
        document.getElementById("epixError").innerHTML = "ERROR: missing Epix User ID(s) or Discord Token";
        return;
      }

      GM_setValue('epixIds', EPIX_IDS);
      dialog.parentNode.removeChild(dialog);
      resolve();
    }, false);
  });
}

function getEpixCodes(discordMessages) {
  const codes = discordMessages.reduce((acc, curr) => {
    const message = curr[0]
    const matches = message.content.matchAll(/epix-connect=([\w\d]{7})/ig);

    for (const match of matches) {
      if (match?.[1] && !EPIX_FRIENDS.includes(match[1])) {
        acc.push({code: match[1], messageId: message.id});
      }
    }

    return acc;
  }, [])

  return codes;
}


function updateFriends(status, code) {
  if (status === "CONNECTION_SUCCESSFUL" || status === "ALREADY_MATCHED" || status === "USER_NOT_FOUND") {
    EPIX_FRIENDS.push(code.code);
    GM_setValue('epixFriends', EPIX_FRIENDS);
    GM_setValue('discordMinId', code.messageId);
  }

}

/**
 * @param {string} [text] - Button text
 * @returns {null}
 */
function disableButton(text = '') {
  EPIX_BUTTON.toggleAttribute('disabled', true);
  if (text) {
    updateButton(text);
  }
}

/**
 * @param {string} [text] - Button text
 * @returns {null}
 */
function enableButton(text = '') {
  EPIX_BUTTON.toggleAttribute('disabled', false);
  if (text) {
    updateButton(text);
  }
}

/**
 * @param {string} [text] - Button text
 * @returns {null}
 */
function updateButton(text = "Run Epix Friend Adder") {
  EPIX_BUTTON.innerHTML = text;
}


//--- Style our newly added elements using CSS.
GM_addStyle (`
  #epixButton {
    margin-left: 10px;
  }

  #epixIdsDialog {
    position: absolute;
    top: 5rem;
    z-index: 100;
  }
  #epixIdsDialog ol {
    list-style: auto;
    padding-left: 35px;
    margin-bottom: 10px;
  }
  #epixIdsDialog input {
    width: 100%;
  }
  #epixIdsDialog button {
    margin: 5px auto;
    display: block;
    font-size: large;
    background: greenyellow;
    padding: 2px 10px;
  }
`);


async function connectRequest(userId, profileId) {
  return fetch("https://wfppjum4x2.execute-api.eu-central-1.amazonaws.com/production/connection-request", {
    "referrer": "https://www.gamescom.global/",
    "referrerPolicy": "strict-origin-when-cross-origin",
    "body": `{"userId":"${userId}","profileId":"${profileId}"}`,
    "method": "POST",
    "mode": "cors",
    "credentials": "omit"
  });
}


// A lot of this function was adapted from: https://github.com/victornpb/undiscord/blob/master/deleteDiscordMessages.user.js#L652-L712
async function getDiscordMessages(minId) {
  const params = queryString([
    ['limit', 25],
    ['channel_id', '1259933715409145966'],
    ['min_id', minId],
    ['sort_by', 'timestamp'],
    ['sort_order', 'asc'],
    ['has','link'],
  ]);
  let resp;
  try {
    resp = await fetch(`https://discord.com/api/v9/guilds/574865170694799400/messages/search?${params}`, {
      "headers": {
        "accept": "*/*",
        "authorization": DISCORD_TOKEN
      },
      "referrer": "https://discord.com/channels/574865170694799400/1259933715409145966",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "method": "GET",
      "mode": "cors",
      "credentials": "include"
    });
  } catch (err) {
    this.state.running = false;
    console.error('Search request threw an error:', err);
    disableButton("ERROR");
    throw err;
  }

  // not indexed yet
  if (resp.status === 202) {
    let w = (await resp.json()).retry_after * 1000 || 30 * 1000;
    console.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
    await wait(w);
    return await getDiscordMessages(minId);
  }

  if (!resp.ok) {
    // searching messages too fast
    if (resp.status === 429) {
      let w = (await resp.json()).retry_after * 1000 || 30 * 1000;
      console.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`);
      console.warn(`Cooling down for ${w * 2}ms before retrying...`);

      await wait(w * 2);
      return await getDiscordMessages(minId);
    } else {
      console.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
      disableButton("ERROR");
      throw resp;
    }
  }

  const data = await resp.json();
  return data.messages;
}

function getDiscordToken() {
  window.dispatchEvent(new Event('beforeunload'));
  const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  try {
    return JSON.parse(LS.token);
  } catch {
    console.info('Could not automatically detect Authorization Token in local storage!');
    console.info('Attempting to grab token using webpack');
    return (window.webpackChunkdiscord_app?.push([[''], {}, e => { window.m = []; for (let c in e.c) window.m.push(e.c[c]); }]), window.m)?.find(m => m?.exports?.default?.getToken !== void 0).exports.default.getToken();
  }
}


const wait = async ms => new Promise(done => setTimeout(done, ms));
const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&');