// ==UserScript== // @name RemoveCord // @namespace https://github.com/05v/RemoveCord // @version 1.5.0 // @description Free open source Tampermonkey script to delete Discord messages in a server you can manage. Working UnDiscord alternative for Discord servers. // @author 05v // @license MIT // @homepageURL https://github.com/05v/RemoveCord // @supportURL https://github.com/05v/RemoveCord/issues // @downloadURL https://raw.githubusercontent.com/05v/RemoveCord/main/RemoveCord.user.js // @updateURL https://raw.githubusercontent.com/05v/RemoveCord/main/RemoveCord.user.js // @match https://discord.com/* // @match https://ptb.discord.com/* // @match https://canary.discord.com/* // @run-at document-start // @grant none // ==/UserScript== (function () { "use strict"; const PAGE_SCRIPT = function () { const NS = "removecord"; const API = "https://discord.com/api/v9"; const state = { token: null, superProps: null, me: null, running: false, stop: false, modal: null, lastScan: null, }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function rand(min, max) { return min + Math.random() * (max - min); } function randInt(min, max) { return Math.floor(rand(min, max + 1)); } function formatWait(ms) { const sec = ms / 1000; return sec >= 10 ? `${Math.round(sec)}s` : `${sec.toFixed(1)}s`; } function formatDuration(ms) { const sec = Math.max(0, Math.round(ms / 1000)); if (sec < 60) return `${sec}s`; const mins = Math.floor(sec / 60); const rem = sec % 60; if (mins < 60) return rem ? `${mins}m ${rem}s` : `${mins}m`; const hours = Math.floor(mins / 60); const leftMins = mins % 60; if (hours < 48) return leftMins ? `${hours}h ${leftMins}m` : `${hours}h`; const days = Math.floor(hours / 24); const leftHours = hours % 24; return leftHours ? `${days}d ${leftHours}h` : `${days}d`; } function formatDate(iso) { if (!iso) return "Unknown"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "Unknown"; return d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }); } function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[ch])); } function estimateWipe(count, channelCount, recentCount) { const n = Math.max(0, count); const recent = Math.max(0, Math.min(n, recentCount == null ? 0 : recentCount)); const old = n - recent; const searches = Math.max(1, Math.ceil(n / 25)); const batches = Math.ceil(recent / 100); return { low: 200 + searches * 180 + batches * 350 + old * 220, mid: 400 + searches * 320 + batches * 600 + old * 360, high: 800 + searches * 550 + batches * 1100 + old * 700, }; } function isRecent(id) { try { const created = Number((BigInt(id) >> 22n) + 1420070400000n); return Date.now() - created < 14 * 24 * 60 * 60 * 1000; } catch (_) { return false; } } async function humanWait(ms, onTick) { const end = Date.now() + ms; while (Date.now() < end) { if (state.stop) return; const left = end - Date.now(); if (onTick) onTick(left); await sleep(Math.min(250, left)); } } function headerGet(headers, name) { if (!headers) return null; if (typeof headers.get === "function") return headers.get(name); const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()); return key ? headers[key] : null; } function captureAuth(headers) { const auth = headerGet(headers, "Authorization"); if (auth && auth !== "undefined" && auth.length > 20) state.token = auth; const props = headerGet(headers, "X-Super-Properties"); if (props) state.superProps = props; } function hookNetwork() { const origFetch = window.fetch; window.fetch = function (...args) { try { captureAuth(args[1] && args[1].headers); } catch (_) {} return origFetch.apply(this, args); }; const origOpen = XMLHttpRequest.prototype.open; const origSet = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function (...args) { this.__rcHeaders = {}; return origOpen.apply(this, args); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { try { this.__rcHeaders[name] = value; if (name.toLowerCase() === "authorization") state.token = value; if (name.toLowerCase() === "x-super-properties") state.superProps = value; } catch (_) {} return origSet.apply(this, arguments); }; } function tokenFromWebpack() { const chunk = window.webpackChunkdiscord_app; if (!chunk) return null; let found = null; try { chunk.push([ [Symbol(NS)], {}, (req) => { if (!req || !req.c) return; for (const id of Object.keys(req.c)) { const exp = req.c[id] && req.c[id].exports; if (!exp) continue; const cands = [exp, exp.default, exp.Z, exp.ZP]; for (const cand of cands) { if (cand && typeof cand.getToken === "function") { try { const t = cand.getToken(); if (t && typeof t === "string" && t.length > 20) found = t; } catch (_) {} } } } }, ]); } catch (_) {} return found; } function getToken() { if (state.token) return state.token; const fromWp = tokenFromWebpack(); if (fromWp) state.token = fromWp; return state.token; } function routeParts() { const m = location.pathname.match(/^\/channels\/(\d+|@me)\/(\d+)/); if (!m) return { guildId: null, channelId: null }; return { guildId: m[1] === "@me" ? null : m[1], channelId: m[2], }; } async function api(method, path, body) { const token = getToken(); if (!token) throw new Error("Discord session not ready yet. Open a channel and try again."); const headers = { Authorization: token, Accept: "*/*", }; if (body !== undefined) headers["Content-Type"] = "application/json"; if (state.superProps) headers["X-Super-Properties"] = state.superProps; const res = await fetch(API + path, { method, headers, credentials: "include", body: body !== undefined ? JSON.stringify(body) : undefined, }); if (res.status === 429) { const data = await res.json().catch(() => ({})); const wait = Math.ceil((data.retry_after || 5) * 1000) + randInt(400, 2200); await sleep(wait); return api(method, path, body); } return res; } async function apiJson(method, path, body) { const res = await api(method, path, body); if (res.status === 204) return null; const data = await res.json().catch(() => ({})); if (!res.ok) { const msg = data.message || data.code || res.statusText || String(res.status); const err = new Error(msg); err.status = res.status; err.data = data; throw err; } return data; } function extractHits(data, authorId) { const hits = []; for (const group of data.messages || []) { let picked = group.find((m) => m.hit); if (!picked) { picked = group.find((m) => m.author && m.author.id === authorId); } if (picked) hits.push(picked); } return hits; } async function searchMessages(guildId, authorId, channelId, opts) { const options = opts || {}; const params = new URLSearchParams({ author_id: authorId, sort_by: "timestamp", sort_order: options.sortOrder || "desc", offset: String(options.offset || 0), include_nsfw: "true", }); if (channelId) params.set("channel_id", channelId); if (options.maxId) params.set("max_id", options.maxId); const res = await api("GET", `/guilds/${guildId}/messages/search?${params}`); if (res.status === 202) { const data = await res.json().catch(() => ({})); const wait = Math.ceil((data.retry_after || 2) * 1000); await sleep(wait); return searchMessages(guildId, authorId, channelId, options); } const data = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(data.message || "Search failed"); err.status = res.status; err.data = data; throw err; } return data; } async function deleteOne(channelId, messageId) { const res = await api("DELETE", `/channels/${channelId}/messages/${messageId}`); if (res.status === 204 || res.status === 404) return "ok"; if (res.status === 429) return "retry"; const data = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(data.message || `Delete failed (${res.status})`); err.status = res.status; throw err; } return "ok"; } async function bulkDelete(channelId, ids) { const chunk = ids.slice(0, 100); if (chunk.length === 1) return deleteOne(channelId, chunk[0]); const res = await api("POST", `/channels/${channelId}/messages/bulk-delete`, { messages: chunk, }); if (res.status === 204 || res.status === 404) return "ok"; const data = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(data.message || `Bulk delete failed (${res.status})`); err.status = res.status; throw err; } return "ok"; } async function loadMe() { if (state.me) return state.me; state.me = await apiJson("GET", "/users/@me"); return state.me; } async function loadUser(userId) { return apiJson("GET", `/users/${userId}`); } async function loadGuild(guildId) { return apiJson("GET", `/guilds/${guildId}`); } async function loadChannel(channelId) { return apiJson("GET", `/channels/${channelId}`); } async function searchMembers(guildId, query) { const q = encodeURIComponent(query); return apiJson("GET", `/guilds/${guildId}/members/search?query=${q}&limit=8`); } function qs(sel, root) { return (root || document).querySelector(sel); } function findToolbar() { const inbox = document.querySelector('[aria-label="Inbox"]'); if (!inbox) return null; let node = inbox; for (let i = 0; i < 8 && node; i++) { const hasHelp = node.querySelector('[aria-label="Help"]'); if (hasHelp && node !== inbox) return node; node = node.parentElement; } return inbox.closest('[class*="trailing"]'); } function iconSvg() { return ` `; } function injectStyles() { if (document.getElementById(`${NS}-css`)) return; const style = document.createElement("style"); style.id = `${NS}-css`; style.textContent = ` #${NS}-btn { display: flex; align-items: center; margin: 0 4px; } #${NS}-btn .${NS}-hit { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; cursor: pointer; border-radius: 8px; color: #fff; background: var(--status-danger, #f23f43); box-shadow: 0 0 0 2px rgba(242, 63, 67, 0.28); } #${NS}-btn .${NS}-hit:hover { background: #da373c; box-shadow: 0 0 0 2px rgba(242, 63, 67, 0.5); } #${NS}-overlay { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.7); display: flex; align-items: center; justify-content: center; font-family: var(--font-primary, gg sans, "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif); } #${NS}-modal { width: 480px; max-width: calc(100vw - 32px); background: var(--background-floating, #111214); color: var(--text-default, #dbdee1); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.4); overflow: hidden; } #${NS}-modal header { padding: 16px 16px 0; } #${NS}-modal h2 { margin: 0; font-size: 20px; line-height: 24px; color: var(--header-primary, #f2f3f5); font-weight: 700; } #${NS}-modal .${NS}-sub { margin: 6px 0 0; font-size: 14px; color: var(--header-secondary, #b5bac1); } #${NS}-modal .${NS}-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; } #${NS}-modal label { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--header-secondary, #b5bac1); } #${NS}-modal input[type="text"] { width: 100%; box-sizing: border-box; background: var(--input-background, #1e1f22); color: var(--text-default, #dbdee1); border: none; border-radius: 4px; padding: 10px; font-size: 16px; outline: none; } #${NS}-modal .${NS}-row { display: flex; gap: 8px; } #${NS}-modal .${NS}-row input { flex: 1; } #${NS}-modal .${NS}-hint { font-size: 12px; color: var(--text-muted, #949ba4); } #${NS}-modal .${NS}-picks { display: flex; flex-direction: column; gap: 4px; } #${NS}-modal .${NS}-pick { text-align: left; border: none; border-radius: 4px; cursor: pointer; background: var(--background-modifier-hover, #3f4248); color: var(--interactive-active, #f2f3f5); padding: 8px 10px; font-size: 14px; } #${NS}-modal .${NS}-pick:hover { background: var(--background-modifier-selected, #43464d); } #${NS}-modal .${NS}-stats { background: var(--background-secondary, #2b2d31); border-radius: 4px; padding: 10px 12px; font-size: 13px; line-height: 1.45; max-height: 280px; overflow: auto; } #${NS}-modal .${NS}-stat-k { color: var(--header-secondary, #b5bac1); } #${NS}-modal .${NS}-stat-v { color: var(--header-primary, #f2f3f5); font-weight: 600; } #${NS}-modal .${NS}-chans { margin: 6px 0 0; padding: 0; list-style: none; } #${NS}-modal .${NS}-chans li { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; color: var(--text-default, #dbdee1); } #${NS}-modal .${NS}-eta { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--background-tertiary, #1e1f22); } #${NS}-modal .${NS}-eta-main { font-size: 15px; font-weight: 700; color: var(--header-primary, #f2f3f5); } #${NS}-modal .${NS}-preview { margin: 8px 0 0; padding: 0; list-style: none; color: var(--text-muted, #949ba4); font-size: 12px; } #${NS}-modal .${NS}-preview li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${NS}-modal .${NS}-bar { height: 6px; border-radius: 99px; background: var(--background-tertiary, #1e1f22); overflow: hidden; } #${NS}-modal .${NS}-bar > div { height: 100%; width: 0; background: var(--status-danger, #f23f43); transition: width .2s ease; } #${NS}-modal footer { display: flex; justify-content: flex-end; gap: 8px; padding: 0 16px 16px; } #${NS}-modal button.${NS}-btn { border: none; border-radius: 4px; cursor: pointer; padding: 8px 16px; font-size: 14px; font-weight: 500; } #${NS}-modal button.${NS}-ghost { background: transparent; color: var(--text-default, #dbdee1); } #${NS}-modal button.${NS}-ghost:hover { text-decoration: underline; } #${NS}-modal button.${NS}-brand { background: var(--button-filled-brand-background, #5865f2); color: #fff; } #${NS}-modal button.${NS}-danger { background: var(--button-danger-background, #da373c); color: #fff; } #${NS}-modal button:disabled { opacity: .5; cursor: not-allowed; } #${NS}-modal .${NS}-err { color: var(--text-danger, #fa777c); font-size: 13px; } #${NS}-modal .${NS}-check { display: flex; align-items: center; gap: 8px; font-size: 14px; } #${NS}-modal .${NS}-check input { accent-color: var(--brand-500, #5865f2); } `; document.documentElement.appendChild(style); } function injectButton() { if (document.getElementById(`${NS}-btn`)) return; const toolbar = findToolbar(); if (!toolbar) return; const wrap = document.createElement("div"); wrap.id = `${NS}-btn`; wrap.innerHTML = `
Detecting server...
Uses your logged-in Discord session. Recent messages are bulk-deleted; older ones are deleted quickly one by one. Deleting another member's messages needs Manage Messages. It will not say finished while messages are still left. If Discord search lags, click Wipe again. Stop anytime.