Greasy Fork 支持简体中文。

Deletecord - Mass Delete Discord Messages

Adds a button to the Discord browser UI to mass delete messages from Discord channels and direct messages

目前為 2024-11-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Deletecord - Mass Delete Discord Messages
  3. // @description Adds a button to the Discord browser UI to mass delete messages from Discord channels and direct messages
  4. // @description:ja DiscordのブラウザUIにボタンを追加し、Discordのチャンネルおよびダイレクトメッセージからメッセージを一括削除します
  5. // @description:zh-CN 在Discord浏览器UI中添加按钮,以批量删除Discord频道和私信中的消息
  6. // @description:zh-HK 在Discord瀏覽器UI中添加按鈕,以批量刪除Discord頻道和私信中的消息
  7. // @description:fr Ajoute un bouton à l'interface utilisateur du navigateur Discord pour supprimer en masse les messages des canaux et des messages directs de Discord
  8. // @namespace https://github.com/bekkibau/deletecord
  9. // @version 0.1
  10. // @match https://discord.com/*
  11. // @supportURL https://github.com/bekkibau/deletecord/issues
  12. // @contributionURL https://www.buymeacoffee.com/bekkibau
  13. // @grant none
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. /**
  18. * Delete all messages in a Discord channel or DM
  19. * @param {string} authToken Your authorization token
  20. * @param {string} authorId Author of the messages you want to delete
  21. * @param {string} guildId Server were the messages are located
  22. * @param {string} channelId Channel were the messages are located
  23. * @param {string} minId Only delete messages after this, leave blank do delete all
  24. * @param {string} maxId Only delete messages before this, leave blank do delete all
  25. * @param {string} content Filter messages that contains this text content
  26. * @param {boolean} hasLink Filter messages that contains link
  27. * @param {boolean} hasFile Filter messages that contains file
  28. * @param {boolean} includeNsfw Search in NSFW channels
  29. * @param {function(string, Array)} extLogger Function for logging
  30. * @param {function} stopHndl stopHndl used for stopping
  31. * @author bekkibau <https://www.github.com/bekkibau>
  32. * @see https://github.com/bekkibau/deletecord
  33. */
  34. async function deleteMessages(authToken, authorId, guildId, channelId, minId, maxId, content, hasLink, hasFile, includeNsfw, includePinned, searchDelay, deleteDelay, extLogger, stopHndl, onProgress) {
  35. const start = new Date();
  36. let delCount = 0;
  37. let failCount = 0;
  38. let avgPing;
  39. let lastPing;
  40. let grandTotal;
  41. let throttledCount = 0;
  42. let throttledTotalTime = 0;
  43. let offset = 0;
  44. let iterations = -1;
  45.  
  46. const wait = async ms => new Promise(done => setTimeout(done, ms));
  47. const msToHMS = s => `${s / 3.6e6 | 0}h ${(s % 3.6e6) / 6e4 | 0}m ${(s % 6e4) / 1000 | 0}s`;
  48. const escapeHTML = html => html.replace(/[&<"']/g, m => ({ '&': '&amp;', '<': '&lt;', '"': '&quot;', '\'': '&#039;' })[m]);
  49. const redact = str => `<span class="priv">${escapeHTML(str)}</span><span class="mask">REDACTED</span>`;
  50. const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&');
  51. const ask = async msg => new Promise(resolve => setTimeout(() => resolve(window.confirm(msg)), 10));
  52. const printDelayStats = () => log.verb(`Delete delay: ${deleteDelay}ms, Search delay: ${searchDelay}ms`, `Last Ping: ${lastPing}ms, Average Ping: ${avgPing | 0}ms`);
  53. const toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date;
  54.  
  55. const MAX_LOG_ENTRIES = 1000; // Limit the number of log entries
  56. const BATCH_SIZE = 100; // Process messages in smaller batches
  57.  
  58. const log = {
  59. entries: [],
  60. add(type, args) {
  61. if (this.entries.length >= MAX_LOG_ENTRIES) {
  62. this.entries.shift(); // Remove the oldest entry
  63. }
  64. this.entries.push({ type, args });
  65. this.display();
  66. },
  67. display() {
  68. logArea.innerHTML = this.entries.map(entry => {
  69. const style = { '': '', info: 'color:#00b0f4;', verb: 'color:#72767d;', warn: 'color:#faa61a;', error: 'color:#f04747;', success: 'color:#43b581;' }[entry.type];
  70. return `<div style="${style}">${Array.from(entry.args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}</div>`;
  71. }).join('');
  72. if (autoScroll.checked) logArea.querySelector('div:last-child').scrollIntoView(false);
  73. },
  74. debug() { this.add('debug', arguments); },
  75. info() { this.add('info', arguments); },
  76. verb() { this.add('verb', arguments); },
  77. warn() { this.add('warn', arguments); },
  78. error() { this.add('error', arguments); },
  79. success() { this.add('success', arguments); },
  80. };
  81.  
  82. // Ensure logArea is correctly referenced
  83. const logArea = document.querySelector('#deletecord .logarea');
  84.  
  85. const adjustDelay = (retryAfter) => {
  86. const newDelay = retryAfter * 2; // Adjust delay to twice the retry_after value
  87. searchDelay = Math.max(searchDelay, newDelay);
  88. deleteDelay = Math.max(deleteDelay, newDelay);
  89. };
  90.  
  91. async function recurse() {
  92. let API_SEARCH_URL;
  93. if (guildId === '@me') {
  94. API_SEARCH_URL = `https://discord.com/api/v6/channels/${channelId}/messages/`; // DMs
  95. }
  96. else {
  97. API_SEARCH_URL = `https://discord.com/api/v6/guilds/${guildId}/messages/`; // Server
  98. }
  99.  
  100. const headers = {
  101. 'Authorization': authToken
  102. };
  103.  
  104. let resp;
  105. try {
  106. const s = Date.now();
  107. resp = await fetch(API_SEARCH_URL + 'search?' + queryString([
  108. ['author_id', authorId || undefined],
  109. ['channel_id', (guildId !== '@me' ? channelId : undefined) || undefined],
  110. ['min_id', minId ? toSnowflake(minId) : undefined],
  111. ['max_id', maxId ? toSnowflake(maxId) : undefined],
  112. ['sort_by', 'timestamp'],
  113. ['sort_order', 'desc'],
  114. ['offset', offset],
  115. ['has', hasLink ? 'link' : undefined],
  116. ['has', hasFile ? 'file' : undefined],
  117. ['content', content || undefined],
  118. ['include_nsfw', includeNsfw ? true : undefined],
  119. ]), { headers });
  120. lastPing = (Date.now() - s);
  121. avgPing = avgPing > 0 ? (avgPing * 0.9) + (lastPing * 0.1) : lastPing;
  122. } catch (err) {
  123. return log.error('Search request threw an error:', err);
  124. }
  125.  
  126. // not indexed yet
  127. if (resp.status === 202) {
  128. const w = (await resp.json()).retry_after;
  129. throttledCount++;
  130. throttledTotalTime += w;
  131. log.warn(`This channel wasn't indexed, waiting ${w}ms for discord to index it...`);
  132. await wait(w);
  133. return await recurse();
  134. }
  135.  
  136. if (!resp.ok) {
  137. // searching messages too fast
  138. if (resp.status === 429) {
  139. const w = (await resp.json()).retry_after;
  140. throttledCount++;
  141. throttledTotalTime += w;
  142. adjustDelay(w); // Adjust delay based on retry_after value
  143. log.warn(`Being rate limited by the API for ${w}ms! Increasing search and delete delay...`);
  144. printDelayStats();
  145. log.verb(`Cooling down for ${w * 2}ms before retrying...`);
  146.  
  147. await wait(w * 2);
  148. return await recurse();
  149. } else {
  150. return log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
  151. }
  152. }
  153.  
  154. const data = await resp.json();
  155. const total = data.total_results;
  156. if (!grandTotal) grandTotal = total;
  157. const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true));
  158. const messagesToDelete = discoveredMessages.filter(msg => {
  159. return msg.type === 0 || msg.type === 6 || (msg.pinned && includePinned);
  160. });
  161. const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id));
  162.  
  163. const end = () => {
  164. log.success(`Ended at ${new Date().toLocaleString()}! Total time: ${msToHMS(Date.now() - start.getTime())}`);
  165. printDelayStats();
  166. log.verb(`Rate Limited: ${throttledCount} times. Total time throttled: ${msToHMS(throttledTotalTime)}.`);
  167. log.debug(`Deleted ${delCount} messages, ${failCount} failed.\n`);
  168. }
  169.  
  170. const etr = msToHMS((searchDelay * Math.round(total / 25)) + ((deleteDelay + avgPing) * total));
  171. log.info(`Total messages found: ${data.total_results}`, `(Messages in current page: ${data.messages.length}, To be deleted: ${messagesToDelete.length}, System: ${skippedMessages.length})`, `offset: ${offset}`);
  172. printDelayStats();
  173. log.verb(`Estimated time remaining: ${etr}`)
  174.  
  175.  
  176. if (messagesToDelete.length > 0) {
  177.  
  178. if (++iterations < 1) {
  179. log.verb(`Waiting for your confirmation...`);
  180. if (!await ask(`Do you want to delete ~${total} messages?\nEstimated time: ${etr}\n\n---- Preview ----\n` +
  181. messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n')))
  182. return end(log.error('Aborted by you!'));
  183. log.verb(`OK`);
  184. }
  185.  
  186. for (let i = 0; i < messagesToDelete.length; i += BATCH_SIZE) {
  187. const batch = messagesToDelete.slice(i, i + BATCH_SIZE);
  188. for (let j = 0; j < batch.length; j++) {
  189. const message = batch[j];
  190. if (stopHndl && stopHndl() === false) return end(log.error('Stopped by you!'));
  191.  
  192. log.debug(`${((delCount + 1) / grandTotal * 100).toFixed(2)}% (${delCount + 1}/${grandTotal})`,
  193. `Deleting ID:${redact(message.id)} <b>${redact(message.author.username + '#' + message.author.discriminator)} <small>(${redact(new Date(message.timestamp).toLocaleString())})</small>:</b> <i>${redact(message.content).replace(/\n/g, '↵')}</i>`,
  194. message.attachments.length ? redact(JSON.stringify(message.attachments)) : '');
  195. if (onProgress) onProgress(delCount + 1, grandTotal);
  196.  
  197. let resp;
  198. try {
  199. const s = Date.now();
  200. const API_DELETE_URL = `https://discord.com/api/v6/channels/${message.channel_id}/messages/${message.id}`;
  201. resp = await fetch(API_DELETE_URL, {
  202. headers,
  203. method: 'DELETE'
  204. });
  205. lastPing = (Date.now() - s);
  206. avgPing = (avgPing * 0.9) + (lastPing * 0.1);
  207. delCount++;
  208. if (onProgress) onProgress(delCount, grandTotal); // Update progress after each delete
  209. } catch (err) {
  210. log.error('Delete request throwed an error:', err);
  211. log.verb('Related object:', redact(JSON.stringify(message)));
  212. failCount++;
  213. }
  214.  
  215. if (!resp.ok) {
  216. // deleting messages too fast
  217. if (resp.status === 429) {
  218. const w = (await resp.json()).retry_after;
  219. throttledCount++;
  220. throttledTotalTime += w;
  221. adjustDelay(w); // Adjust delay based on retry_after value
  222. log.warn(`Being rate limited by the API for ${w}ms! Adjusted delete delay to ${deleteDelay}ms.`);
  223. printDelayStats();
  224. log.verb(`Cooling down for ${w * 2}ms before retrying...`);
  225. await wait(w * 2);
  226. j--; // retry
  227. } else {
  228. log.error(`Error deleting message, API responded with status ${resp.status}!`, await resp.json());
  229. log.verb('Related object:', redact(JSON.stringify(message)));
  230. failCount++;
  231. }
  232. }
  233.  
  234. await wait(deleteDelay);
  235. }
  236. }
  237.  
  238. if (skippedMessages.length > 0) {
  239. grandTotal -= skippedMessages.length;
  240. offset += skippedMessages.length;
  241. log.verb(`Found ${skippedMessages.length} system messages! Decreasing grandTotal to ${grandTotal} and increasing offset to ${offset}.`);
  242. }
  243.  
  244. log.verb(`Searching next messages in ${searchDelay}ms...`, (offset ? `(offset: ${offset})` : ''));
  245. await wait(searchDelay);
  246.  
  247. if (stopHndl && stopHndl() === false) return end(log.error('Stopped by you!'));
  248.  
  249. return await recurse();
  250. } else {
  251. if (total - offset > 0) {
  252. log.warn('API returned an empty page, but there are still messages to process. Continuing...');
  253. offset += 25; // Increment offset to continue pagination
  254. await wait(searchDelay);
  255. return await recurse();
  256. }
  257. return end();
  258. }
  259. }
  260.  
  261. log.success(`\nStarted at ${start.toLocaleString()}`);
  262. log.debug(`authorId="${redact(authorId)}" guildId="${redact(guildId)}" channelId="${redact(channelId)}" minId="${redact(minId)}" maxId="${redact(maxId)}" hasLink=${!!hasLink} hasFile=${!!hasFile}`);
  263. if (onProgress) onProgress(null, 1);
  264. return await recurse();
  265. }
  266.  
  267. //---- User interface ----//
  268.  
  269. let popover;
  270. let btn;
  271. let stop;
  272.  
  273. function initUI() {
  274.  
  275. // Keep the tab active by periodically triggering DOM mutations
  276. const keepAliveDiv = document.createElement('div');
  277. keepAliveDiv.id = 'keep-alive';
  278. keepAliveDiv.style.display = 'none';
  279. document.body.appendChild(keepAliveDiv);
  280.  
  281. let keepAliveObserver = new MutationObserver(() => {});
  282. keepAliveObserver.observe(keepAliveDiv, { attributes: true });
  283.  
  284. setInterval(() => {
  285. keepAliveDiv.classList.toggle('active');
  286. }, 1000);
  287.  
  288.  
  289. const insertCss = (css) => {
  290. const style = document.createElement('style');
  291. style.appendChild(document.createTextNode(css));
  292. document.head.appendChild(style);
  293. return style;
  294. }
  295.  
  296. const createElm = (html) => {
  297. const temp = document.createElement('div');
  298. temp.innerHTML = html;
  299. return temp.removeChild(temp.firstElementChild);
  300. }
  301.  
  302. insertCss(`
  303. #deletecord-btn{position: relative; height: 24px;width: auto;-webkit-box-flex: 0;-ms-flex: 0 0 auto;flex: 0 0 auto;margin: 0 8px;cursor:pointer; color: var(--interactive-normal);}
  304. #deletecord{position:fixed;top:100px;right:10px;bottom:10px;width:780px;z-index:99;color:var(--text-normal);background-color:var(--background-secondary);box-shadow:var(--elevation-stroke),var(--elevation-high);border-radius:4px;display:flex;flex-direction:column}
  305. #deletecord a{color:#00b0f4}
  306. #deletecord.redact .priv{display:none!important}
  307. #deletecord:not(.redact) .mask{display:none!important}
  308. #deletecord.redact [priv]{-webkit-text-security:disc!important}
  309. #deletecord .toolbar span{margin-right:8px}
  310. #deletecord button,#deletecord .btn{color:#fff;background:#7289da;border:0;border-radius:4px;font-size:14px}
  311. #deletecord button:disabled{display:none}
  312. #deletecord input[type="text"],#deletecord input[type="search"],#deletecord input[type="password"],#deletecord input[type="datetime-local"],#deletecord input[type="number"]{background-color:#202225;color:#b9bbbe;border-radius:4px;border:0;padding:0 .5em;height:24px;width:144px;margin:2px}
  313. #deletecord input#file{display:none}
  314. #deletecord hr{border-color:rgba(255,255,255,0.1)}
  315. #deletecord .header{padding:12px 16px;background-color:var(--background-tertiary);color:var(--text-muted)}
  316. #deletecord .form{padding:8px;background:var(--background-secondary);box-shadow:0 1px 0 rgba(0,0,0,.2),0 1.5px 0 rgba(0,0,0,.05),0 2px 0 rgba(0,0,0,.05)}
  317. #deletecord .logarea{overflow:auto;font-size:.75rem;font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;flex-grow:1;padding:10px}
  318. `);
  319.  
  320. popover = createElm(`
  321. <div id="deletecord" style="display:none;">
  322. <div class="header">
  323. deletecord - mass delete messages
  324. </div>
  325. <div class="form">
  326. <div style="display:flex;flex-wrap:wrap;">
  327. <span>Authorization <a
  328. href="https://github.com/bekkibau/deletecord/wiki/authToken" title="Help"
  329. target="_blank">?</a> <button id="getToken">get</button><br>
  330. <input type="password" id="authToken" placeholder="Auth Token" autofocus>*<br>
  331. <span>Author <a href="https://github.com/bekkibau/deletecord/wiki/authorId"
  332. title="Help" target="_blank">?</a> <button id="getAuthor">get</button></span>
  333. <br><input id="authorId" type="text" placeholder="Author ID" priv></span>
  334. <span>Guild/Channel <a
  335. href="https://github.com/bekkibau/deletecord/wiki/channelId" title="Help"
  336. target="_blank">?</a>
  337. <button id="getGuildAndChannel">get</button><br>
  338. <input id="guildId" type="text" placeholder="Guild ID" priv><br>
  339. <input id="channelId" type="text" placeholder="Channel ID" priv><br>
  340. <label><input id="includeNsfw" type="checkbox">NSFW Channel</label><br><br>
  341. <label for="file" title="Import list of channels from messages/index.json file"> Import: <span
  342. class="btn">...</span> <input id="file" type="file" accept="application/json,.json"></label>
  343. </span><br>
  344. <span>Range <a href="https://github.com/bekkibau/deletecord/wiki/messageId"
  345. title="Help" target="_blank">?</a><br>
  346. <input id="minDate" type="datetime-local" title="After" style="width:auto;"><br>
  347. <input id="maxDate" type="datetime-local" title="Before" style="width:auto;"><br>
  348. <input id="minId" type="text" placeholder="After message with Id" priv><br>
  349. <input id="maxId" type="text" placeholder="Before message with Id" priv><br>
  350. </span>
  351. <span>Search messages <a
  352. href="https://github.com/bekkibau/deletecord/wiki/filters" title="Help"
  353. target="_blank">?</a><br>
  354. <input id="content" type="text" placeholder="Containing text" priv><br>
  355. <label><input id="hasLink" type="checkbox">has: link</label><br>
  356. <label><input id="hasFile" type="checkbox">has: file</label><br>
  357. <label><input id="includePinned" type="checkbox">Include pinned</label>
  358. </span><br>
  359. <span>Search Delay <a
  360. href="https://github.com/bekkibau/deletecord/wiki/delay" title="Help"
  361. target="_blank">?</a><br>
  362. <input id="searchDelay" type="number" value="100" step="100"><br>
  363. </span>
  364. <span>Delete Delay <a
  365. href="https://github.com/bekkibau/deletecord/wiki/delay" title="Help"
  366. target="_blank">?</a><br>
  367. <input id="deleteDelay" type="number" value="1000" step="100">
  368. </span>
  369. </div>
  370. <hr>
  371. <button id="start" style="background:#43b581;width:80px;">Start</button>
  372. <button id="stop" style="background:#f04747;width:80px;" disabled>Stop</button>
  373. <button id="clear" style="width:80px;">Clear log</button>
  374. <label><input id="autoScroll" type="checkbox" checked>Auto scroll</label>
  375. <label title="Hide sensitive information for taking screenshots"><input id="redact" type="checkbox">Screenshot
  376. mode</label>
  377. <progress id="progress" style="display:none;"></progress> <span class="percent"></span>
  378. </div>
  379. <pre class="logarea">
  380. <center>Star this project on <a href="https://github.com/bekkibau/deletecord" target="_blank">github.com/bekkibau/deletecord</a>!\n\n
  381. <a href="https://github.com/bekkibau/deletecord/issues" target="_blank">Issues or help</a>
  382. </center>
  383. </pre>
  384. </div>
  385. `);
  386.  
  387. document.body.appendChild(popover);
  388.  
  389. btn = createElm(`<div id="deletecord-btn" tabindex="0" role="button" aria-label="Delete Messages" title="Delete Messages">
  390. <svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
  391. <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
  392. <path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
  393. </svg>
  394. <br><progress style="display:none; width:24px;"></progress>
  395. </div>`);
  396.  
  397. btn.onclick = function togglePopover() {
  398. if (popover.style.display !== 'none') {
  399. popover.style.display = 'none';
  400. btn.style.color = 'var(--interactive-normal)';
  401. }
  402. else {
  403. popover.style.display = '';
  404. btn.style.color = '#f04747';
  405. }
  406. };
  407.  
  408. function mountBtn() {
  409. const toolbar = document.querySelector('[class^=toolbar]');
  410. if (toolbar) toolbar.appendChild(btn);
  411. }
  412.  
  413. const observer = new MutationObserver(function (_mutationsList, _observer) {
  414. if (!document.body.contains(btn)) mountBtn(); // re-mount the button to the toolbar
  415. });
  416. observer.observe(document.body, { attributes: false, childList: true, subtree: true });
  417.  
  418. mountBtn();
  419.  
  420. const $ = s => popover.querySelector(s);
  421. const logArea = $('pre');
  422. const startBtn = $('button#start');
  423. const stopBtn = $('button#stop');
  424. const autoScroll = $('#autoScroll');
  425.  
  426. startBtn.onclick = async e => {
  427. const authToken = $('input#authToken').value.trim();
  428. const authorId = $('input#authorId').value.trim();
  429. const guildId = $('input#guildId').value.trim();
  430. const channelIds = $('input#channelId').value.trim().split(/\s*,\s*/);
  431. const minId = $('input#minId').value.trim();
  432. const maxId = $('input#maxId').value.trim();
  433. const minDate = $('input#minDate').value.trim();
  434. const maxDate = $('input#maxDate').value.trim();
  435. const content = $('input#content').value.trim();
  436. const hasLink = $('input#hasLink').checked;
  437. const hasFile = $('input#hasFile').checked;
  438. const includeNsfw = $('input#includeNsfw').checked;
  439. const includePinned = $('input#includePinned').checked;
  440. const searchDelay = parseInt($('input#searchDelay').value.trim());
  441. const deleteDelay = parseInt($('input#deleteDelay').value.trim());
  442. const progress = $('#progress');
  443. const progress2 = btn.querySelector('progress');
  444. const percent = $('.percent');
  445.  
  446. const fileSelection = $("input#file");
  447. fileSelection.addEventListener("change", () => {
  448. const files = fileSelection.files;
  449. const channelIdField = $('input#channelId');
  450. if (files.length > 0) {
  451. const file = files[0];
  452. file.text().then(text => {
  453. let json = JSON.parse(text);
  454. let channels = Object.keys(json);
  455. channelIdField.value = channels.join(",");
  456. });
  457. }
  458. }, false);
  459.  
  460. const stopHndl = () => !(stop === true);
  461.  
  462. const onProg = (value, max) => {
  463. if (value && max && value > max) max = value;
  464. progress.setAttribute('max', max);
  465. progress.value = value;
  466. progress.style.display = max ? '' : 'none';
  467. progress2.setAttribute('max', max);
  468. progress2.value = value;
  469. progress2.style.display = max ? '' : 'none';
  470. percent.innerHTML = value && max ? Math.round(value / max * 100) + '%' : '';
  471. };
  472.  
  473.  
  474. stop = stopBtn.disabled = !(startBtn.disabled = true);
  475. for (let i = 0; i < channelIds.length; i++) {
  476. await deleteMessages(authToken, authorId, guildId, channelIds[i], minId || minDate, maxId || maxDate, content, hasLink, hasFile, includeNsfw, includePinned, searchDelay, deleteDelay, logger, stopHndl, onProg);
  477. stop = stopBtn.disabled = !(startBtn.disabled = false);
  478. }
  479. };
  480. stopBtn.onclick = e => stop = stopBtn.disabled = !(startBtn.disabled = false);
  481. $('button#clear').onclick = e => { logArea.innerHTML = ''; };
  482. $('button#getToken').onclick = e => {
  483. window.dispatchEvent(new Event('beforeunload'));
  484. const ls = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  485. $('input#authToken').value = JSON.parse(localStorage.token);
  486. };
  487. $('button#getAuthor').onclick = e => {
  488. $('input#authorId').value = JSON.parse(localStorage.user_id_cache);
  489. };
  490. $('button#getGuildAndChannel').onclick = e => {
  491. const m = location.href.match(/channels\/([\w@]+)\/(\d+)/);
  492. $('input#guildId').value = m[1];
  493. $('input#channelId').value = m[2];
  494. };
  495. $('#redact').onchange = e => {
  496. popover.classList.toggle('redact') &&
  497. window.alert('This will attempt to hide personal information, but make sure to double check before sharing screenshots.');
  498. };
  499.  
  500. const logger = (type = '', args) => log.add(type, args); // Use log.add method
  501.  
  502. // fixLocalStorage
  503. window.localStorage = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  504. }
  505.  
  506. initUI();
  507.  
  508.  
  509. //END.