diff --git a/discord-message-cleaner.user.js b/discord-message-cleaner.user.js index 66db082..ba09c77 100644 --- a/discord-message-cleaner.user.js +++ b/discord-message-cleaner.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name Discoraser -// @namespace discoraser +// @namespace https://azom.dev/discoraser // @version 1.0.0 // @description Userscript that deletes your Discord messages by scrolling through channels/DMs. Slow, thorough, and less likely to get caught (hopefully). // @match https://discord.com/* @@ -11,507 +11,524 @@ // ==/UserScript== (function () { - "use strict"; - - // ============ CONFIG ============ - const CONFIG = { - keepDays: 7, - minDelay: 3000, - maxDelay: 8000, - breakEvery: 5, - breakMin: 15000, - breakMax: 45000, - scrollAmount: 300, - }; - - // ============ STATE ============ - let running = false; - let deleted = 0; - let skipped = 0; - let reactionsRemoved = 0; - let abortController = null; - const logs = []; - - // ============ HELPERS ============ - - function sleep(ms) { - return new Promise((resolve, reject) => { - const id = setTimeout(resolve, ms); - abortController?.signal.addEventListener( - "abort", - () => { - clearTimeout(id); - reject(new Error("Aborted")); - }, - { once: true }, - ); - }); - } - - function rand(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; - } - - function log(msg) { - const line = `[${new Date().toLocaleTimeString()}] ${msg}`; - logs.push(line); - if (logs.length > 200) logs.shift(); - console.log("[Cleaner]", msg); - const el = document.getElementById("dc-cleaner-log"); - if (el) { - el.textContent = logs.slice(-50).join("\n"); - el.scrollTop = el.scrollHeight; - } - const st = document.getElementById("dc-cleaner-status"); - if (st) st.textContent = msg; - } - - function waitFor(fn, timeout = 10000) { - return new Promise((resolve, reject) => { - const start = Date.now(); - const check = () => { - const result = fn(); - if (result) return resolve(result); - if (Date.now() - start > timeout) - return reject(new Error(`Timeout: ${fn.toString().slice(0, 80)}`)); - requestAnimationFrame(check); - }; - check(); - }); - } - - function mouseOpts(el) { - const r = el.getBoundingClientRect(); - return { - bubbles: true, - cancelable: true, - view: window, - clientX: r.left + r.width / 2, - clientY: r.top + r.height / 2, + "use strict"; + + // ============ CONFIG ============ + const CONFIG = { + keepDays: 7, + minDelay: 3000, + maxDelay: 8000, + breakEvery: 5, + breakMin: 15000, + breakMax: 45000, + scrollAmount: 300, }; - } - - function click(el) { - if (!el) return; - const o = mouseOpts(el); - el.dispatchEvent(new MouseEvent("mouseenter", o)); - el.dispatchEvent(new MouseEvent("mouseover", o)); - el.dispatchEvent(new MouseEvent("mousedown", o)); - el.dispatchEvent(new MouseEvent("mouseup", o)); - el.dispatchEvent(new MouseEvent("click", o)); - } - - function hover(el) { - if (!el) return; - const o = mouseOpts(el); - el.dispatchEvent(new MouseEvent("mouseenter", o)); - el.dispatchEvent(new MouseEvent("mouseover", o)); - el.dispatchEvent(new MouseEvent("mousemove", o)); - } - - // ============ DISCORD HELPERS ============ - - function detectUserId() { - try { - const chunks = window.webpackChunkdiscord_app; - if (!chunks) return null; - const req = chunks.push([[Symbol()], {}, (r) => r]); - chunks.pop(); - const stores = Object.values(req.c).filter( - (m) => m?.exports?.default?.getToken || m?.exports?.getToken, - ); - const token = stores - .map((m) => (m.exports.default || m.exports).getToken?.()) - .find((t) => typeof t === "string"); - if (!token) return null; - const part = token.split(".")[0]; - const padded = part + "==".slice(part.length % 4 || 4); - return atob(padded); - } catch { - return null; + + // ============ STATE ============ + let running = false; + let deleted = 0; + let skipped = 0; + let reactionsRemoved = 0; + let abortController = null; + const logs = []; + + // ============ HELPERS ============ + + function sleep(ms) { + return new Promise((resolve, reject) => { + const id = setTimeout(resolve, ms); + abortController?.signal.addEventListener( + "abort", + () => { + clearTimeout(id); + reject(new Error("Aborted")); + }, + { once: true }, + ); + }); } - } - - function getScroller() { - return document.querySelector('[class*="scroller__"][class*="auto_"]'); - } - - function getMessageElements() { - return [...document.querySelectorAll('[id^="chat-messages-"]')]; - } - - function getMessageId(el) { - return el.id.split("-").pop(); - } - - // Discord snowflake → timestamp (ms since epoch) - function snowflakeToTimestamp(id) { - return Number(BigInt(id) >> 22n) + 1420070400000; - } - - function isOlderThanKeepDays(msgId) { - const cutoff = Date.now() - CONFIG.keepDays * 24 * 60 * 60 * 1000; - return snowflakeToTimestamp(msgId) < cutoff; - } - - // Determine if a message belongs to the target user via React fiber internals. - function isOwnMessage(msgEl, userId) { - const fiberKey = Object.keys(msgEl).find( - (k) => - k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance"), - ); - if (!fiberKey) return false; - - let fiber = msgEl[fiberKey]; - for (let i = 0; i < 15 && fiber; i++) { - const props = fiber.memoizedProps || fiber.pendingProps; - if (props?.message?.author) { - return props.message.author.id === userId; - } - fiber = fiber.return; + + function rand(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; } - return false; - } - - // Find all our reactions (the inner button that toggles on click) - function getMyReactions() { - return [ - ...document.querySelectorAll( - '[class*="reactionMe_"] [class*="reactionInner_"]', - ), - ]; - } - - // ============ DELETE ============ - - function closeOpenMenu() { - document.dispatchEvent( - new KeyboardEvent("keydown", { - key: "Escape", - code: "Escape", - keyCode: 27, - bubbles: true, - cancelable: true, - }), - ); - } - - async function deleteMessage(messageEl) { - const innerMsg = - messageEl.querySelector('[class*="message_"]') || messageEl; - - hover(innerMsg); - await sleep(rand(500, 1000)); - - const moreBtn = await waitFor( - () => - messageEl.querySelector('[aria-label="More"]') || - innerMsg.querySelector('[aria-label="More"]'), - 4000, - ).catch(() => null); - - if (!moreBtn) { - log(" More button not found, skipping"); - skipped++; - return false; + function log(msg) { + const line = `[${new Date().toLocaleTimeString()}] ${msg}`; + logs.push(line); + if (logs.length > 200) logs.shift(); + console.log("[Cleaner]", msg); + const el = document.getElementById("dc-cleaner-log"); + if (el) { + el.textContent = logs.slice(-50).join("\n"); + el.scrollTop = el.scrollHeight; + } + const st = document.getElementById("dc-cleaner-status"); + if (st) st.textContent = msg; } - click(moreBtn); - await sleep(rand(400, 800)); - - const deleteItem = await waitFor( - () => - [...document.querySelectorAll('[role="menuitem"]')].find((el) => - el.textContent?.includes("Delete Message"), - ), - 5000, - ).catch(() => null); - - if (!deleteItem) { - closeOpenMenu(); - await sleep(rand(300, 600)); - log(" Delete option not in menu, skipping"); - skipped++; - return false; + function waitFor(fn, timeout = 10000) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const check = () => { + const result = fn(); + if (result) return resolve(result); + if (Date.now() - start > timeout) + return reject( + new Error(`Timeout: ${fn.toString().slice(0, 80)}`), + ); + requestAnimationFrame(check); + }; + check(); + }); } - click(deleteItem); - await sleep(rand(500, 1000)); - - const confirmBtn = await waitFor(() => { - const dialog = document.querySelector('[role="dialog"]'); - if (!dialog) return null; - return [...dialog.querySelectorAll("button")].find( - (btn) => btn.textContent?.trim() === "Delete", - ); - }, 5000); - - click(confirmBtn); - await sleep(rand(500, 1000)); - - deleted++; - log(`Deleted ${deleted} messages`); - return true; - } - - // ============ MAIN LOOP ============ - - async function run() { - const userId = detectUserId(); - if (!userId) { - log("Could not detect user ID. Cannot proceed."); - running = false; - updateUI(); - return; + function mouseOpts(el) { + const r = el.getBoundingClientRect(); + return { + bubbles: true, + cancelable: true, + view: window, + clientX: r.left + r.width / 2, + clientY: r.top + r.height / 2, + }; } - const daysVal = parseInt(document.getElementById("dc-days").value); - if (isNaN(daysVal) || daysVal < 0) { - log("Invalid 'keep days' value. Please enter a number >= 0."); - running = false; - updateUI(); - return; + + function click(el) { + if (!el) return; + const o = mouseOpts(el); + el.dispatchEvent(new MouseEvent("mouseenter", o)); + el.dispatchEvent(new MouseEvent("mouseover", o)); + el.dispatchEvent(new MouseEvent("mousedown", o)); + el.dispatchEvent(new MouseEvent("mouseup", o)); + el.dispatchEvent(new MouseEvent("click", o)); } - CONFIG.keepDays = daysVal; - log(`Detected user ID: ${userId}`); - log(`Will delete messages older than ${CONFIG.keepDays} days`); - - running = true; - deleted = 0; - skipped = 0; - reactionsRemoved = 0; - abortController = new AbortController(); - updateUI(); - - try { - const scroller = getScroller(); - if (!scroller) { - log("Could not find chat scroller. Are you in a channel/DM?"); - running = false; - updateUI(); - return; - } - log("Starting deletion — scrolling through channel..."); - const deletedIds = new Set(); - let noNewContentCount = 0; - const removeReactions = document.getElementById("dc-reactions").checked; + function hover(el) { + if (!el) return; + const o = mouseOpts(el); + el.dispatchEvent(new MouseEvent("mouseenter", o)); + el.dispatchEvent(new MouseEvent("mouseover", o)); + el.dispatchEvent(new MouseEvent("mousemove", o)); + } - while (running) { - const allMessages = getMessageElements(); - const myMessages = allMessages.filter((el) => isOwnMessage(el, userId)); + // ============ DISCORD HELPERS ============ + + function detectUserId() { + try { + const chunks = window.webpackChunkdiscord_app; + if (!chunks) return null; + const req = chunks.push([[Symbol()], {}, (r) => r]); + chunks.pop(); + const stores = Object.values(req.c).filter( + (m) => m?.exports?.default?.getToken || m?.exports?.getToken, + ); + const token = stores + .map((m) => (m.exports.default || m.exports).getToken?.()) + .find((t) => typeof t === "string"); + if (!token) return null; + const part = token.split(".")[0]; + const padded = part + "==".slice(part.length % 4 || 4); + return atob(padded); + } catch { + return null; + } + } - const toDelete = myMessages.filter((el) => { - const msgId = getMessageId(el); - return !deletedIds.has(msgId) && isOlderThanKeepDays(msgId); - }); + function getScroller() { + return document.querySelector('[class*="scroller__"][class*="auto_"]'); + } - if (toDelete.length > 0) { - log(`Found ${toDelete.length} of my messages on screen`); - noNewContentCount = 0; - - for (let i = toDelete.length - 1; i >= 0 && running; i--) { - const msgEl = toDelete[i]; - const msgId = getMessageId(msgEl); - if (!document.contains(msgEl)) continue; - - const success = await deleteMessage(msgEl); - deletedIds.add(msgId); - - if (success) { - if (deleted % CONFIG.breakEvery === 0) { - const ms = rand(CONFIG.breakMin, CONFIG.breakMax); - log(`Break: ${(ms / 1000).toFixed(0)}s`); - await sleep(ms); - } else { - const ms = rand(CONFIG.minDelay, CONFIG.maxDelay); - log(`Waiting ${(ms / 1000).toFixed(1)}s...`); - await sleep(ms); - } - } - } - } else if (removeReactions && getMyReactions().length > 0) { - const myReactions = getMyReactions(); - log(`Removing ${myReactions.length} reactions on screen...`); - noNewContentCount = 0; - - for (const reactionEl of myReactions) { - if (!running) break; - if (!document.contains(reactionEl)) continue; - click(reactionEl); - reactionsRemoved++; - await sleep(rand(800, 1500)); - } - } else { - // Nothing to do on screen so scroll up to load older messages - const prevScrollTop = scroller.scrollTop; - const prevScrollHeight = scroller.scrollHeight; - scroller.scrollTop -= CONFIG.scrollAmount; - await sleep(rand(1500, 2500)); - - const scrollMoved = Math.abs(scroller.scrollTop - prevScrollTop) > 5; - const heightGrew = scroller.scrollHeight > prevScrollHeight; - - if (!scrollMoved && !heightGrew) { - noNewContentCount++; - if (noNewContentCount >= 3) { - log("Reached the top of the channel. No more messages to load."); - break; + function getMessageElements() { + return [...document.querySelectorAll('[id^="chat-messages-"]')]; + } + + function getMessageId(el) { + return el.id.split("-").pop(); + } + + // Discord snowflake → timestamp (ms since epoch) + function snowflakeToTimestamp(id) { + return Number(BigInt(id) >> 22n) + 1420070400000; + } + + function isOlderThanKeepDays(msgId) { + const cutoff = Date.now() - CONFIG.keepDays * 24 * 60 * 60 * 1000; + return snowflakeToTimestamp(msgId) < cutoff; + } + + // Determine if a message belongs to the target user via React fiber internals. + function isOwnMessage(msgEl, userId) { + const fiberKey = Object.keys(msgEl).find( + (k) => + k.startsWith("__reactFiber") || + k.startsWith("__reactInternalInstance"), + ); + if (!fiberKey) return false; + + let fiber = msgEl[fiberKey]; + for (let i = 0; i < 15 && fiber; i++) { + const props = fiber.memoizedProps || fiber.pendingProps; + if (props?.message?.author) { + return props.message.author.id === userId; } - scroller.scrollTop -= CONFIG.scrollAmount; - await sleep(rand(2000, 3000)); - } else { - noNewContentCount = 0; - log("Scrolled up, loading older messages..."); - } + fiber = fiber.return; } - } - - log( - `Done! Deleted ${deleted} messages, removed ${reactionsRemoved} reactions (${skipped} skipped).`, - ); - } catch (err) { - if (err.message !== "Aborted") log(`Fatal: ${err.message}`); - else log(`Stopped. Deleted ${deleted}.`); - } finally { - running = false; - updateUI(); + + return false; } - } - - function stop() { - running = false; - abortController?.abort(); - log(`Stopped. Deleted ${deleted}.`); - updateUI(); - } - - // ============ UI ============ - - function updateUI() { - const startBtn = document.getElementById("dc-start"); - const stopBtn = document.getElementById("dc-stop"); - if (startBtn) startBtn.disabled = running; - if (stopBtn) stopBtn.disabled = !running; - } - - function createUI() { - const panel = document.createElement("div"); - panel.id = "dc-panel"; - panel.innerHTML = ` - - -

🧹 Message Cleaner

-
-
- Navigate to a DM or channel, then click Start.
- The script will scroll up and delete your messages. -
- - - -
Idle
-
-
- - -
-
- `; - document.body.appendChild(panel); - - document.getElementById("dc-start").addEventListener("click", run); - document.getElementById("dc-stop").addEventListener("click", stop); - - let collapsed = false; - document.getElementById("dc-collapse").addEventListener("click", () => { - collapsed = !collapsed; - document.getElementById("dc-body").style.display = collapsed - ? "none" - : "block"; - document.getElementById("dc-collapse").textContent = collapsed - ? "+" - : "\u2212"; - }); - - // Drag to reposition - const handle = panel.querySelector("h3"); - let dragging = false, - dragX = 0, - dragY = 0; - handle.addEventListener("mousedown", (e) => { - if (e.target.id === "dc-collapse") return; - dragging = true; - dragX = e.clientX - panel.offsetLeft; - dragY = e.clientY - panel.offsetTop; - }); - document.addEventListener("mousemove", (e) => { - if (!dragging) return; - panel.style.left = e.clientX - dragX + "px"; - panel.style.top = e.clientY - dragY + "px"; - panel.style.right = "auto"; - panel.style.bottom = "auto"; - }); - document.addEventListener("mouseup", () => { - dragging = false; - }); - } - - function init() { - const check = () => { - if ( - document.querySelector('[data-list-id="chat-messages"]') || - document.querySelector('[class*="chat_"]') - ) { - createUI(); - } else { - setTimeout(check, 1000); - } - }; - check(); - } + } + + function stop() { + running = false; + abortController?.abort(); + log(`Stopped. Deleted ${deleted}.`); + updateUI(); + } + + // ============ UI ============ + + function updateUI() { + const startBtn = document.getElementById("dc-start"); + const stopBtn = document.getElementById("dc-stop"); + if (startBtn) startBtn.disabled = running; + if (stopBtn) stopBtn.disabled = !running; + } + + function createUI() { + const panel = document.createElement("div"); + panel.id = "dc-panel"; + panel.innerHTML = ` + + +

🧹 Message Cleaner

+
+
+ Navigate to a DM or channel, then click Start.
+ The script will scroll up and delete your messages. +
+ + + +
Idle
+
+
+ + +
+
+ `; + document.body.appendChild(panel); + + document.getElementById("dc-start").addEventListener("click", run); + document.getElementById("dc-stop").addEventListener("click", stop); + + let collapsed = false; + document.getElementById("dc-collapse").addEventListener("click", () => { + collapsed = !collapsed; + document.getElementById("dc-body").style.display = collapsed + ? "none" + : "block"; + document.getElementById("dc-collapse").textContent = collapsed + ? "+" + : "\u2212"; + }); + + // Drag to reposition + const handle = panel.querySelector("h3"); + let dragging = false, + dragX = 0, + dragY = 0; + handle.addEventListener("mousedown", (e) => { + if (e.target.id === "dc-collapse") return; + dragging = true; + dragX = e.clientX - panel.offsetLeft; + dragY = e.clientY - panel.offsetTop; + }); + document.addEventListener("mousemove", (e) => { + if (!dragging) return; + panel.style.left = e.clientX - dragX + "px"; + panel.style.top = e.clientY - dragY + "px"; + panel.style.right = "auto"; + panel.style.bottom = "auto"; + }); + document.addEventListener("mouseup", () => { + dragging = false; + }); + } + + function init() { + const check = () => { + if ( + document.querySelector('[data-list-id="chat-messages"]') || + document.querySelector('[class*="chat_"]') + ) { + createUI(); + } else { + setTimeout(check, 1000); + } + }; + check(); + } - init(); + init(); })();