Undiscord

Delete all messages in a Discord channel or DM (Bulk deletion)

  1. // ==UserScript==
  2. // @name Undiscord
  3. // @description Delete all messages in a Discord channel or DM (Bulk deletion)
  4. // @version 5.2.4
  5. // @author louga31
  6. // @homepageURL https://github.com/louga31/undiscord
  7. // @supportURL https://github.com/louga31/undiscord/discussions
  8. // @match https://*.discord.com/app
  9. // @match https://*.discord.com/channels/*
  10. // @match https://*.discord.com/login
  11. // @license MIT
  12. // @namespace https://github.com/louga31/deleteDiscordMessages
  13. // @icon https://louga31.github.io/undiscord/images/icon128.png
  14. // @contributionURL https://www.buymeacoffee.com/vitim
  15. // @grant none
  16. // ==/UserScript==
  17. (function () {
  18. 'use strict';
  19.  
  20. /* rollup-plugin-baked-env */
  21. const VERSION = "5.2.3";
  22.  
  23. var themeCss = (`
  24. /* undiscord window */
  25. #undiscord.browser { box-shadow: var(--elevation-stroke), var(--elevation-high); overflow: hidden; }
  26. #undiscord.container,
  27. #undiscord .container { background-color: var(--background-secondary); border-radius: 8px; box-sizing: border-box; cursor: default; flex-direction: column; }
  28. #undiscord .header { background-color: var(--background-tertiary); height: 48px; align-items: center; min-height: 48px; padding: 0 16px; display: flex; color: var(--header-secondary); cursor: grab; }
  29. #undiscord .header .icon { color: var(--interactive-normal); margin-right: 8px; flex-shrink: 0; width: 24; height: 24; }
  30. #undiscord .header .icon:hover { color: var(--interactive-hover); }
  31. #undiscord .header h3 { font-size: 16px; line-height: 20px; font-weight: 500; font-family: var(--font-display); color: var(--header-primary); flex-shrink: 0; margin-right: 16px; }
  32. #undiscord .spacer { flex-grow: 1; }
  33. #undiscord .header .vert-divider { width: 1px; height: 24px; background-color: var(--background-modifier-accent); margin-right: 16px; flex-shrink: 0; }
  34. #undiscord legend,
  35. #undiscord label { color: var(--header-secondary); font-size: 12px; line-height: 16px; font-weight: 500; text-transform: uppercase; cursor: default; font-family: var(--font-display); margin-bottom: 8px; }
  36. #undiscord .multiInput { display: flex; align-items: center; font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; }
  37. #undiscord .multiInput :first-child { flex-grow: 1; }
  38. #undiscord .multiInput button:last-child { margin-right: 4px; }
  39. #undiscord .input { font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; padding: 10px; height: 40px; }
  40. #undiscord fieldset { margin-top: 16px; }
  41. #undiscord .input-wrapper { display: flex; align-items: center; font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; }
  42. #undiscord input[type="text"],
  43. #undiscord input[type="search"],
  44. #undiscord input[type="password"],
  45. #undiscord input[type="datetime-local"],
  46. #undiscord input[type="number"],
  47. #undiscord input[type="range"] { font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; padding: 10px; height: 40px; }
  48. #undiscord .divider,
  49. #undiscord hr { border: none; margin-bottom: 24px; padding-bottom: 4px; border-bottom: 1px solid var(--background-modifier-accent); }
  50. #undiscord .sectionDescription { margin-bottom: 16px; color: var(--header-secondary); font-size: 14px; line-height: 20px; font-weight: 400; }
  51. #undiscord a { color: var(--text-link); text-decoration: none; }
  52. #undiscord .btn,
  53. #undiscord button { position: relative; display: flex; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; box-sizing: border-box; background: none; border: none; border-radius: 3px; font-size: 14px; font-weight: 500; line-height: 16px; padding: 2px 16px; user-select: none; /* sizeSmall */ width: 60px; height: 32px; min-width: 60px; min-height: 32px; /* lookFilled colorPrimary */ color: rgb(255, 255, 255); background-color: var(--button-secondary-background); }
  54. #undiscord .sizeMedium { width: 96px; height: 38px; min-width: 96px; min-height: 38px; }
  55. #undiscord .sizeMedium.icon { width: 38px; min-width: 38px; }
  56. #undiscord sup { vertical-align: top; }
  57. /* lookFilled colorPrimary */
  58. #undiscord .accent { background-color: var(--brand-experiment); }
  59. #undiscord .danger { background-color: var(--button-danger-background); }
  60. #undiscord .positive { background-color: var(--button-positive-background); }
  61. #undiscord .info { font-size: 12px; line-height: 16px; padding: 8px 10px; color: var(--text-muted); }
  62. /* Scrollbar */
  63. #undiscord .scroll::-webkit-scrollbar { width: 8px; height: 8px; }
  64. #undiscord .scroll::-webkit-scrollbar-corner { background-color: transparent; }
  65. #undiscord .scroll::-webkit-scrollbar-thumb { background-clip: padding-box; border: 2px solid transparent; border-radius: 4px; background-color: var(--scrollbar-thin-thumb); min-height: 40px; }
  66. #undiscord .scroll::-webkit-scrollbar-track { border-color: var(--scrollbar-thin-track); background-color: var(--scrollbar-thin-track); border: 2px solid var(--scrollbar-thin-track); }
  67. /* fade scrollbar */
  68. #undiscord .scroll::-webkit-scrollbar-thumb,
  69. #undiscord .scroll::-webkit-scrollbar-track { visibility: hidden; }
  70. #undiscord .scroll:hover::-webkit-scrollbar-thumb,
  71. #undiscord .scroll:hover::-webkit-scrollbar-track { visibility: visible; }
  72. /**** functional classes ****/
  73. #undiscord.redact .priv { display: none !important; }
  74. #undiscord.redact x:not(:active) { color: transparent !important; background-color: var(--primary-700) !important; cursor: default; user-select: none; }
  75. #undiscord.redact x:hover { position: relative; }
  76. #undiscord.redact x:hover::after { content: "Redacted information (Streamer mode: ON)"; position: absolute; display: inline-block; top: -32px; left: -20px; padding: 4px; width: 150px; font-size: 8pt; text-align: center; white-space: pre-wrap; background-color: var(--background-floating); -webkit-box-shadow: var(--elevation-high); box-shadow: var(--elevation-high); color: var(--text-normal); border-radius: 5px; pointer-events: none; }
  77. #undiscord.redact [priv] { -webkit-text-security: disc !important; }
  78. #undiscord :disabled { display: none; }
  79. /**** layout and utility classes ****/
  80. #undiscord,
  81. #undiscord * { box-sizing: border-box; }
  82. #undiscord .col { display: flex; flex-direction: column; }
  83. #undiscord .row { display: flex; flex-direction: row; align-items: center; }
  84. #undiscord .mb1 { margin-bottom: 8px; }
  85. #undiscord .log { margin-bottom: 0.25em; }
  86. #undiscord .log-debug { color: inherit; }
  87. #undiscord .log-info { color: #00b0f4; }
  88. #undiscord .log-verb { color: #72767d; }
  89. #undiscord .log-warn { color: #faa61a; }
  90. #undiscord .log-error { color: #f04747; }
  91. #undiscord .log-success { color: #43b581; }
  92. `);
  93.  
  94. var mainCss = (`
  95. /**** Undiscord Button ****/
  96. #undicord-btn { position: relative; width: auto; height: 24px; margin: 0 8px; cursor: pointer; color: var(--interactive-normal); flex: 0 0 auto; }
  97. #undicord-btn progress { position: absolute; top: 23px; left: -4px; width: 32px; height: 12px; display: none; }
  98. #undicord-btn.running { color: var(--button-danger-background) !important; }
  99. #undicord-btn.running progress { display: block; }
  100. /**** Undiscord Interface ****/
  101. #undiscord { position: fixed; z-index: 100; top: 58px; right: 10px; display: flex; flex-direction: column; width: 800px; height: 80vh; min-width: 610px; max-width: 100vw; min-height: 448px; max-height: 100vh; color: var(--text-normal); border-radius: 4px; background-color: var(--background-secondary); box-shadow: var(--elevation-stroke), var(--elevation-high); will-change: top, left, width, height; }
  102. #undiscord .header .icon { cursor: pointer; }
  103. #undiscord .window-body { height: calc(100% - 48px); }
  104. #undiscord .sidebar { overflow: hidden scroll; overflow-y: auto; width: 270px; min-width: 250px; height: 100%; max-height: 100%; padding: 8px; background: var(--background-secondary); }
  105. #undiscord .sidebar legend,
  106. #undiscord .sidebar label { display: block; width: 100%; }
  107. #undiscord .main { display: flex; max-width: calc(100% - 250px); background-color: var(--background-primary); flex-grow: 1; }
  108. #undiscord.hide-sidebar .sidebar { display: none; }
  109. #undiscord.hide-sidebar .main { max-width: 100%; }
  110. #undiscord #logArea { font-family: Consolas, Liberation Mono, Menlo, Courier, monospace; font-size: 0.75rem; overflow: auto; padding: 10px; user-select: text; flex-grow: 1; flex-grow: 1; cursor: auto; }
  111. #undiscord .tbar { padding: 8px; background-color: var(--background-secondary-alt); }
  112. #undiscord .tbar button { margin-right: 4px; margin-bottom: 4px; }
  113. #undiscord .footer { cursor: se-resize; padding-right: 30px; }
  114. #undiscord .footer #progressPercent { padding: 0 1em; font-size: small; color: var(--interactive-muted); flex-grow: 1; }
  115. .resize-handle { position: absolute; bottom: -15px; right: -15px; width: 30px; height: 30px; transform: rotate(-45deg); background: repeating-linear-gradient(0, var(--background-modifier-accent), var(--background-modifier-accent) 1px, transparent 2px, transparent 4px); cursor: nwse-resize; }
  116. /**** Elements ****/
  117. #undiscord summary { font-size: 16px; font-weight: 500; line-height: 20px; position: relative; overflow: hidden; margin-bottom: 2px; padding: 6px 10px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; color: var(--interactive-normal); border-radius: 4px; flex-shrink: 0; }
  118. #undiscord fieldset { padding-left: 8px; }
  119. #undiscord legend a { float: right; text-transform: initial; }
  120. #undiscord progress { height: 8px; margin-top: 4px; flex-grow: 1; }
  121. #undiscord .importJson { display: flex; flex-direction: row; }
  122. #undiscord .importJson button { margin-left: 5px; width: fit-content; }
  123. `);
  124.  
  125. var dragCss = (`
  126. [name^="grab-"] { position: absolute; --size: 6px; --corner-size: 16px; --offset: -1px; z-index: 9; }
  127. [name^="grab-"]:hover{ background: rgba(128,128,128,0.1); }
  128. [name="grab-t"] { top: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-top: var(--offset); cursor: ns-resize; }
  129. [name="grab-r"] { top: var(--corner-size); bottom: var(--corner-size); right: 0px; width: var(--size); margin-right: var(--offset);
  130. cursor: ew-resize; }
  131. [name="grab-b"] { bottom: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-bottom: var(--offset); cursor: ns-resize; }
  132. [name="grab-l"] { top: var(--corner-size); bottom: var(--corner-size); left: 0px; width: var(--size); margin-left: var(--offset); cursor: ew-resize; }
  133. [name="grab-tl"] { top: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-left: var(--offset); cursor: nwse-resize; }
  134. [name="grab-tr"] { top: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-right: var(--offset); cursor: nesw-resize; }
  135. [name="grab-br"] { bottom: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-right: var(--offset); cursor: nwse-resize; }
  136. [name="grab-bl"] { bottom: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-left: var(--offset); cursor: nesw-resize; }
  137. `);
  138.  
  139. var buttonHtml = (`
  140. <div id="undicord-btn" tabindex="0" role="button" aria-label="Delete Messages" title="Delete Messages with Undiscord">
  141. <svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
  142. <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
  143. <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>
  144. </svg>
  145. <progress></progress>
  146. </div>
  147. `);
  148.  
  149. var undiscordTemplate = (`
  150. <div id="undiscord" class="browser container redact" style="display:none;">
  151. <div class="header">
  152. <svg class="icon" aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
  153. <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
  154. <path fill="currentColor"
  155. 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">
  156. </path>
  157. </svg>
  158. <h3>Undiscord</h3>
  159. <div class="vert-divider"></div>
  160. <span> Bulk delete messages</span>
  161. <div class="spacer"></div>
  162. <div id="hide" class="icon" aria-label="Close" role="button" tabindex="0">
  163. <svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
  164. <path fill="currentColor"
  165. d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z">
  166. </path>
  167. </svg>
  168. </div>
  169. </div>
  170. <div class="window-body" style="display: flex; flex-direction: row;">
  171. <div class="sidebar scroll">
  172. <details open>
  173. <summary>General</summary>
  174. <fieldset>
  175. <legend>
  176. Author ID
  177. <a href="{{WIKI}}/authorId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  178. </legend>
  179. <div class="multiInput">
  180. <div class="input-wrapper">
  181. <input class="input" id="authorId" type="text" priv>
  182. </div>
  183. <button id="getAuthor">me</button>
  184. </div>
  185. </fieldset>
  186. <hr>
  187. <fieldset>
  188. <legend>
  189. Server ID
  190. <a href="{{WIKI}}/guildId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  191. </legend>
  192. <div class="multiInput">
  193. <div class="input-wrapper">
  194. <input class="input" id="guildId" type="text" priv>
  195. </div>
  196. <button id="getGuild">current</button>
  197. </div>
  198. </fieldset>
  199. <fieldset>
  200. <legend>
  201. Channel ID
  202. <a href="{{WIKI}}/channelId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  203. </legend>
  204. <div class="multiInput mb1">
  205. <div class="input-wrapper">
  206. <input class="input" id="channelId" type="text" priv>
  207. </div>
  208. <button id="getChannel">current</button>
  209. </div>
  210. <div class="sectionDescription">
  211. <label class="row"><input id="includeNsfw" type="checkbox">This is a NSFW channel</label>
  212. </div>
  213. </fieldset>
  214. </details>
  215. <details>
  216. <summary>Wipe Archive</summary>
  217. <fieldset>
  218. <legend>
  219. Import index.json
  220. <a href="{{WIKI}}/importJson" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  221. </legend>
  222. <div class="input-wrapper">
  223. <input type="file" id="importJsonInput" accept="application/json,.json" style="width:100%";>
  224. </div>
  225. <div class="sectionDescription">
  226. <br>
  227. After requesting your data from discord, you can import it here.<br>
  228. Select the "messages/index.json" file from the discord archive.
  229. </div>
  230. <div class="sectionDescription">
  231. <label><input id="includeServers" type="checkbox">Include servers</label>
  232. </div>
  233. </fieldset>
  234. </details>
  235. <hr>
  236. <details>
  237. <summary>Filter</summary>
  238. <fieldset>
  239. <legend>
  240. Search
  241. <a href="{{WIKI}}/filters" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  242. </legend>
  243. <div class="input-wrapper">
  244. <input id="search" type="text" placeholder="Containing text" priv>
  245. </div>
  246. <div class="sectionDescription">
  247. Only delete messages that contain the text
  248. </div>
  249. <div class="sectionDescription">
  250. <label><input id="hasLink" type="checkbox">has: link</label>
  251. </div>
  252. <div class="sectionDescription">
  253. <label><input id="hasFile" type="checkbox">has: file</label>
  254. </div>
  255. <div class="sectionDescription">
  256. <label><input id="includePinned" type="checkbox" checked>Include pinned</label>
  257. </div>
  258. <div class="sectionDescription">
  259. <label><input id="includeApplications" type="checkbox">Include Applications</label>
  260. </div>
  261. </fieldset>
  262. <hr>
  263. <fieldset>
  264. <legend>
  265. Pattern
  266. <a href="{{WIKI}}/pattern" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  267. </legend>
  268. <div class="sectionDescription">
  269. Delete messages that match the regular expression
  270. </div>
  271. <div class="input-wrapper">
  272. <span class="info">/</span>
  273. <input id="pattern" type="text" placeholder="regular expression" priv>
  274. <span class="info">/</span>
  275. </div>
  276. </fieldset>
  277. </details>
  278. <details>
  279. <summary>Messages interval</summary>
  280. <fieldset>
  281. <legend>
  282. Interval of messages
  283. <a href="{{WIKI}}/messageId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  284. </legend>
  285. <div class="multiInput mb1">
  286. <div class="input-wrapper">
  287. <input id="minId" type="text" placeholder="After a message" priv>
  288. </div>
  289. <button id="pickMessageAfter">Pick</button>
  290. </div>
  291. <div class="multiInput">
  292. <div class="input-wrapper">
  293. <input id="maxId" type="text" placeholder="Before a message" priv>
  294. </div>
  295. <button id="pickMessageBefore">Pick</button>
  296. </div>
  297. <div class="sectionDescription">
  298. Specify an interval to delete messages.
  299. </div>
  300. </fieldset>
  301. </details>
  302. <details>
  303. <summary>Date interval</summary>
  304. <fieldset>
  305. <legend>
  306. After date
  307. <a href="{{WIKI}}/dateRange" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  308. </legend>
  309. <div class="input-wrapper mb1">
  310. <input id="minDate" type="datetime-local" title="Messages posted AFTER this date">
  311. </div>
  312. <legend>
  313. Before date
  314. <a href="{{WIKI}}/dateRange" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  315. </legend>
  316. <div class="input-wrapper">
  317. <input id="maxDate" type="datetime-local" title="Messages posted BEFORE this date">
  318. </div>
  319. <div class="sectionDescription">
  320. Delete messages that were posted between the two dates.
  321. </div>
  322. <div class="sectionDescription">
  323. * Filtering by date doesn't work if you use the "Messages interval".
  324. </div>
  325. </fieldset>
  326. </details>
  327. <hr>
  328. <details>
  329. <summary>Advanced settings</summary>
  330. <fieldset>
  331. <legend>
  332. Search delay
  333. <a href="{{WIKI}}/delay" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  334. </legend>
  335. <div class="input-wrapper">
  336. <input id="searchDelay" type="range" value="1400" step="100" min="100" max="60000">
  337. <div id="searchDelayValue"></div>
  338. </div>
  339. </fieldset>
  340. <fieldset>
  341. <legend>
  342. Delete delay
  343. <a href="{{WIKI}}/delay" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  344. </legend>
  345. <div class="input-wrapper">
  346. <input id="deleteDelay" type="range" value="1400" step="50" min="50" max="10000">
  347. <div id="deleteDelayValue"></div>
  348. </div>
  349. <br>
  350. <div class="sectionDescription">
  351. This will affect the speed in which the messages are deleted.
  352. Use the help link for more information.
  353. </div>
  354. </fieldset>
  355. <label><input id="rateLimitPrevention" type="checkbox" checked>Rate limit prevention</label>
  356. <hr>
  357. <fieldset>
  358. <legend>
  359. Authorization Token
  360. <a href="{{WIKI}}/authToken" title="Help" target="_blank" rel="noopener noreferrer">help</a>
  361. </legend>
  362. <div class="multiInput">
  363. <div class="input-wrapper">
  364. <input class="input" id="token" type="password" autocomplete="dont" priv>
  365. </div>
  366. <button id="getToken">fill</button>
  367. </div>
  368. </fieldset>
  369. </details>
  370. <hr>
  371. <div></div>
  372. <div class="info">
  373. Undiscord {{VERSION}}
  374. <br> louga31
  375. </div>
  376. </div>
  377. <div class="main col">
  378. <div class="tbar col">
  379. <div class="row">
  380. <button id="toggleSidebar" class="sizeMedium icon">☰</button>
  381. <button id="start" class="sizeMedium danger" style="width: 150px;" title="Start the deletion process">▶︎ Delete</button>
  382. <button id="stop" class="sizeMedium" title="Stop the deletion process" disabled>🛑 Stop</button>
  383. <button id="clear" class="sizeMedium">Clear log</button>
  384. <label class="row" title="Hide sensitive information on your screen for taking screenshots">
  385. <input id="redact" type="checkbox" checked> Streamer mode
  386. </label>
  387. </div>
  388. <div class="row">
  389. <progress id="progressBar" style="display:none;"></progress>
  390. </div>
  391. </div>
  392. <pre id="logArea" class="logarea scroll">
  393. <div class="" style="background: var(--background-mentioned); padding: .5em;">Notice: Undiscord may be working slower than usual and<wbr>require multiple attempts due to a recent Discord update.<br>We're working on a fix, and we thank you for your patience.</div>
  394. <center>
  395. <div>Star <a href="{{HOME}}" target="_blank" rel="noopener noreferrer">this project</a> on GitHub!</div>
  396. <div><a href="{{HOME}}/discussions" target="_blank" rel="noopener noreferrer">Issues or help</a></div>
  397. </center>
  398. </pre>
  399. <div class="tbar footer row">
  400. <div id="progressPercent"></div>
  401. <span class="spacer"></span>
  402. <label>
  403. <input id="trimLog" type="checkbox" checked> Trim log
  404. </label>
  405. <label>
  406. <input id="autoScroll" type="checkbox" checked> Auto scroll
  407. </label>
  408. <div class="resize-handle"></div>
  409. </div>
  410. </div>
  411. </div>
  412. </div>
  413.  
  414. `);
  415.  
  416. const log = {
  417. debug() { return logFn ? logFn('debug', arguments) : console.debug.apply(console, arguments); },
  418. info() { return logFn ? logFn('info', arguments) : console.info.apply(console, arguments); },
  419. verb() { return logFn ? logFn('verb', arguments) : console.log.apply(console, arguments); },
  420. warn() { return logFn ? logFn('warn', arguments) : console.warn.apply(console, arguments); },
  421. error() { return logFn ? logFn('error', arguments) : console.error.apply(console, arguments); },
  422. success() { return logFn ? logFn('success', arguments) : console.info.apply(console, arguments); },
  423. };
  424.  
  425. var logFn; // custom console.log function
  426. const setLogFn = (fn) => logFn = fn;
  427.  
  428. // Web Worker code as a string
  429. const workerScript = `
  430. self.addEventListener('message', function(e) {
  431. const ms = e.data;
  432. setTimeout(() => {
  433. self.postMessage('done');
  434. }, ms);
  435. });
  436. `;
  437. // Create a Blob URL for the Web Worker
  438. const blob = new Blob([workerScript], { type: 'application/javascript' });
  439. const workerUrl = URL.createObjectURL(blob);
  440.  
  441. // Helpers
  442. const wait = ms => {
  443. return new Promise((resolve, reject) => {
  444. const worker = new Worker(workerUrl);
  445. let start = Date.now();
  446. worker.postMessage(ms);
  447. worker.addEventListener('message', function(e) {
  448. if (e.data === 'done') {
  449. let delay = Date.now() - start - ms;
  450. if(delay > 100) console.warn(`This action was delayed ${delay}ms more than it should've, make sure you don't have too many tabs open!`);
  451. resolve();
  452. worker.terminate();
  453. }
  454. });
  455. worker.addEventListener('error', reject);
  456. });
  457. };
  458. const msToHMS = s => `${s / 3.6e6 | 0}h ${(s % 3.6e6) / 6e4 | 0}m ${(s % 6e4) / 1000 | 0}s`;
  459. const escapeHTML = html => String(html).replace(/[&<"']/g, m => ({ '&': '&amp;', '<': '&lt;', '"': '&quot;', '\'': '&#039;' })[m]);
  460. const redact = str => `<x>${escapeHTML(str)}</x>`;
  461. const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&');
  462. const ask = async msg => new Promise(resolve => setTimeout(() => resolve(window.confirm(msg)), 10));
  463. const toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date;
  464. const replaceInterpolations = (str, obj, removeMissing = false) => str.replace(/\{\{([\w_]+)\}\}/g, (m, key) => obj[key] || (removeMissing ? '' : m));
  465.  
  466. const PREFIX$1 = '[UNDISCORD]';
  467.  
  468. /**
  469. * Delete all messages in a Discord channel or DM
  470. * @author louga31 <https://www.github.com/louga31>
  471. * @see https://github.com/louga31/undiscord
  472. */
  473. class UndiscordCore {
  474.  
  475. options = {
  476. authToken: null, // Your authorization token
  477. authorId: null, // Author of the messages you want to delete
  478. guildId: null, // Server were the messages are located
  479. channelId: null, // Channel were the messages are located
  480. minId: null, // Only delete messages after this, leave blank do delete all
  481. maxId: null, // Only delete messages before this, leave blank do delete all
  482. content: null, // Filter messages that contains this text content
  483. hasLink: null, // Filter messages that contains link
  484. hasFile: null, // Filter messages that contains file
  485. includeNsfw: null, // Search in NSFW channels
  486. includeServers: null, // Search in server channels
  487. includePinned: null, // Delete messages that are pinned
  488. pattern: null, // Only delete messages that match the regex (insensitive)
  489. searchDelay: null, // Delay each time we fetch for more messages
  490. includeApplications: null, //Include application/bot messages
  491. deleteDelay: null, // Delay between each delete operation
  492. rateLimitPrevention: null, // Whether rate limit prevention is enabled or not
  493. maxAttempt: 2, // Attempts to delete a single message if it fails
  494. askForConfirmation: true,
  495. };
  496.  
  497. state = {
  498. running: false,
  499. delCount: 0,
  500. failCount: 0,
  501. grandTotal: 0,
  502. offset: {'asc': 0, 'desc': 0},
  503. iterations: 0,
  504. sortOrder: 'asc',
  505. searchedPages: 0,
  506. totalSkippedMessages: 0,
  507. startEmptyPages: -1,
  508.  
  509. _seachResponse: null,
  510. _messagesToDelete: [],
  511. _skippedMessages: [],
  512. };
  513.  
  514. stats = {
  515. startTime: new Date(), // start time
  516. throttledCount: 0, // how many times you have been throttled
  517. throttledTotalTime: 0, // the total amount of time you spent being throttled
  518. lastPing: null, // the most recent ping
  519. avgPing: null, // average ping used to calculate the estimated remaining time
  520. etr: 0,
  521. };
  522.  
  523. // events
  524. onStart = undefined;
  525. onProgress = undefined;
  526. onStop = undefined;
  527.  
  528. resetState() {
  529. this.state = {
  530. running: false,
  531. delCount: 0,
  532. failCount: 0,
  533. grandTotal: 0,
  534. offset: {'asc': 0, 'desc': 0},
  535. iterations: 0,
  536. sortOrder: 'asc',
  537. searchedPages: 0,
  538. totalSkippedMessages: 0,
  539. startEmptyPages: -1,
  540.  
  541. _seachResponse: null,
  542. _messagesToDelete: [],
  543. _skippedMessages: [],
  544. };
  545.  
  546. this.options.askForConfirmation = true;
  547. }
  548.  
  549. /** Automate the deletion process of multiple channels */
  550. async runBatch(queue) {
  551. if (this.state.running) return log.error('Already running!');
  552.  
  553. log.info(`Runnning batch with queue of ${queue.length} jobs`);
  554. for (let i = 0; i < queue.length; i++) {
  555. const job = queue[i];
  556. log.info('Starting job...', `(${i + 1}/${queue.length})`);
  557.  
  558. // set options
  559. this.options = {
  560. ...this.options, // keep current options
  561. ...job, // override with options for that job
  562. };
  563.  
  564. await this.run(true);
  565. if (!this.state.running) break;
  566.  
  567. log.info('Job ended.', `(${i + 1}/${queue.length})`);
  568. this.resetState();
  569. this.options.askForConfirmation = false;
  570. this.state.running = true; // continue running
  571. }
  572.  
  573. log.info('Batch finished.');
  574. this.state.running = false;
  575. }
  576.  
  577. /** Start the deletion process */
  578. async run(isJob = false) {
  579. if (this.state.running && !isJob) return log.error('Already running!');
  580.  
  581. this.state.running = true;
  582. this.stats.startTime = new Date();
  583.  
  584. log.success(`\nStarted at ${this.stats.startTime.toLocaleString()}`);
  585. if (this.onStart) this.onStart(this.state, this.stats);
  586.  
  587. if (!this.options.guildId) {
  588. log.verb('Fetching channel info...');
  589. await this.fetchChannelInfo();
  590. }
  591. if (!this.options.guildId) return; // message is handled in fetchChannelInfo
  592. if (isJob && this.options.guildId !== '@me' && !this.options.includeServers) {
  593. log.warn(`Skipping the channel ${this.options.channelId} as it's a server channel.`);
  594. return;
  595. }
  596.  
  597. log.debug(
  598. `authorId = "${redact(this.options.authorId)}"`,
  599. `guildId = "${redact(this.options.guildId)}"`,
  600. `channelId = "${redact(this.options.channelId)}"`,
  601. `minId = "${redact(this.options.minId)}"`,
  602. `maxId = "${redact(this.options.maxId)}"`,
  603. `hasLink = ${!!this.options.hasLink}`,
  604. `hasFile = ${!!this.options.hasFile}`,
  605. );
  606.  
  607. do {
  608. this.state.iterations++;
  609.  
  610. log.verb('Fetching messages...');
  611. // Search messages
  612. this.state.sortOrder = this.state.sortOrder == 'desc' ? 'asc' : 'desc';
  613. log.verb(`Set sort order to ${this.state.sortOrder} for this search.`);
  614. await this.search();
  615. this.state.searchedPages++;
  616.  
  617. // Process results and find which messages should be deleted
  618. await this.filterResponse();
  619.  
  620. log.verb(
  621. `Grand total: ${this.state.grandTotal}`,
  622. `(Messages in current page: ${this.state._seachResponse.messages.length}`,
  623. `To be deleted: ${this.state._messagesToDelete.length}`,
  624. `Skipped: ${this.state._skippedMessages.length})`,
  625. `offset (asc): ${this.state.offset['asc']}`,
  626. `offset (desc): ${this.state.offset['desc']}`
  627. );
  628. this.printStats();
  629. this.state.totalSkippedMessages += this.state._skippedMessages.length;
  630.  
  631. // Calculate estimated time
  632. this.calcEtr();
  633. log.verb(`Estimated time remaining: ${msToHMS(this.stats.etr)}`);
  634.  
  635. // if there are messages to delete, delete them
  636. if (this.state._messagesToDelete.length > 0) {
  637. this.state.startEmptyPages = -1;
  638.  
  639. if (await this.confirm() === false) {
  640. this.state.running = false; // break out of a job
  641. break; // immmediately stop this iteration
  642. }
  643.  
  644. await this.deleteMessagesFromList();
  645. }
  646. else if (this.state._skippedMessages.length > 0) {
  647. // There are stuff, but nothing to delete (example a page full of system messages)
  648. // check next page until we see a page with nothing in it (end of results).
  649. this.state.startEmptyPages = -1;
  650.  
  651. const oldOffset = this.state.offset[this.state.sortOrder];
  652. this.state.offset[this.state.sortOrder] += this.state._skippedMessages.length;
  653. log.verb('There\'s nothing we can delete on this page, checking next page...');
  654. log.verb(`Skipped ${this.state._skippedMessages.length} out of ${this.state._seachResponse.messages.length} in this page.`, `(Offset for ${this.state.sortOrder} was ${oldOffset}, ajusted to ${this.state.offset[this.state.sortOrder]})`);
  655. }
  656. else {
  657. if (this.state.startEmptyPages == -1) this.state.startEmptyPages = Date.now();
  658. // if the first page we are searching is empty
  659. // or we've been getting empty page responses for the past 30 seconds (enough for Discord to re-index the pages)
  660. // or (deleted messages + failed to delete + total skipped) >= total messages
  661. // ONLY THEN proceed with ending the job
  662. if (this.state.searchedPages == 1 || (Date.now() - this.state.startEmptyPages) > 30 * 1000 || (this.state.delCount + this.state.failCount + this.state.totalSkippedMessages) >= this.state.grandTotal) {
  663. log.verb('Ended because API returned an empty page.');
  664. log.verb('[End state]', this.state);
  665. if (isJob) break; // break without stopping if this is part of a job
  666. this.state.running = false;
  667. } else {
  668. // wait 10 seconds for Discord to re-index the search page before retrying
  669. const waitingTime = 10 * 1000;
  670. log.verb(`API returned an empty page, waiting an extra ${(waitingTime / 1000).toFixed(2)}s before searching again...`);
  671. await wait(waitingTime);
  672. }
  673. }
  674.  
  675. // wait before next page (fix search page not updating fast enough)
  676. log.verb(`Waiting ${(this.options.searchDelay / 1000).toFixed(2)}s before next page...`);
  677. await wait(this.options.searchDelay);
  678.  
  679. } while (this.state.running);
  680.  
  681. this.stats.endTime = new Date();
  682. log.success(`Ended at ${this.stats.endTime.toLocaleString()}! Total time: ${msToHMS(this.stats.endTime.getTime() - this.stats.startTime.getTime())}`);
  683. this.printStats();
  684. log.debug(`Deleted ${this.state.delCount} messages, ${this.state.failCount} failed.\n`);
  685.  
  686. if (this.onStop) this.onStop(this.state, this.stats);
  687. }
  688.  
  689. stop() {
  690. this.state.running = false;
  691. if (this.onStop) this.onStop(this.state, this.stats);
  692. }
  693.  
  694. /** Calculate the estimated time remaining based on the current stats */
  695. calcEtr() {
  696. this.stats.etr = (this.options.searchDelay + this.stats.avgPing) * Math.round((this.state.grandTotal - this.state.delCount) / 25) + (this.options.deleteDelay + this.stats.avgPing) * (this.state.grandTotal - this.state.delCount);
  697. }
  698.  
  699. /** As for confirmation in the beggining process */
  700. async confirm() {
  701. if (!this.options.askForConfirmation) return true;
  702.  
  703. log.verb('Waiting for your confirmation...');
  704. const preview = this.state._messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n');
  705.  
  706. const answer = await ask(
  707. `Do you want to delete ~${this.state.grandTotal} messages? (Estimated time: ${msToHMS(this.stats.etr)})` +
  708. '(The actual number of messages may be less, depending if you\'re using filters to skip some messages)' +
  709. '\n\n---- Preview ----\n' +
  710. preview
  711. );
  712.  
  713. if (!answer) {
  714. log.error('Aborted by you!');
  715. return false;
  716. }
  717. else {
  718. log.verb('OK');
  719. this.options.askForConfirmation = false; // do not ask for confirmation again on the next request
  720. return true;
  721. }
  722. }
  723.  
  724. async fetchChannelInfo() {
  725. let API_CHANNEL_URL = `https://discord.com/api/v9/channels/${this.options.channelId}`;
  726.  
  727. let resp;
  728. try {
  729. await this.beforeRequest();
  730. resp = await fetch(API_CHANNEL_URL, {
  731. headers: {
  732. 'Authorization': this.options.authToken,
  733. }
  734. });
  735. this.afterRequest();
  736. } catch (err) {
  737. this.state.running = false;
  738. log.error('Channel request threw an error:', err);
  739. throw err;
  740. }
  741.  
  742. // not indexed yet
  743. if (resp.status === 202) {
  744. let w = (await resp.json()).retry_after;
  745. w = !isNaN(w) ? w * 1000 : this.stats.searchDelay; // Fix retry_after 0
  746. this.stats.throttledCount++;
  747. this.stats.throttledTotalTime += w;
  748. log.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
  749. await wait(w);
  750. return await this.fetchChannelInfo();
  751. }
  752.  
  753. if (!resp.ok) {
  754. // rate limit
  755. if (resp.status === 429) {
  756. let w = (await resp.json()).retry_after;
  757. w = !isNaN(w) ? w * 1000 : this.stats.searchDelay; // Fix retry_after 0
  758.  
  759. this.stats.throttledCount++;
  760. this.stats.throttledTotalTime += w;
  761. log.warn(`Being rate limited by the API for ${w}ms!`);
  762. this.printStats();
  763. log.verb(`Cooling down for ${w * 2}ms before retrying...`);
  764.  
  765. await wait(w * 2);
  766. return await this.fetchChannelInfo();
  767. }
  768. else {
  769. log.error(`Error fetching the channel, API responded with status ${resp.status}!\n`, await resp.json());
  770. return {};
  771. }
  772. }
  773. const data = await resp.json();
  774. this.options.guildId = data.guild_id ?? '@me';
  775. return data;
  776. }
  777.  
  778. async search() {
  779. let API_SEARCH_URL;
  780. if (this.options.guildId === '@me') API_SEARCH_URL = `https://discord.com/api/v9/channels/${this.options.channelId}/messages/`; // DMs
  781. else API_SEARCH_URL = `https://discord.com/api/v9/guilds/${this.options.guildId}/messages/`; // Server
  782.  
  783. let resp;
  784. try {
  785. await this.beforeRequest();
  786. resp = await fetch(API_SEARCH_URL + 'search?' + queryString([
  787. ['author_id', this.options.authorId || undefined],
  788. ['channel_id', (this.options.guildId !== '@me' ? this.options.channelId : undefined) || undefined],
  789. ['sort_by', 'timestamp'],
  790. ['sort_order', this.state.sortOrder],
  791. ['offset', this.state.offset[this.state.sortOrder]],
  792. ['has', this.options.hasLink ? 'link' : undefined],
  793. ['has', this.options.hasFile ? 'file' : undefined],
  794. ['content', this.options.content || undefined],
  795. ['include_nsfw', this.options.includeNsfw ? true : undefined],
  796. ]), {
  797. headers: {
  798. 'Authorization': this.options.authToken,
  799. }
  800. });
  801. this.afterRequest();
  802. } catch (err) {
  803. this.state.running = false;
  804. log.error('Search request threw an error:', err);
  805. throw err;
  806. }
  807.  
  808. // not indexed yet
  809. if (resp.status === 202) {
  810. let w = (await resp.json()).retry_after;
  811. w = !isNaN(w) ? w * 1000 : this.stats.searchDelay; // Fix retry_after 0
  812. this.stats.throttledCount++;
  813. this.stats.throttledTotalTime += w;
  814. log.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
  815. await wait(w);
  816. return await this.search();
  817. }
  818.  
  819. if (!resp.ok) {
  820. // searching messages too fast
  821. if (resp.status === 429) {
  822. let w = (await resp.json()).retry_after;
  823. w = !isNaN(w) ? w * 1000 : this.stats.searchDelay; // Fix retry_after 0
  824.  
  825. this.stats.throttledCount++;
  826. this.stats.throttledTotalTime += w;
  827. log.warn(`Being rate limited by the API for ${w}ms!`);
  828. this.printStats();
  829. log.verb(`Cooling down for ${w * 2}ms before retrying...`);
  830.  
  831. await wait(w * 2);
  832. return await this.search();
  833. }
  834. else {
  835. log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
  836. const data = {messages: []};
  837. this.state._seachResponse = data;
  838. return data;
  839. }
  840. }
  841. const data = await resp.json();
  842. this.state._seachResponse = data;
  843. console.log(PREFIX$1, 'search', data);
  844. return data;
  845. }
  846.  
  847. async filterResponse() {
  848. const data = this.state._seachResponse;
  849.  
  850. // the search total will decrease as we delete stuff
  851. const total = data.total_results;
  852. if (total > this.state.grandTotal) this.state.grandTotal = total;
  853.  
  854. // search returns messages near the the actual message, only get the messages we searched for.
  855. const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true));
  856.  
  857. // we can only delete some types of messages, system messages are not deletable.
  858. let messagesToDelete = discoveredMessages;
  859. messagesToDelete = messagesToDelete.filter(msg => msg.type === 0 || (msg.type >= 6 && msg.type <= 21));
  860. messagesToDelete = messagesToDelete.filter(msg => msg.pinned ? this.options.includePinned : true);
  861.  
  862. // if the user hasn't checked the include applications option, filter out all bots
  863. // fixes issues with bots & applications hanging the deletion
  864. if (this.options.includeApplications == false) {
  865. log.verb("Include Applications is false. Skipping bots and applications...");
  866. messagesToDelete = messagesToDelete.filter(msg => !msg.author.bot);
  867. }
  868.  
  869. // custom filter of messages
  870. try {
  871. const regex = new RegExp(this.options.pattern, 'i');
  872. messagesToDelete = messagesToDelete.filter(msg => regex.test(msg.content));
  873. } catch (e) {
  874. log.warn('Ignoring RegExp because pattern is malformed!', e);
  875. }
  876.  
  877. // create an array containing everything we skipped. (used to calculate offset for next searches)
  878. const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id));
  879.  
  880. this.state._messagesToDelete = messagesToDelete;
  881. this.state._skippedMessages = skippedMessages;
  882.  
  883. console.log(PREFIX$1, 'filterResponse', this.state);
  884. }
  885.  
  886. async deleteMessagesFromList() {
  887. for (let i = 0; i < this.state._messagesToDelete.length; i++) {
  888. const message = this.state._messagesToDelete[i];
  889. if (!this.state.running) return log.error('Stopped by you!');
  890.  
  891. log.debug(
  892. // `${((this.state.delCount + 1) / this.state.grandTotal * 100).toFixed(2)}%`,
  893. `[${this.state.delCount + 1}/${this.state.grandTotal}] ` +
  894. `<sup>${new Date(message.timestamp).toLocaleString()}</sup> ` +
  895. `<b>${redact(message.author.username + '#' + message.author.discriminator)}</b>` +
  896. `: <i>${redact(message.content).replace(/\n/g, '↵')}</i>` +
  897. (message.attachments.length ? redact(JSON.stringify(message.attachments)) : ''),
  898. `<sup>{ID:${redact(message.id)}}</sup>`
  899. );
  900.  
  901. // Delete a single message (with retry)
  902. let attempt = 0;
  903. while (attempt < this.options.maxAttempt) {
  904. const result = await this.deleteMessage(message);
  905.  
  906. if (result === 'RETRY') {
  907. attempt++;
  908. log.verb(`Retrying in ${this.options.deleteDelay}ms... (${attempt}/${this.options.maxAttempt})`);
  909. await wait(this.options.deleteDelay);
  910. }
  911. else break;
  912. }
  913.  
  914. this.calcEtr();
  915. if (this.onProgress) this.onProgress(this.state, this.stats);
  916.  
  917. await wait(this.options.deleteDelay);
  918. }
  919. }
  920.  
  921. async deleteMessage(message) {
  922. const API_DELETE_URL = `https://discord.com/api/v9/channels/${message.channel_id}/messages/${message.id}`;
  923. let resp;
  924. try {
  925. await this.beforeRequest();
  926. resp = await fetch(API_DELETE_URL, {
  927. method: 'DELETE',
  928. headers: {
  929. 'Authorization': this.options.authToken,
  930. },
  931. });
  932. this.afterRequest();
  933. } catch (err) {
  934. // no response error (e.g. network error)
  935. log.error('Delete request throwed an error:', err);
  936. log.verb('Related object:', redact(JSON.stringify(message)));
  937. this.state.failCount++;
  938. return 'FAILED';
  939. }
  940.  
  941. if (!resp.ok) {
  942. if (resp.status === 429) {
  943. // deleting messages too fast
  944. let w = (await resp.json()).retry_after;
  945. w = !isNaN(w) ? w * 1000 : this.stats.deleteDelay;
  946. this.stats.throttledCount++;
  947. this.stats.throttledTotalTime += w;
  948. log.warn(`Being rate limited by the API for ${w}ms!`);
  949. this.printStats();
  950. log.verb(`Cooling down for ${w * 2}ms before retrying...`);
  951. await wait(w * 2);
  952. return 'RETRY';
  953. } else {
  954. const body = await resp.text();
  955.  
  956. try {
  957. const r = JSON.parse(body);
  958.  
  959. if (resp.status === 400 && r.code === 50083) {
  960. // 400 can happen if the thread is archived (code=50083)
  961. // in this case we need to "skip" this message from the next search
  962. // otherwise it will come up again in the next page (and fail to delete again)
  963. log.warn('Error deleting message (Thread is archived). Will increment offset so we don\'t search this in the next page...');
  964. this.state.offset[this.state.sortOrder]++;
  965. this.state.failCount++;
  966. return 'FAIL_SKIP'; // Failed but we will skip it next time
  967. }
  968.  
  969. log.error(`Error deleting message, API responded with status ${resp.status}!`, r);
  970. log.verb('Related object:', redact(JSON.stringify(message)));
  971. this.state.failCount++;
  972. return 'FAILED';
  973. } catch (e) {
  974. log.error(`Fail to parse JSON. API responded with status ${resp.status}!`, body);
  975. }
  976. }
  977. }
  978.  
  979. this.state.delCount++;
  980. return 'OK';
  981. }
  982.  
  983. #beforeTs = 0; // used to calculate latency
  984. #requestLog = []; // used to add any extra delay
  985. async beforeRequest() {
  986. this.#requestLog.push(Date.now());
  987. this.#requestLog = this.#requestLog.filter(timestamp => (Date.now() - timestamp) < 60 * 1000);
  988. if (this.options.rateLimitPrevention) {
  989. let rateLimits = [[45, 60], [4, 5]]; // todo: confirm, testing shows these are right
  990. for (let [maxRequests, timePeriod] of rateLimits) {
  991. if (this.#requestLog.length >= maxRequests && (Date.now() - this.#requestLog[this.#requestLog.length - maxRequests]) < timePeriod * 1000) {
  992. let delay = timePeriod * 1000 - (Date.now() - this.#requestLog[this.#requestLog.length - maxRequests]);
  993. delay = delay * 1.15 + 300; // adding a buffer and additional wait time
  994. log.verb(`Delaying for an extra ${(delay / 1000).toFixed(2)}s to avoid rate limits...`);
  995. await new Promise(resolve => setTimeout(resolve, delay));
  996. break;
  997. }
  998. }
  999. }
  1000. this.#beforeTs = Date.now();
  1001. }
  1002. afterRequest() {
  1003. this.stats.lastPing = (Date.now() - this.#beforeTs);
  1004. this.stats.avgPing = this.stats.avgPing > 0 ? (this.stats.avgPing * 0.9) + (this.stats.lastPing * 0.1) : this.stats.lastPing;
  1005. }
  1006.  
  1007. printStats() {
  1008. log.verb(
  1009. `Delete delay: ${this.options.deleteDelay}ms, Search delay: ${this.options.searchDelay}ms`,
  1010. `Last Ping: ${this.stats.lastPing}ms, Average Ping: ${this.stats.avgPing | 0}ms`,
  1011. );
  1012. log.verb(
  1013. `Rate Limited: ${this.stats.throttledCount} times.`,
  1014. `Total time throttled: ${msToHMS(this.stats.throttledTotalTime)}.`
  1015. );
  1016. }
  1017. }
  1018.  
  1019. const MOVE = 0;
  1020. const RESIZE_T = 1;
  1021. const RESIZE_B = 2;
  1022. const RESIZE_L = 4;
  1023. const RESIZE_R = 8;
  1024. const RESIZE_TL = RESIZE_T + RESIZE_L;
  1025. const RESIZE_TR = RESIZE_T + RESIZE_R;
  1026. const RESIZE_BL = RESIZE_B + RESIZE_L;
  1027. const RESIZE_BR = RESIZE_B + RESIZE_R;
  1028.  
  1029. /**
  1030. * Make an element draggable/resizable
  1031. * @author Victor N. wwww.vitim.us
  1032. */
  1033. class DragResize {
  1034. constructor({ elm, moveHandle, options }) {
  1035. this.options = defaultArgs({
  1036. enabledDrag: true,
  1037. enabledResize: true,
  1038. minWidth: 200,
  1039. maxWidth: Infinity,
  1040. minHeight: 100,
  1041. maxHeight: Infinity,
  1042. dragAllowX: true,
  1043. dragAllowY: true,
  1044. resizeAllowX: true,
  1045. resizeAllowY: true,
  1046. draggingClass: 'drag',
  1047. useMouseEvents: true,
  1048. useTouchEvents: true,
  1049. createHandlers: true,
  1050. }, options);
  1051. Object.assign(this, options);
  1052. options = undefined;
  1053.  
  1054. elm.style.position = 'fixed';
  1055.  
  1056. this.drag_m = new Draggable(elm, moveHandle, MOVE, this.options);
  1057.  
  1058. if (this.options.createHandlers) {
  1059. this.el_t = createElement('div', { name: 'grab-t' }, elm);
  1060. this.drag_t = new Draggable(elm, this.el_t, RESIZE_T, this.options);
  1061. this.el_r = createElement('div', { name: 'grab-r' }, elm);
  1062. this.drag_r = new Draggable(elm, this.el_r, RESIZE_R, this.options);
  1063. this.el_b = createElement('div', { name: 'grab-b' }, elm);
  1064. this.drag_b = new Draggable(elm, this.el_b, RESIZE_B, this.options);
  1065. this.el_l = createElement('div', { name: 'grab-l' }, elm);
  1066. this.drag_l = new Draggable(elm, this.el_l, RESIZE_L, this.options);
  1067. this.el_tl = createElement('div', { name: 'grab-tl' }, elm);
  1068. this.drag_tl = new Draggable(elm, this.el_tl, RESIZE_TL, this.options);
  1069. this.el_tr = createElement('div', { name: 'grab-tr' }, elm);
  1070. this.drag_tr = new Draggable(elm, this.el_tr, RESIZE_TR, this.options);
  1071. this.el_br = createElement('div', { name: 'grab-br' }, elm);
  1072. this.drag_br = new Draggable(elm, this.el_br, RESIZE_BR, this.options);
  1073. this.el_bl = createElement('div', { name: 'grab-bl' }, elm);
  1074. this.drag_bl = new Draggable(elm, this.el_bl, RESIZE_BL, this.options);
  1075. }
  1076. }
  1077. }
  1078.  
  1079. class Draggable {
  1080. constructor(targetElm, handleElm, op, options) {
  1081. Object.assign(this, options);
  1082. options = undefined;
  1083.  
  1084. this._targetElm = targetElm;
  1085. this._handleElm = handleElm;
  1086.  
  1087. let vw = window.innerWidth;
  1088. let vh = window.innerHeight;
  1089. let initialX, initialY, initialT, initialL, initialW, initialH;
  1090.  
  1091. const clamp = (value, min, max) => value < min ? min : value > max ? max : value;
  1092.  
  1093. const moveOp = (x, y) => {
  1094. const deltaX = (x - initialX);
  1095. const deltaY = (y - initialY);
  1096. const t = clamp(initialT + deltaY, 0, vh - initialH);
  1097. const l = clamp(initialL + deltaX, 0, vw - initialW);
  1098. this._targetElm.style.top = t + 'px';
  1099. this._targetElm.style.left = l + 'px';
  1100. };
  1101.  
  1102. const resizeOp = (x, y) => {
  1103. x = clamp(x, 0, vw);
  1104. y = clamp(y, 0, vh);
  1105. const deltaX = (x - initialX);
  1106. const deltaY = (y - initialY);
  1107. const resizeDirX = (op & RESIZE_L) ? -1 : 1;
  1108. const resizeDirY = (op & RESIZE_T) ? -1 : 1;
  1109. const deltaXMax = (this.maxWidth - initialW);
  1110. const deltaXMin = (this.minWidth - initialW);
  1111. const deltaYMax = (this.maxHeight - initialH);
  1112. const deltaYMin = (this.minHeight - initialH);
  1113. const t = initialT + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax) * resizeDirY;
  1114. const l = initialL + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax) * resizeDirX;
  1115. const w = initialW + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax);
  1116. const h = initialH + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax);
  1117. if (op & RESIZE_T) { // resize ↑
  1118. this._targetElm.style.top = t + 'px';
  1119. this._targetElm.style.height = h + 'px';
  1120. }
  1121. if (op & RESIZE_B) { // resize ↓
  1122. this._targetElm.style.height = h + 'px';
  1123. }
  1124. if (op & RESIZE_L) { // resize ←
  1125. this._targetElm.style.left = l + 'px';
  1126. this._targetElm.style.width = w + 'px';
  1127. }
  1128. if (op & RESIZE_R) { // resize →
  1129. this._targetElm.style.width = w + 'px';
  1130. }
  1131. };
  1132.  
  1133. let operation = op === MOVE ? moveOp : resizeOp;
  1134.  
  1135. function dragStartHandler(e) {
  1136. const touch = e.type === 'touchstart';
  1137. if ((e.buttons === 1 || e.which === 1) || touch) {
  1138. e.preventDefault();
  1139. const x = touch ? e.touches[0].clientX : e.clientX;
  1140. const y = touch ? e.touches[0].clientY : e.clientY;
  1141. initialX = x;
  1142. initialY = y;
  1143. vw = window.innerWidth;
  1144. vh = window.innerHeight;
  1145. initialT = this._targetElm.offsetTop;
  1146. initialL = this._targetElm.offsetLeft;
  1147. initialW = this._targetElm.clientWidth;
  1148. initialH = this._targetElm.clientHeight;
  1149. if (this.useMouseEvents) {
  1150. document.addEventListener('mousemove', this._dragMoveHandler);
  1151. document.addEventListener('mouseup', this._dragEndHandler);
  1152. }
  1153. if (this.useTouchEvents) {
  1154. document.addEventListener('touchmove', this._dragMoveHandler, { passive: false });
  1155. document.addEventListener('touchend', this._dragEndHandler);
  1156. }
  1157. this._targetElm.classList.add(this.draggingClass);
  1158. }
  1159. }
  1160.  
  1161. function dragMoveHandler(e) {
  1162. e.preventDefault();
  1163. let x, y;
  1164. const touch = e.type === 'touchmove';
  1165. if (touch) {
  1166. const t = e.touches[0];
  1167. x = t.clientX;
  1168. y = t.clientY;
  1169. } else { //mouse
  1170. // If the button is not down, dispatch a "fake" mouse up event, to stop listening to mousemove
  1171. // This happens when the mouseup is not captured (outside the browser)
  1172. if ((e.buttons || e.which) !== 1) {
  1173. this._dragEndHandler();
  1174. return;
  1175. }
  1176. x = e.clientX;
  1177. y = e.clientY;
  1178. }
  1179. // perform drag / resize operation
  1180. operation(x, y);
  1181. }
  1182.  
  1183. function dragEndHandler(e) {
  1184. if (this.useMouseEvents) {
  1185. document.removeEventListener('mousemove', this._dragMoveHandler);
  1186. document.removeEventListener('mouseup', this._dragEndHandler);
  1187. }
  1188. if (this.useTouchEvents) {
  1189. document.removeEventListener('touchmove', this._dragMoveHandler);
  1190. document.removeEventListener('touchend', this._dragEndHandler);
  1191. }
  1192. this._targetElm.classList.remove(this.draggingClass);
  1193. }
  1194.  
  1195. // We need to bind the handlers to this instance
  1196. this._dragStartHandler = dragStartHandler.bind(this);
  1197. this._dragMoveHandler = dragMoveHandler.bind(this);
  1198. this._dragEndHandler = dragEndHandler.bind(this);
  1199.  
  1200. this.enable();
  1201. }
  1202.  
  1203. /** Turn on the drag and drop of the instance */
  1204. enable() {
  1205. this.destroy(); // prevent events from getting binded twice
  1206. if (this.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler);
  1207. if (this.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false });
  1208. }
  1209.  
  1210. /** Teardown all events bound to the document and elements. You can resurrect this instance by calling enable() */
  1211. destroy() {
  1212. this._targetElm.classList.remove(this.draggingClass);
  1213. if (this.useMouseEvents) {
  1214. this._handleElm.removeEventListener('mousedown', this._dragStartHandler);
  1215. document.removeEventListener('mousemove', this._dragMoveHandler);
  1216. document.removeEventListener('mouseup', this._dragEndHandler);
  1217. }
  1218. if (this.useTouchEvents) {
  1219. this._handleElm.removeEventListener('touchstart', this._dragStartHandler);
  1220. document.removeEventListener('touchmove', this._dragMoveHandler);
  1221. document.removeEventListener('touchend', this._dragEndHandler);
  1222. }
  1223. }
  1224. }
  1225.  
  1226. function createElement(tag='div', attrs, parent) {
  1227. const elm = document.createElement(tag);
  1228. if (attrs) Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v));
  1229. if (parent) parent.appendChild(elm);
  1230. return elm;
  1231. }
  1232.  
  1233. function defaultArgs(defaults, options) {
  1234. function isObj(x) { return x !== null && typeof x === 'object'; }
  1235. function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
  1236. if (isObj(options)) for (let prop in defaults) {
  1237. if (hasOwn(defaults, prop) && hasOwn(options, prop) && options[prop] !== undefined) {
  1238. if (isObj(defaults[prop])) defaultArgs(defaults[prop], options[prop]);
  1239. else defaults[prop] = options[prop];
  1240. }
  1241. }
  1242. return defaults;
  1243. }
  1244.  
  1245. function createElm(html) {
  1246. const temp = document.createElement('div');
  1247. temp.innerHTML = html;
  1248. return temp.removeChild(temp.firstElementChild);
  1249. }
  1250.  
  1251. function insertCss(css) {
  1252. const style = document.createElement('style');
  1253. style.appendChild(document.createTextNode(css));
  1254. document.head.appendChild(style);
  1255. return style;
  1256. }
  1257.  
  1258. const messagePickerCss = `
  1259. body.undiscord-pick-message [data-list-id="chat-messages"] {
  1260. background-color: var(--background-secondary-alt);
  1261. box-shadow: inset 0 0 0px 2px var(--button-outline-brand-border);
  1262. }
  1263.  
  1264. body.undiscord-pick-message [id^="message-content-"]:hover {
  1265. cursor: pointer;
  1266. cursor: cell;
  1267. background: var(--background-message-automod-hover);
  1268. }
  1269. body.undiscord-pick-message [id^="message-content-"]:hover::after {
  1270. position: absolute;
  1271. top: calc(50% - 11px);
  1272. left: 4px;
  1273. z-index: 1;
  1274. width: 65px;
  1275. height: 22px;
  1276. line-height: 22px;
  1277. font-family: var(--font-display);
  1278. background-color: var(--button-secondary-background);
  1279. color: var(--header-secondary);
  1280. font-size: 12px;
  1281. font-weight: 500;
  1282. text-transform: uppercase;
  1283. text-align: center;
  1284. border-radius: 3px;
  1285. content: 'This 👉';
  1286. }
  1287. body.undiscord-pick-message.before [id^="message-content-"]:hover::after {
  1288. content: 'Before 👆';
  1289. }
  1290. body.undiscord-pick-message.after [id^="message-content-"]:hover::after {
  1291. content: 'After 👇';
  1292. }
  1293. `;
  1294.  
  1295. const messagePicker = {
  1296. init() {
  1297. insertCss(messagePickerCss);
  1298. },
  1299. grab(auxiliary) {
  1300. return new Promise((resolve, reject) => {
  1301. document.body.classList.add('undiscord-pick-message');
  1302. if (auxiliary) document.body.classList.add(auxiliary);
  1303. function clickHandler(e) {
  1304. const message = e.target.closest('[id^="message-content-"]');
  1305. if (message) {
  1306. e.preventDefault();
  1307. e.stopPropagation();
  1308. e.stopImmediatePropagation();
  1309. if (auxiliary) document.body.classList.remove(auxiliary);
  1310. document.body.classList.remove('undiscord-pick-message');
  1311. document.removeEventListener('click', clickHandler);
  1312. try {
  1313. resolve(message.id.match(/message-content-(\d+)/)[1]);
  1314. } catch (e) {
  1315. resolve(null);
  1316. }
  1317. }
  1318. }
  1319. document.addEventListener('click', clickHandler);
  1320. });
  1321. }
  1322. };
  1323. window.messagePicker = messagePicker;
  1324.  
  1325. function getToken() {
  1326. window.dispatchEvent(new Event('beforeunload'));
  1327. const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  1328. try {
  1329. return JSON.parse(LS.token);
  1330. } catch {
  1331. log.info('Could not automatically detect Authorization Token in local storage!');
  1332. log.info('Attempting to grab token using webpack');
  1333. 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();
  1334. }
  1335. }
  1336.  
  1337. function getAuthorId() {
  1338. const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
  1339. return JSON.parse(LS.user_id_cache);
  1340. }
  1341.  
  1342. function getGuildId() {
  1343. const m = location.href.match(/channels\/([\w@]+)\/(\d+)/);
  1344. if (m) return m[1];
  1345. else alert('Could not find the Guild ID!\nPlease make sure you are on a Server or DM.');
  1346. }
  1347.  
  1348. function getChannelId() {
  1349. const m = location.href.match(/channels\/([\w@]+)\/(\d+)/);
  1350. if (m) return m[2];
  1351. else alert('Could not find the Channel ID!\nPlease make sure you are on a Channel or DM.');
  1352. }
  1353.  
  1354. function fillToken() {
  1355. try {
  1356. return getToken();
  1357. } catch (err) {
  1358. log.verb(err);
  1359. log.error('Could not automatically detect Authorization Token!');
  1360. log.info('Please make sure Undiscord is up to date');
  1361. log.debug('Alternatively, you can try entering a Token manually in the "Advanced Settings" section.');
  1362. }
  1363. return '';
  1364. }
  1365.  
  1366. const PREFIX = '[UNDISCORD]';
  1367.  
  1368. // -------------------------- User interface ------------------------------- //
  1369.  
  1370. // links
  1371. const HOME = 'https://github.com/louga31/undiscord';
  1372. const WIKI = 'https://github.com/louga31/undiscord/wiki';
  1373.  
  1374. const undiscordCore = new UndiscordCore();
  1375. messagePicker.init();
  1376.  
  1377. const ui = {
  1378. undiscordWindow: null,
  1379. undiscordBtn: null,
  1380. logArea: null,
  1381. autoScroll: null,
  1382. trimLog: null,
  1383.  
  1384. // progress handler
  1385. progressMain: null,
  1386. progressIcon: null,
  1387. percent: null,
  1388. };
  1389. const $ = s => ui.undiscordWindow.querySelector(s);
  1390.  
  1391. function initUI() {
  1392.  
  1393. insertCss(themeCss);
  1394. insertCss(mainCss);
  1395. insertCss(dragCss);
  1396.  
  1397. // create undiscord window
  1398. const undiscordUI = replaceInterpolations(undiscordTemplate, {
  1399. VERSION,
  1400. HOME,
  1401. WIKI,
  1402. });
  1403. ui.undiscordWindow = createElm(undiscordUI);
  1404. document.body.appendChild(ui.undiscordWindow);
  1405.  
  1406. // enable drag and resize on undiscord window
  1407. new DragResize({ elm: ui.undiscordWindow, moveHandle: $('.header') });
  1408.  
  1409. // create undiscord Trash icon
  1410. ui.undiscordBtn = createElm(buttonHtml);
  1411. ui.undiscordBtn.onclick = toggleWindow;
  1412. function mountBtn() {
  1413. const toolbar = document.querySelector('#app-mount [class^=toolbar]');
  1414. if (toolbar) toolbar.appendChild(ui.undiscordBtn);
  1415. }
  1416. mountBtn();
  1417. // watch for changes and re-mount button if necessary
  1418. const discordElm = document.querySelector('#app-mount');
  1419. let observerThrottle = null;
  1420. const observer = new MutationObserver((_mutationsList, _observer) => {
  1421. if (observerThrottle) return;
  1422. observerThrottle = setTimeout(() => {
  1423. observerThrottle = null;
  1424. if (!discordElm.contains(ui.undiscordBtn)) mountBtn(); // re-mount the button to the toolbar
  1425. }, 3000);
  1426. });
  1427. observer.observe(discordElm, { attributes: false, childList: true, subtree: true });
  1428.  
  1429. function toggleWindow() {
  1430. if (ui.undiscordWindow.style.display !== 'none') {
  1431. ui.undiscordWindow.style.display = 'none';
  1432. ui.undiscordBtn.style.color = 'var(--interactive-normal)';
  1433. }
  1434. else {
  1435. ui.undiscordWindow.style.display = '';
  1436. ui.undiscordBtn.style.color = 'var(--interactive-active)';
  1437. }
  1438. }
  1439.  
  1440. // cached elements
  1441. ui.logArea = $('#logArea');
  1442. ui.autoScroll = $('#autoScroll');
  1443. ui.trimLog = $('#trimLog');
  1444. ui.progressMain = $('#progressBar');
  1445. ui.progressIcon = ui.undiscordBtn.querySelector('progress');
  1446. ui.percent = $('#progressPercent');
  1447.  
  1448. // register event listeners
  1449. $('#hide').onclick = toggleWindow;
  1450. $('#toggleSidebar').onclick = ()=> ui.undiscordWindow.classList.toggle('hide-sidebar');
  1451. $('button#start').onclick = startAction;
  1452. $('button#stop').onclick = stopAction;
  1453. $('button#clear').onclick = () => ui.logArea.innerHTML = '';
  1454. $('button#getAuthor').onclick = () => $('input#authorId').value = getAuthorId();
  1455. $('button#getGuild').onclick = () => {
  1456. const guildId = $('input#guildId').value = getGuildId();
  1457. if (guildId === '@me') $('input#channelId').value = getChannelId();
  1458. };
  1459. $('button#getChannel').onclick = () => {
  1460. $('input#channelId').value = getChannelId();
  1461. $('input#guildId').value = getGuildId();
  1462. };
  1463. $('#redact').onchange = () => {
  1464. const b = ui.undiscordWindow.classList.toggle('redact');
  1465. if (b) alert('This mode will attempt to hide personal information, so you can screen share / take screenshots.\nAlways double check you are not sharing sensitive information!');
  1466. };
  1467. $('#pickMessageAfter').onclick = async () => {
  1468. alert('Select a message on the chat.\nThe message below it will be deleted.');
  1469. toggleWindow();
  1470. const id = await messagePicker.grab('after');
  1471. if (id) $('input#minId').value = id;
  1472. toggleWindow();
  1473. };
  1474. $('#pickMessageBefore').onclick = async () => {
  1475. alert('Select a message on the chat.\nThe message above it will be deleted.');
  1476. toggleWindow();
  1477. const id = await messagePicker.grab('before');
  1478. if (id) $('input#maxId').value = id;
  1479. toggleWindow();
  1480. };
  1481. $('button#getToken').onclick = () => $('input#token').value = fillToken();
  1482.  
  1483. // sync advanced settings
  1484. $('input#searchDelay').onchange = (e) => {
  1485. const v = parseInt(e.target.value);
  1486. if (v) undiscordCore.options.searchDelay = v;
  1487. };
  1488. $('input#deleteDelay').onchange = (e) => {
  1489. const v = parseInt(e.target.value);
  1490. if (v) undiscordCore.options.deleteDelay = v;
  1491. };
  1492. $('input#rateLimitPrevention').onchange = (e) => {
  1493. undiscordCore.options.rateLimitPrevention = e.target.checked ?? false;
  1494. };
  1495.  
  1496. $('input#searchDelay').addEventListener('input', (event) => {
  1497. $('div#searchDelayValue').textContent = event.target.value + 'ms';
  1498. });
  1499. $('input#deleteDelay').addEventListener('input', (event) => {
  1500. $('div#deleteDelayValue').textContent = event.target.value + 'ms';
  1501. });
  1502.  
  1503. // import json
  1504. const fileSelection = $('input#importJsonInput');
  1505. fileSelection.onchange = async () => {
  1506. const files = fileSelection.files;
  1507.  
  1508. // No files added
  1509. if (files.length === 0) return log.warn('No file selected.');
  1510.  
  1511. // Get channel id field to set it later
  1512. const channelIdField = $('input#channelId');
  1513.  
  1514. // Force the guild id to be 'null' (placeholder value)
  1515. const guildIdField = $('input#guildId');
  1516. guildIdField.value = 'null';
  1517.  
  1518. // Set author id in case its not set already
  1519. $('input#authorId').value = getAuthorId();
  1520. try {
  1521. const file = files[0];
  1522. const text = await file.text();
  1523. const json = JSON.parse(text);
  1524. const channelIds = Object.keys(json);
  1525. channelIdField.value = channelIds.join(',');
  1526. log.info(`Loaded ${channelIds.length} channels.`);
  1527. } catch(err) {
  1528. log.error('Error parsing file!', err);
  1529. }
  1530. };
  1531.  
  1532. // redirect console logs to inside the window after setting up the UI
  1533. setLogFn(printLog);
  1534.  
  1535. setupUndiscordCore();
  1536. }
  1537.  
  1538. function printLog(type = '', args) {
  1539. ui.logArea.insertAdjacentHTML('beforeend', `<div class="log log-${type}">${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}</div>`);
  1540.  
  1541. if (ui.trimLog.checked) {
  1542. const maxLogEntries = 500;
  1543. const logEntries = ui.logArea.querySelectorAll('.log');
  1544. if (logEntries.length > maxLogEntries) {
  1545. for (let i = 0; i < (logEntries.length - maxLogEntries); i++) {
  1546. logEntries[i].remove();
  1547. }
  1548. }
  1549. }
  1550.  
  1551. if (ui.autoScroll.checked) ui.logArea.querySelector('div:last-child').scrollIntoView(false);
  1552. if (type==='error') console.error(PREFIX, ...Array.from(args));
  1553. }
  1554.  
  1555. function setupUndiscordCore() {
  1556.  
  1557. undiscordCore.onStart = (state, stats) => {
  1558. console.log(PREFIX, 'onStart', state, stats);
  1559. $('#start').disabled = true;
  1560. $('#stop').disabled = false;
  1561.  
  1562. ui.undiscordBtn.classList.add('running');
  1563. ui.progressMain.style.display = 'block';
  1564. ui.percent.style.display = 'block';
  1565. };
  1566.  
  1567. undiscordCore.onProgress = (state, stats) => {
  1568. // console.log(PREFIX, 'onProgress', state, stats);
  1569. let max = state.grandTotal;
  1570. const value = state.delCount + state.failCount;
  1571. max = Math.max(max, value, 0); // clamp max
  1572.  
  1573. // status bar
  1574. const percent = value >= 0 && max ? Math.round(value / max * 100) + '%' : '';
  1575. const elapsed = msToHMS(Date.now() - stats.startTime.getTime());
  1576. const remaining = msToHMS(stats.etr);
  1577. ui.percent.innerHTML = `${percent} (${value}/${max}) Elapsed: ${elapsed} Remaining: ${remaining}`;
  1578.  
  1579. ui.progressIcon.value = value;
  1580. ui.progressMain.value = value;
  1581.  
  1582. // indeterminate progress bar
  1583. if (max) {
  1584. ui.progressIcon.setAttribute('max', max);
  1585. ui.progressMain.setAttribute('max', max);
  1586. } else {
  1587. ui.progressIcon.removeAttribute('value');
  1588. ui.progressMain.removeAttribute('value');
  1589. ui.percent.innerHTML = '...';
  1590. }
  1591.  
  1592. // update delays
  1593. const searchDelayInput = $('input#searchDelay');
  1594. searchDelayInput.value = undiscordCore.options.searchDelay;
  1595. $('div#searchDelayValue').textContent = undiscordCore.options.searchDelay+'ms';
  1596.  
  1597. const deleteDelayInput = $('input#deleteDelay');
  1598. deleteDelayInput.value = undiscordCore.options.deleteDelay;
  1599. $('div#deleteDelayValue').textContent = undiscordCore.options.deleteDelay+'ms';
  1600. };
  1601.  
  1602. undiscordCore.onStop = (state, stats) => {
  1603. console.log(PREFIX, 'onStop', state, stats);
  1604. $('#start').disabled = false;
  1605. $('#stop').disabled = true;
  1606. ui.undiscordBtn.classList.remove('running');
  1607. ui.progressMain.style.display = 'none';
  1608. ui.percent.style.display = 'none';
  1609. };
  1610. }
  1611.  
  1612. async function startAction() {
  1613. console.log(PREFIX, 'startAction');
  1614. // general
  1615. const authorId = $('input#authorId').value.trim();
  1616. const guildId = $('input#guildId').value.trim();
  1617. const channelIds = $('input#channelId').value.trim().split(/\s*,\s*/);
  1618. const includeNsfw = $('input#includeNsfw').checked;
  1619. // wipe archive
  1620. const includeServers = $('input#includeServers').checked;
  1621. // filter
  1622. const content = $('input#search').value.trim();
  1623. const hasLink = $('input#hasLink').checked;
  1624. const hasFile = $('input#hasFile').checked;
  1625. const includePinned = $('input#includePinned').checked;
  1626. const pattern = $('input#pattern').value;
  1627. const includeApplications = $('input#includeApplications').checked;
  1628. // message interval
  1629. const minId = $('input#minId').value.trim();
  1630. const maxId = $('input#maxId').value.trim();
  1631. // date range
  1632. const minDate = $('input#minDate').value.trim();
  1633. const maxDate = $('input#maxDate').value.trim();
  1634. //advanced
  1635. const searchDelay = parseInt($('input#searchDelay').value.trim());
  1636. const deleteDelay = parseInt($('input#deleteDelay').value.trim());
  1637. const rateLimitPrevention = $('input#rateLimitPrevention').checked;
  1638.  
  1639. // token
  1640. const authToken = $('input#token').value.trim() || fillToken();
  1641. if (!authToken) return; // get token already logs an error.
  1642.  
  1643. // validate input
  1644. if (!guildId) return log.error('You must fill the "Server ID" field!');
  1645.  
  1646. // clear logArea
  1647. ui.logArea.innerHTML = '';
  1648.  
  1649. undiscordCore.resetState();
  1650. undiscordCore.options = {
  1651. ...undiscordCore.options,
  1652. authToken,
  1653. authorId,
  1654. guildId,
  1655. channelId: channelIds.length === 1 ? channelIds[0] : undefined, // single or multiple channel
  1656. minId: minId || minDate,
  1657. maxId: maxId || maxDate,
  1658. content,
  1659. hasLink,
  1660. hasFile,
  1661. includeApplications,
  1662. includeNsfw,
  1663. includeServers,
  1664. includePinned,
  1665. pattern,
  1666. searchDelay,
  1667. deleteDelay,
  1668. rateLimitPrevention,
  1669. // maxAttempt: 2,
  1670. };
  1671. if (channelIds.length > 1) {
  1672. const jobs = channelIds.map(ch => ({
  1673. guildId: null,
  1674. channelId: ch,
  1675. }));
  1676.  
  1677. try {
  1678. await undiscordCore.runBatch(jobs);
  1679. } catch (err) {
  1680. log.error('CoreException', err);
  1681. }
  1682. }
  1683. // single channel
  1684. else {
  1685. try {
  1686. await undiscordCore.run();
  1687. } catch (err) {
  1688. log.error('CoreException', err);
  1689. undiscordCore.stop();
  1690. }
  1691. }
  1692. }
  1693.  
  1694. function stopAction() {
  1695. console.log(PREFIX, 'stopAction');
  1696. undiscordCore.stop();
  1697. }
  1698.  
  1699. // ---- END Undiscord ----
  1700.  
  1701. initUI();
  1702.  
  1703. })();