Userscript that deletes your Discord messages by scrolling through channels/DMs. Slow, thorough, and less likely to get caught (hopefully).
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

azomDev (May 17, 2026, 5:07 PM EDT) 62e0fde3

+578
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 azom 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+45
README.md
··· 1 + # Discoraser 2 + 3 + > [!WARNING] 4 + > Automating actions on user accounts ("self-botting") violates [Discord's Terms of Service](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) and could result in your account being limited or terminated. This tool simulates human-like behavior to reduce that risk, but there are no guarantees. Use at your own risk. 5 + 6 + A userscript that deletes your Discord messages by scrolling through channels and DMs. Navigate to a conversation, click Start, and it handles the rest. 7 + 8 + ## What it does 9 + 10 + - Scrolls up through a channel/DM and deletes your messages 11 + - Removes your reactions from other people's messages (toggleable) 12 + - Lets you keep messages newer than X days 13 + - Auto-detects your user ID from Discord's internals 14 + 15 + ## Why it's slow 16 + 17 + The script intentionally operates at human speed: 3-8 second delays between deletions, with longer breaks every 5 messages. This is by design: 18 + 19 + - Discord rate-limits aggressive API usage and can flag/ban accounts for bot-like behavior 20 + - Randomized delays make the pattern less detectable 21 + - It's meant to run in the background (overnight, over a few days) while you do other things 22 + 23 + If you need instant bulk deletion, this isn't the tool for that. This is for when you want to be thorough (it scrolls through every message rather than relying on Discord's search API, so nothing gets missed) and want to minimize the risk of detection. 24 + 25 + ## Install 26 + 27 + 1. Install a userscript manager ([Violentmonkey](https://violentmonkey.github.io/), Tampermonkey, etc.) 28 + 2. Create a new script and paste the contents of `discord-message-cleaner.user.js` 29 + 3. Open Discord in your browser (not the desktop app) 30 + 31 + ## Usage 32 + 33 + 1. Navigate to the DM or channel you want to clean 34 + 2. The panel appears at the top-left (drag it anywhere) 35 + 3. Set how many days of messages to keep (default: 7) 36 + 4. Click **Start** 37 + 5. Click **Stop** whenever you want to pause 38 + 39 + The script processes the current channel only. Move to another DM/channel and start again to clean that one. 40 + 41 + ## About 42 + 43 + This was fully implemented by an LLM. I read through the code and tested it myself. I just didn't have the time or desire to figure out Discord's obfuscated DOM structure and write all of this from scratch. 44 + 45 + [Undiscord](https://github.com/victornpb/undiscord) is a great project and its author did an awesome job keeping it alive solo for years. Discord is a massive company pushing changes constantly, and chasing that as a single unpaid developer is genuinely hard. It just wasn't working reliably for me at the time, so I built this as a personal workaround.
+512
discord-message-cleaner.user.js
··· 1 + // ==UserScript== 2 + // @name Discoraser 3 + // @namespace discoraser 4 + // @version 1.0.0 5 + // @description Userscript that deletes your Discord messages by scrolling through channels/DMs. Slow, thorough, and less likely to get caught (hopefully). 6 + // @match https://discord.com/* 7 + // @match https://ptb.discord.com/* 8 + // @match https://canary.discord.com/* 9 + // @grant none 10 + // @run-at document-idle 11 + // ==/UserScript== 12 + 13 + (function () { 14 + "use strict"; 15 + 16 + // ============ CONFIG ============ 17 + const CONFIG = { 18 + keepDays: 7, 19 + minDelay: 3000, 20 + maxDelay: 8000, 21 + breakEvery: 5, 22 + breakMin: 15000, 23 + breakMax: 45000, 24 + scrollAmount: 300, 25 + }; 26 + 27 + // ============ STATE ============ 28 + let running = false; 29 + let deleted = 0; 30 + let skipped = 0; 31 + let reactionsRemoved = 0; 32 + let abortController = null; 33 + const logs = []; 34 + 35 + // ============ HELPERS ============ 36 + 37 + function sleep(ms) { 38 + return new Promise((resolve, reject) => { 39 + const id = setTimeout(resolve, ms); 40 + abortController?.signal.addEventListener( 41 + "abort", 42 + () => { 43 + clearTimeout(id); 44 + reject(new Error("Aborted")); 45 + }, 46 + { once: true }, 47 + ); 48 + }); 49 + } 50 + 51 + function rand(min, max) { 52 + return Math.floor(Math.random() * (max - min + 1)) + min; 53 + } 54 + 55 + function log(msg) { 56 + const line = `[${new Date().toLocaleTimeString()}] ${msg}`; 57 + logs.push(line); 58 + if (logs.length > 200) logs.shift(); 59 + console.log("[Cleaner]", msg); 60 + const el = document.getElementById("dc-cleaner-log"); 61 + if (el) { 62 + el.textContent = logs.slice(-50).join("\n"); 63 + el.scrollTop = el.scrollHeight; 64 + } 65 + const st = document.getElementById("dc-cleaner-status"); 66 + if (st) st.textContent = msg; 67 + } 68 + 69 + function waitFor(fn, timeout = 10000) { 70 + return new Promise((resolve, reject) => { 71 + const start = Date.now(); 72 + const check = () => { 73 + const result = fn(); 74 + if (result) return resolve(result); 75 + if (Date.now() - start > timeout) 76 + return reject(new Error(`Timeout: ${fn.toString().slice(0, 80)}`)); 77 + requestAnimationFrame(check); 78 + }; 79 + check(); 80 + }); 81 + } 82 + 83 + function mouseOpts(el) { 84 + const r = el.getBoundingClientRect(); 85 + return { 86 + bubbles: true, 87 + cancelable: true, 88 + view: window, 89 + clientX: r.left + r.width / 2, 90 + clientY: r.top + r.height / 2, 91 + }; 92 + } 93 + 94 + function click(el) { 95 + if (!el) return; 96 + const o = mouseOpts(el); 97 + el.dispatchEvent(new MouseEvent("mouseenter", o)); 98 + el.dispatchEvent(new MouseEvent("mouseover", o)); 99 + el.dispatchEvent(new MouseEvent("mousedown", o)); 100 + el.dispatchEvent(new MouseEvent("mouseup", o)); 101 + el.dispatchEvent(new MouseEvent("click", o)); 102 + } 103 + 104 + function hover(el) { 105 + if (!el) return; 106 + const o = mouseOpts(el); 107 + el.dispatchEvent(new MouseEvent("mouseenter", o)); 108 + el.dispatchEvent(new MouseEvent("mouseover", o)); 109 + el.dispatchEvent(new MouseEvent("mousemove", o)); 110 + } 111 + 112 + // ============ DISCORD HELPERS ============ 113 + 114 + function detectUserId() { 115 + try { 116 + const chunks = window.webpackChunkdiscord_app; 117 + if (!chunks) return null; 118 + const req = chunks.push([[Symbol()], {}, (r) => r]); 119 + chunks.pop(); 120 + const stores = Object.values(req.c).filter( 121 + (m) => m?.exports?.default?.getToken || m?.exports?.getToken, 122 + ); 123 + const token = stores 124 + .map((m) => (m.exports.default || m.exports).getToken?.()) 125 + .find((t) => typeof t === "string"); 126 + if (!token) return null; 127 + const part = token.split(".")[0]; 128 + const padded = part + "==".slice(part.length % 4 || 4); 129 + return atob(padded); 130 + } catch { 131 + return null; 132 + } 133 + } 134 + 135 + function getScroller() { 136 + return document.querySelector('[class*="scroller__"][class*="auto_"]'); 137 + } 138 + 139 + function getMessageElements() { 140 + return [...document.querySelectorAll('[id^="chat-messages-"]')]; 141 + } 142 + 143 + function getMessageId(el) { 144 + return el.id.split("-").pop(); 145 + } 146 + 147 + // Discord snowflake → timestamp (ms since epoch) 148 + function snowflakeToTimestamp(id) { 149 + return Number(BigInt(id) >> 22n) + 1420070400000; 150 + } 151 + 152 + function isOlderThanKeepDays(msgId) { 153 + const cutoff = Date.now() - CONFIG.keepDays * 24 * 60 * 60 * 1000; 154 + return snowflakeToTimestamp(msgId) < cutoff; 155 + } 156 + 157 + // Determine if a message belongs to the target user. 158 + // Group-start messages have an avatar with the user ID in the URL. 159 + // Continuation messages inherit from the previous group-start. 160 + function isOwnMessage(msgEl, userId) { 161 + const avatar = msgEl.querySelector(`img[src*="avatars/${userId}"]`); 162 + if (avatar) return true; 163 + 164 + // Continuation message (no avatar) — walk back to group start 165 + if (!msgEl.querySelector('img[src*="avatars/"]')) { 166 + let prev = msgEl.previousElementSibling; 167 + while (prev && prev.id?.startsWith("chat-messages-")) { 168 + const prevAvatar = prev.querySelector('img[src*="avatars/"]'); 169 + if (prevAvatar) { 170 + return prevAvatar.src.includes(`avatars/${userId}`); 171 + } 172 + prev = prev.previousElementSibling; 173 + } 174 + } 175 + 176 + return false; 177 + } 178 + 179 + // Find all our reactions (the inner button that toggles on click) 180 + function getMyReactions() { 181 + return [ 182 + ...document.querySelectorAll( 183 + '[class*="reactionMe_"] [class*="reactionInner_"]', 184 + ), 185 + ]; 186 + } 187 + 188 + // ============ DELETE ============ 189 + 190 + function closeOpenMenu() { 191 + document.dispatchEvent( 192 + new KeyboardEvent("keydown", { 193 + key: "Escape", 194 + code: "Escape", 195 + keyCode: 27, 196 + bubbles: true, 197 + cancelable: true, 198 + }), 199 + ); 200 + } 201 + 202 + async function deleteMessage(messageEl) { 203 + const innerMsg = 204 + messageEl.querySelector('[class*="message_"]') || messageEl; 205 + 206 + hover(innerMsg); 207 + await sleep(rand(500, 1000)); 208 + 209 + const moreBtn = await waitFor( 210 + () => 211 + messageEl.querySelector('[aria-label="More"]') || 212 + innerMsg.querySelector('[aria-label="More"]'), 213 + 4000, 214 + ).catch(() => null); 215 + 216 + if (!moreBtn) { 217 + log(" More button not found, skipping"); 218 + skipped++; 219 + return false; 220 + } 221 + 222 + click(moreBtn); 223 + await sleep(rand(400, 800)); 224 + 225 + const deleteItem = await waitFor( 226 + () => 227 + [...document.querySelectorAll('[role="menuitem"]')].find((el) => 228 + el.textContent?.includes("Delete Message"), 229 + ), 230 + 5000, 231 + ).catch(() => null); 232 + 233 + if (!deleteItem) { 234 + closeOpenMenu(); 235 + await sleep(rand(300, 600)); 236 + log(" Delete option not in menu, skipping"); 237 + skipped++; 238 + return false; 239 + } 240 + 241 + click(deleteItem); 242 + await sleep(rand(500, 1000)); 243 + 244 + const confirmBtn = await waitFor(() => { 245 + const dialog = document.querySelector('[role="dialog"]'); 246 + if (!dialog) return null; 247 + return [...dialog.querySelectorAll("button")].find( 248 + (btn) => btn.textContent?.trim() === "Delete", 249 + ); 250 + }, 5000); 251 + 252 + click(confirmBtn); 253 + await sleep(rand(500, 1000)); 254 + 255 + deleted++; 256 + log(`Deleted ${deleted} messages`); 257 + return true; 258 + } 259 + 260 + // ============ MAIN LOOP ============ 261 + 262 + async function run() { 263 + const userId = detectUserId(); 264 + if (!userId) { 265 + log("Could not detect user ID. Cannot proceed."); 266 + running = false; 267 + updateUI(); 268 + return; 269 + } 270 + CONFIG.keepDays = parseInt(document.getElementById("dc-days").value) || 7; 271 + log(`Detected user ID: ${userId}`); 272 + log(`Will delete messages older than ${CONFIG.keepDays} days`); 273 + 274 + running = true; 275 + deleted = 0; 276 + skipped = 0; 277 + reactionsRemoved = 0; 278 + abortController = new AbortController(); 279 + updateUI(); 280 + 281 + try { 282 + const scroller = getScroller(); 283 + if (!scroller) { 284 + log("Could not find chat scroller. Are you in a channel/DM?"); 285 + running = false; 286 + updateUI(); 287 + return; 288 + } 289 + 290 + log("Starting deletion — scrolling through channel..."); 291 + const deletedIds = new Set(); 292 + let noNewContentCount = 0; 293 + const removeReactions = document.getElementById("dc-reactions").checked; 294 + 295 + while (running) { 296 + const allMessages = getMessageElements(); 297 + const myMessages = allMessages.filter((el) => isOwnMessage(el, userId)); 298 + 299 + const toDelete = myMessages.filter((el) => { 300 + const msgId = getMessageId(el); 301 + return !deletedIds.has(msgId) && isOlderThanKeepDays(msgId); 302 + }); 303 + 304 + if (toDelete.length > 0) { 305 + log(`Found ${toDelete.length} of my messages on screen`); 306 + noNewContentCount = 0; 307 + 308 + for (let i = toDelete.length - 1; i >= 0 && running; i--) { 309 + const msgEl = toDelete[i]; 310 + const msgId = getMessageId(msgEl); 311 + if (!document.contains(msgEl)) continue; 312 + 313 + const success = await deleteMessage(msgEl); 314 + deletedIds.add(msgId); 315 + 316 + if (success) { 317 + if (deleted % CONFIG.breakEvery === 0) { 318 + const ms = rand(CONFIG.breakMin, CONFIG.breakMax); 319 + log(`Break: ${(ms / 1000).toFixed(0)}s`); 320 + await sleep(ms); 321 + } else { 322 + const ms = rand(CONFIG.minDelay, CONFIG.maxDelay); 323 + log(`Waiting ${(ms / 1000).toFixed(1)}s...`); 324 + await sleep(ms); 325 + } 326 + } 327 + } 328 + } else if (removeReactions && getMyReactions().length > 0) { 329 + const myReactions = getMyReactions(); 330 + log(`Removing ${myReactions.length} reactions on screen...`); 331 + noNewContentCount = 0; 332 + 333 + for (const reactionEl of myReactions) { 334 + if (!running) break; 335 + if (!document.contains(reactionEl)) continue; 336 + click(reactionEl); 337 + reactionsRemoved++; 338 + await sleep(rand(800, 1500)); 339 + } 340 + } else { 341 + // Nothing to do on screen so scroll up to load older messages 342 + const prevScrollTop = scroller.scrollTop; 343 + const prevScrollHeight = scroller.scrollHeight; 344 + scroller.scrollTop -= CONFIG.scrollAmount; 345 + await sleep(rand(1500, 2500)); 346 + 347 + const scrollMoved = Math.abs(scroller.scrollTop - prevScrollTop) > 5; 348 + const heightGrew = scroller.scrollHeight > prevScrollHeight; 349 + 350 + if (!scrollMoved && !heightGrew) { 351 + noNewContentCount++; 352 + if (noNewContentCount >= 3) { 353 + log("Reached the top of the channel. No more messages to load."); 354 + break; 355 + } 356 + scroller.scrollTop -= CONFIG.scrollAmount; 357 + await sleep(rand(2000, 3000)); 358 + } else { 359 + noNewContentCount = 0; 360 + log("Scrolled up, loading older messages..."); 361 + } 362 + } 363 + } 364 + 365 + log( 366 + `Done! Deleted ${deleted} messages, removed ${reactionsRemoved} reactions (${skipped} skipped).`, 367 + ); 368 + } catch (err) { 369 + if (err.message !== "Aborted") log(`Fatal: ${err.message}`); 370 + else log(`Stopped. Deleted ${deleted}.`); 371 + } finally { 372 + running = false; 373 + updateUI(); 374 + } 375 + } 376 + 377 + function stop() { 378 + running = false; 379 + abortController?.abort(); 380 + log(`Stopped. Deleted ${deleted}.`); 381 + updateUI(); 382 + } 383 + 384 + // ============ UI ============ 385 + 386 + function updateUI() { 387 + const startBtn = document.getElementById("dc-start"); 388 + const stopBtn = document.getElementById("dc-stop"); 389 + if (startBtn) startBtn.disabled = running; 390 + if (stopBtn) stopBtn.disabled = !running; 391 + } 392 + 393 + function createUI() { 394 + const panel = document.createElement("div"); 395 + panel.id = "dc-panel"; 396 + panel.innerHTML = ` 397 + <style> 398 + #dc-panel { 399 + position: fixed; top: 20px; left: 20px; 400 + background: #1e1f22; border: 1px solid #3f4147; border-radius: 8px; 401 + padding: 12px 14px; color: #dbdee1; z-index: 999999; 402 + font-family: 'gg sans', sans-serif; font-size: 13px; 403 + min-width: 260px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); 404 + } 405 + #dc-panel h3 { 406 + margin: 0 0 8px; font-size: 14px; color: #fff; 407 + cursor: grab; user-select: none; 408 + } 409 + #dc-panel h3:active { cursor: grabbing; } 410 + #dc-panel .info { 411 + font-size: 11px; color: #949ba4; margin-bottom: 8px; line-height: 1.4; 412 + } 413 + #dc-cleaner-status { 414 + font-size: 12px; color: #b5bac1; margin: 4px 0; 415 + word-break: break-word; min-height: 16px; 416 + } 417 + #dc-cleaner-log { 418 + max-height: 130px; overflow-y: auto; margin: 6px 0; 419 + padding: 4px 6px; background: #111214; border-radius: 4px; 420 + font-size: 11px; font-family: monospace; color: #949ba4; 421 + white-space: pre-wrap; user-select: text; cursor: text; 422 + } 423 + #dc-panel .row { display: flex; gap: 6px; margin-top: 6px; } 424 + #dc-panel button { 425 + flex: 1; padding: 6px 0; border: none; border-radius: 4px; 426 + cursor: pointer; font-size: 12px; font-weight: 600; 427 + } 428 + #dc-start { background: #248046; color: #fff; } 429 + #dc-start:disabled { background: #1a5c33; opacity: 0.6; cursor: default; } 430 + #dc-stop { background: #da373c; color: #fff; } 431 + #dc-stop:disabled { background: #8c1f22; opacity: 0.6; cursor: default; } 432 + #dc-collapse { 433 + float: right; background: none; border: none; 434 + color: #949ba4; font-size: 16px; cursor: pointer; padding: 0; 435 + } 436 + </style> 437 + <button id="dc-collapse">&minus;</button> 438 + <h3>&#129529; Message Cleaner</h3> 439 + <div id="dc-body"> 440 + <div class="info"> 441 + Navigate to a DM or channel, then click Start.<br> 442 + The script will scroll up and delete your messages. 443 + </div> 444 + <label style="font-size:11px;color:#949ba4;display:block;margin:6px 0 2px;">Keep messages newer than (days)</label> 445 + <input type="number" id="dc-days" value="7" min="0" style="width:100%;box-sizing:border-box;background:#2b2d31;border:1px solid #3f4147;border-radius:4px;color:#dbdee1;padding:4px 8px;font-size:12px;" /> 446 + <label style="font-size:11px;color:#949ba4;display:flex;align-items:center;gap:6px;margin:8px 0 2px;cursor:pointer;"> 447 + <input type="checkbox" id="dc-reactions" checked style="margin:0;" /> 448 + Also remove my reactions 449 + </label> 450 + <div id="dc-cleaner-status" style="margin-top:8px;">Idle</div> 451 + <div id="dc-cleaner-log"></div> 452 + <div class="row"> 453 + <button id="dc-start">Start</button> 454 + <button id="dc-stop" disabled>Stop</button> 455 + </div> 456 + </div> 457 + `; 458 + document.body.appendChild(panel); 459 + 460 + document.getElementById("dc-start").addEventListener("click", run); 461 + document.getElementById("dc-stop").addEventListener("click", stop); 462 + 463 + let collapsed = false; 464 + document.getElementById("dc-collapse").addEventListener("click", () => { 465 + collapsed = !collapsed; 466 + document.getElementById("dc-body").style.display = collapsed 467 + ? "none" 468 + : "block"; 469 + document.getElementById("dc-collapse").textContent = collapsed 470 + ? "+" 471 + : "\u2212"; 472 + }); 473 + 474 + // Drag to reposition 475 + const handle = panel.querySelector("h3"); 476 + let dragging = false, 477 + dragX = 0, 478 + dragY = 0; 479 + handle.addEventListener("mousedown", (e) => { 480 + if (e.target.id === "dc-collapse") return; 481 + dragging = true; 482 + dragX = e.clientX - panel.offsetLeft; 483 + dragY = e.clientY - panel.offsetTop; 484 + }); 485 + document.addEventListener("mousemove", (e) => { 486 + if (!dragging) return; 487 + panel.style.left = e.clientX - dragX + "px"; 488 + panel.style.top = e.clientY - dragY + "px"; 489 + panel.style.right = "auto"; 490 + panel.style.bottom = "auto"; 491 + }); 492 + document.addEventListener("mouseup", () => { 493 + dragging = false; 494 + }); 495 + } 496 + 497 + function init() { 498 + const check = () => { 499 + if ( 500 + document.querySelector('[data-list-id="chat-messages"]') || 501 + document.querySelector('[class*="chat_"]') 502 + ) { 503 + createUI(); 504 + } else { 505 + setTimeout(check, 1000); 506 + } 507 + }; 508 + check(); 509 + } 510 + 511 + init(); 512 + })();