gamescom Epix Tools

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

目前为 2024-08-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name gamescom Epix Tools
  3. // @namespace Violentmonkey Scripts
  4. // @match https://discord.com/channels/574865170694799400/1259933715409145966*
  5. // @match https://www.gamescom.global/*/epix/cards
  6. // @inject-into content
  7. // @grant GM_addStyle
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @version 2.1
  11. // @author UpDownLeftDie
  12.  
  13. // @license MIT
  14. // @description Automatically adds all the Epix friends links from the gamescom discord server
  15.  
  16. // @contributionURL https://www.patreon.com/camkitties
  17. // @supportURL https://discord.gg/hWvWGUDf
  18.  
  19. // @homepageURL https://greasyfork.org/en/scripts/503478%20gamescom%20epix%20tools
  20. // ==/UserScript==
  21.  
  22. let EPIX_IDS = [];
  23. let DISCORD_TOKEN = '';
  24. let EPIX_FRIENDS = [];
  25. let EPIX_BUTTON;
  26.  
  27.  
  28. main();
  29. function main() {
  30. if(location.href.includes('discord.com')) {
  31. function check(changes, observer) {
  32. if(document.querySelector('h1')) {
  33. observer.disconnect();
  34. discordMain();
  35. }
  36. }
  37. } else {
  38. function check(changes, observer) {
  39. if(document.querySelector('.card-list--list-item')) {
  40. observer.disconnect();
  41. epixMain();
  42. }
  43. }
  44. }
  45. (new MutationObserver(check)).observe(document, {childList: true, subtree: true});
  46. }
  47.  
  48.  
  49. function epixMain() {
  50. const mainSection = document.querySelector('section > div');
  51. const cardsButton = document.createElement('button');
  52. cardsButton.setAttribute('id', 'loadCardsButton');
  53. cardsButton.setAttribute('type', 'button');
  54. cardsButton.textContent = 'Load have/need card lists';
  55.  
  56. const screenshotButton = document.createElement('button');
  57. screenshotButton.setAttribute('id', 'screenshotButton');
  58. screenshotButton.setAttribute('type', 'button');
  59. screenshotButton.textContent = 'Show only extra cards';
  60.  
  61. const buttonContainer = document.createElement('div');
  62. buttonContainer.setAttribute('style', 'margin-bottom: 20px; display: flex; justify-content: space-between;');
  63.  
  64. buttonContainer.append(cardsButton, screenshotButton)
  65.  
  66. mainSection.prepend(buttonContainer);
  67.  
  68. document.getElementById("loadCardsButton").addEventListener("click", () => {
  69. cardsButton.remove();
  70. epixLoadCards();
  71. }, false);
  72.  
  73. document.getElementById("screenshotButton").addEventListener("click", () => {
  74. cardsButton.remove();
  75. screenshotButton.remove();
  76. document.querySelectorAll('.card-list--list-item').forEach(node => {
  77. const text = node.textContent;
  78. if (text === "x1" || !text) {
  79. node.style.display = "none";
  80. }
  81. });
  82. }, false);
  83. }
  84.  
  85. async function epixLoadCards() {
  86. window.scrollTo({
  87. top: document.body.scrollHeight,
  88. behavior:'instant',
  89. });
  90.  
  91. const scrollPercent = 15
  92. // > -scrollPercent ensure we scroll all the way back to the top
  93. for(let p = 100; p > -scrollPercent; p -= scrollPercent) {
  94. await wait(200);
  95. if (p < 0) p = 0;
  96. window.scrollTo({
  97. top: document.body.scrollHeight * (p/100),
  98. behavior:'smooth',
  99. });
  100. }
  101.  
  102. const lockedCardSrc = 'KdneecaZTKWwd7aCAkOT';
  103. let cardsHave = [];
  104. let cardsNeed = [];
  105. document.querySelectorAll('.card-list--list-item').forEach(node => {
  106. const img = node.querySelector('img');
  107. img.removeAttribute('loading');
  108. });
  109. await wait(500);
  110. document.querySelectorAll('.card-list--list-item').forEach(node => {
  111. const img = node.querySelector('img');
  112. const name = img.title;
  113. if (img.src.includes(lockedCardSrc)) {
  114. cardsNeed.push(name);
  115. } else {
  116. const count = Number(node.textContent.slice(1));
  117. cardsHave.push({name, count});
  118. }
  119. });
  120. cardsHave.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
  121. cardsNeed.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  122.  
  123. const mainSection = document.querySelector('section > div');
  124.  
  125. const cardsLists = document.createElement('div');
  126. cardsLists.setAttribute('class', 'row gx-3 gy-4 g-sm-4');
  127. cardsLists.setAttribute('style', 'margin-bottom: 25px;')
  128. cardsLists.innerHTML = `
  129. <div class="col-6" role="button" tabindex="99">
  130. Extra Cards:<br />
  131. <div style="margin-left: 20px;">
  132. \`${cardsHave.filter(card => card.count > 1).map(card => `${card.name}\` - x${card.count - 1}`).join('<br />`')}
  133. <p>&nbsp;</p>
  134. </div>
  135. </div>
  136. <div class="col-6" role="button" tabindex="0">
  137. Cards Need:<br />
  138. <div style="margin-left: 20px;">
  139. \`${cardsNeed.join('`<br />`')}\`
  140. </div>
  141. </div>
  142. `;
  143.  
  144. mainSection.prepend(cardsLists)
  145. }
  146.  
  147. function discordMain() {
  148. EPIX_FRIENDS = [...new Set(GM_getValue('epixFriends') || [])];
  149. EPIX_IDS = [...new Set(GM_getValue('epixIds') || [])];
  150. DISCORD_TOKEN = getDiscordToken();
  151. console.log({EPIX_FRIENDS, EPIX_IDS, DISCORD_TOKEN: !!DISCORD_TOKEN});
  152.  
  153. EPIX_BUTTON = document.createElement('button');
  154. EPIX_BUTTON.setAttribute('id', 'epixButton');
  155. EPIX_BUTTON.setAttribute('type', 'button');
  156. EPIX_BUTTON.innerHTML = 'Run Epix Friend Adder';
  157. document.querySelector('h1').appendChild(EPIX_BUTTON);
  158.  
  159. document.getElementById("epixButton").addEventListener("click", handleButton, false);
  160. }
  161.  
  162. async function handleButton(event) {
  163. await getEpixIds();
  164.  
  165. let count = 0;
  166. disableButton('Running: added 0');
  167.  
  168. let messages = [];
  169. do {
  170. const minId = GM_getValue('discordMinId');
  171. console.log({minId});
  172. messages = await getDiscordMessages(minId);
  173. const codes = getEpixCodes(messages);
  174. console.log({messages, codes});
  175.  
  176. for(let i in codes) {
  177. const promises = EPIX_IDS.map(epixId => connectRequest(epixId, codes[i].code));
  178. let responses = await Promise.all(promises).catch(err => {
  179. disableButton("ERROR");
  180. throw err;
  181. })
  182. let json = await responses[0].json();
  183. let status = json?.data?.status.toUpperCase();
  184.  
  185. for(let i in responses) {
  186. const resp = responses[i];
  187. console.log({resp})
  188. if (!resp.ok) {
  189. if (resp.status === 400 || json?.message?.toLowerCase() === "user not found") {
  190. status = "USER_NOT_FOUND";
  191. } else {
  192. disableButton("ERROR");
  193. throw resp;
  194. }
  195. }
  196. }
  197.  
  198. count++;
  199. disableButton(`Running: added ${count}`);
  200. updateFriends(status, codes[i]);
  201. }
  202. if (messages.length > 0) {
  203. GM_setValue('discordMinId', messages[messages.length - 1][0].id);
  204. }
  205. } while(messages.length >= 25);
  206.  
  207.  
  208. disableButton("Done!");
  209. }
  210.  
  211. async function getEpixIds() {
  212. return new Promise((resolve, reject) => {
  213. const inputValue = EPIX_IDS?.length ? EPIX_IDS.join(',') : '';
  214. const dialog = document.createElement('dialog');
  215. dialog.setAttribute('open', true);
  216. dialog.setAttribute('id', 'epixIdsDialog')
  217. dialog.innerHTML = `
  218. <p>Enter your Epix user id(s) (<strong>NOT the same as your invite id!</strong>)</p>
  219. To get this go to your <a href="https://www.gamescom.global/en/epix" target="_blank">profile</a>:
  220. <ol>
  221. <li>open dev tools</li>
  222. <li>refresh the page</li>
  223. <li>look at network requests for "user?userId=XXXXXXX"</li>
  224. </ol>
  225. <form method="dialog">
  226. <label for="epixIds">Epix Id(s):</label>
  227. <input required id="epixIds" placeholder="b5629b160f555ab4b08ef8e49568b7dd, a49f9b160f555vd4b08ef8e49568b7a2" value="${inputValue}" />
  228. <label for="discordToken">Discord token:</label>
  229. <span style="display:flex;">
  230. <input required id="discordToken" style="flex: 1 0 0;" placeholder="MTMkaJ9HshK2dAx.Fna4Jk.qsKrjSk42jKns9Js32-G3pnH_qcnIskQzy" value="${DISCORD_TOKEN ? DISCORD_TOKEN : ''}" />
  231. <button id="getDiscordToken" ${!!DISCORD_TOKEN ? "disabled" : null}>Get</button>
  232. </span>
  233. <button id="epixIdAddButton" type="submit">START</button>
  234. <span id="epixError" style="color: red; font-weight: bold;"></span>
  235. </form>
  236. `;
  237. document.body.appendChild(dialog);
  238.  
  239. document.getElementById("getDiscordToken").addEventListener("click", (e) => {
  240. e.preventDefault();
  241.  
  242. DISCORD_TOKEN = getDiscordToken();
  243. document.getElementById("discordToken").value = DISCORD_TOKEN;
  244. });
  245.  
  246. document.getElementById("epixIdAddButton").addEventListener("click", (e) => {
  247. e.preventDefault();
  248. const idStr = document.getElementById("epixIds").value;
  249. const ids = idStr.split(',').reduce((acc, curr) => {
  250. const id = curr.replace(/\W/gi, '');
  251. if (!id) return acc;
  252. acc.push(id);
  253. return acc;
  254. }, []);
  255. EPIX_IDS = ids;
  256.  
  257. if(!DISCORD_TOKEN || !EPIX_IDS?.length) {
  258. document.getElementById("epixError").innerHTML = "ERROR: missing Epix User ID(s) or Discord Token";
  259. return;
  260. }
  261.  
  262. GM_setValue('epixIds', EPIX_IDS);
  263. dialog.parentNode.removeChild(dialog);
  264. resolve();
  265. }, false);
  266. });
  267. }
  268.  
  269. function getEpixCodes(discordMessages) {
  270. const codes = discordMessages.reduce((acc, curr) => {
  271. const message = curr[0]
  272. const matches = message.content.matchAll(/epix-connect=([\w\d]{7})/ig);
  273.  
  274. for (const match of matches) {
  275. if (match?.[1] && !EPIX_FRIENDS.includes(match[1])) {
  276. acc.push({code: match[1], messageId: message.id});
  277. }
  278. }
  279.  
  280. return acc;
  281. }, [])
  282.  
  283. return codes;
  284. }
  285.  
  286.  
  287. function updateFriends(status, code) {
  288. if (status === "CONNECTION_SUCCESSFUL" || status === "ALREADY_MATCHED" || status === "USER_NOT_FOUND") {
  289. EPIX_FRIENDS.push(code.code);
  290. GM_setValue('epixFriends', EPIX_FRIENDS);
  291. GM_setValue('discordMinId', code.messageId);
  292. }
  293.  
  294. }
  295.  
  296. /**
  297. * @param {string} [text] - Button text
  298. * @returns {null}
  299. */
  300. function disableButton(text = '') {
  301. EPIX_BUTTON.toggleAttribute('disabled', true);
  302. if (text) {
  303. updateButton(text);
  304. }
  305. }
  306.  
  307. /**
  308. * @param {string} [text] - Button text
  309. * @returns {null}
  310. */
  311. function enableButton(text = '') {
  312. EPIX_BUTTON.toggleAttribute('disabled', false);
  313. if (text) {
  314. updateButton(text);
  315. }
  316. }
  317.  
  318. /**
  319. * @param {string} [text] - Button text
  320. * @returns {null}
  321. */
  322. function updateButton(text = "Run Epix Friend Adder") {
  323. EPIX_BUTTON.innerHTML = text;
  324. }
  325.  
  326.  
  327. //--- Style our newly added elements using CSS.
  328. GM_addStyle (`
  329. #epixButton {
  330. margin-left: 10px;
  331. }
  332.  
  333. #epixIdsDialog {
  334. position: absolute;
  335. top: 5rem;
  336. z-index: 100;
  337. }
  338. #epixIdsDialog ol {
  339. list-style: auto;
  340. padding-left: 35px;
  341. margin-bottom: 10px;
  342. }
  343. #epixIdsDialog input {
  344. width: 100%;
  345. }
  346. #epixIdsDialog button {
  347. margin: 5px auto;
  348. display: block;
  349. font-size: large;
  350. background: greenyellow;
  351. padding: 2px 10px;
  352. }
  353. `);
  354.  
  355.  
  356. async function connectRequest(userId, profileId) {
  357. return fetch("https://wfppjum4x2.execute-api.eu-central-1.amazonaws.com/production/connection-request", {
  358. "referrer": "https://www.gamescom.global/",
  359. "referrerPolicy": "strict-origin-when-cross-origin",
  360. "body": `{"userId":"${userId}","profileId":"${profileId}"}`,
  361. "method": "POST",
  362. "mode": "cors",
  363. "credentials": "omit"
  364. });
  365. }
  366.  
  367.  
  368. // A lot of this function was adapted from: https://github.com/victornpb/undiscord/blob/master/deleteDiscordMessages.user.js#L652-L712
  369. async function getDiscordMessages(minId) {
  370. const params = queryString([
  371. ['limit', 25],
  372. ['channel_id', '1259933715409145966'],
  373. ['min_id', minId],
  374. ['sort_by', 'timestamp'],
  375. ['sort_order', 'asc'],
  376. ['has','link'],
  377. ]);
  378. let resp;
  379. try {
  380. resp = await fetch(`https://discord.com/api/v9/guilds/574865170694799400/messages/search?${params}`, {
  381. "headers": {
  382. "accept": "*/*",
  383. "authorization": DISCORD_TOKEN
  384. },
  385. "referrer": "https://discord.com/channels/574865170694799400/1259933715409145966",
  386. "referrerPolicy": "strict-origin-when-cross-origin",
  387. "method": "GET",
  388. "mode": "cors",
  389. "credentials": "include"
  390. });
  391. } catch (err) {
  392. this.state.running = false;
  393. console.error('Search request threw an error:', err);
  394. disableButton("ERROR");
  395. throw err;
  396. }
  397.  
  398. // not indexed yet
  399. if (resp.status === 202) {
  400. let w = (await resp.json()).retry_after * 1000 || 30 * 1000;
  401. console.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
  402. await wait(w);
  403. return await getDiscordMessages(minId);
  404. }
  405.  
  406. if (!resp.ok) {
  407. // searching messages too fast
  408. if (resp.status === 429) {
  409. let w = (await resp.json()).retry_after * 1000 || 30 * 1000;
  410. console.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`);
  411. console.warn(`Cooling down for ${w * 2}ms before retrying...`);
  412.  
  413. await wait(w * 2);
  414. return await getDiscordMessages(minId);
  415. } else {
  416. console.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
  417. disableButton("ERROR");
  418. throw resp;
  419. }
  420. }
  421.  
  422. const data = await resp.json();
  423. return data.messages;
  424. }
  425.  
  426. function getDiscordToken() {
  427. window.dispatchEvent(new Event('beforeunload'));
  428. const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  429. try {
  430. return JSON.parse(LS.token);
  431. } catch {
  432. console.info('Could not automatically detect Authorization Token in local storage!');
  433. console.info('Attempting to grab token using webpack');
  434. 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();
  435. }
  436. }
  437.  
  438.  
  439. const wait = async ms => new Promise(done => setTimeout(done, ms));
  440. const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&');