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