Adds a button to the Discord browser UI to mass delete messages from Discord channels and direct messages
// ==UserScript==
// @name DeleteCord
// @namespace https://greasyfork.org/en/users/1431907-theeeunknown
// @version 1.0
// @description Adds a button to the Discord browser UI to mass delete messages from Discord channels and direct messages
// @author TheeeUnknown
// @license MIT
// @match https://discord.com/*
// @match https://canary.discord.com/*
// @match https://ptb.discord.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const CFG = {
pageSizeFetch: 100,
pageDelayMs: 600,
minDeleteMs: 300,
maxDeleteMs: 8000,
emptyPageLimit: 3,
speedSamples: 20,
};
const creds = { token: null, authorId: null };
const _timestamps = [];
function recordDelete () {
_timestamps.push(Date.now());
if (_timestamps.length > CFG.speedSamples) _timestamps.shift();
}
function msgsPerMin () {
if (_timestamps.length < 2) return 0;
const span = (_timestamps[_timestamps.length - 1] - _timestamps[0]) / 60000;
return span > 0 ? Math.round((_timestamps.length - 1) / span) : 0;
}
function autoDelay (headers) {
const remaining = parseInt(headers.get('X-RateLimit-Remaining') ?? '-1');
const resetAfter = parseFloat(headers.get('X-RateLimit-Reset-After') ?? '0');
if (remaining < 0) return null;
if (remaining === 0) return resetAfter * 1000 + 150;
const ideal = (resetAfter * 1000 / remaining) * 1.15;
return Math.max(CFG.minDeleteMs, Math.min(ideal, CFG.maxDeleteMs));
}
const _origFetch = window.fetch;
window.fetch = async function (...args) {
try {
const [resource, options] = args;
const url = typeof resource === 'string' ? resource : resource?.url ?? '';
const hdrs = options?.headers ?? (resource instanceof Request ? resource.headers : null);
if (url.includes('discord.com/api') && !creds.token) {
const auth = hdrs instanceof Headers
? hdrs.get('Authorization')
: (typeof hdrs === 'object' ? (hdrs?.Authorization ?? hdrs?.authorization) : null);
if (auth) { creds.token = auth; _syncCredUI(); }
}
const res = await _origFetch.apply(this, args);
if (url.includes('/users/@me') && !url.includes('/settings') && !creds.authorId)
res.clone().json()
.then(d => { if (d?.id) { creds.authorId = d.id; _syncCredUI(); } })
.catch(() => {});
if (url.includes('/science') && !creds.authorId && options?.body)
try { const b = JSON.parse(options.body); if (b?.user_id) { creds.authorId = b.user_id; _syncCredUI(); } } catch {}
return res;
} catch { return _origFetch.apply(this, args); }
};
const _origSRH = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
if (!creds.token && (k === 'Authorization' || k === 'authorization'))
{ creds.token = v; _syncCredUI(); }
return _origSRH.apply(this, arguments);
};
function _syncCredUI () {
const tEl = document.getElementById('dmd-token');
const aEl = document.getElementById('dmd-author-id');
if (tEl && creds.token) tEl.value = creds.token;
if (aEl && creds.authorId) aEl.value = creds.authorId;
const dot = document.getElementById('dmd-cred-dot');
const lbl = document.getElementById('dmd-cred-label');
if (!dot || !lbl) return;
if (creds.token && creds.authorId) {
dot.className = 'dmd-dot green'; lbl.textContent = 'Ready to delete';
} else if (creds.token) {
dot.className = 'dmd-dot amber'; lbl.textContent = 'Token captured';
}
}
const ST = Object.freeze({ IDLE: 'idle', RUNNING: 'running', PAUSED: 'paused' });
let runState = ST.IDLE;
let stats = { deleted: 0, skipped: 0, failed: 0, scanned: 0 };
let chProg = { done: 0, total: 0, name: '' };
let curDelay = 1000;
const API = 'https://discord.com/api/v9';
async function _df (path, opts = {}) {
const token = document.getElementById('dmd-token')?.value.trim() || creds.token;
const url = path.startsWith('http') ? path : `${API}${path}`;
const res = await _origFetch(url, {
...opts,
headers: { Authorization: token, 'Content-Type': 'application/json', ...opts.headers },
});
return res;
}
async function _guildChannels (guildId) {
const res = await _df(`/guilds/${guildId}/channels`);
if (!res.ok) return [];
const all = await res.json();
return all
.filter(c => new Set([0, 5, 10, 11, 12]).has(c.type))
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
}
async function _dmChannels () {
const res = await _df('/users/@me/channels');
return res.ok ? res.json() : [];
}
async function _resolveAuthorId () {
const res = await _df('/users/@me');
if (!res.ok) return null;
const d = await res.json();
if (d?.id) { creds.authorId = d.id; _syncCredUI(); }
return d?.id ?? null;
}
async function _drainChannelViaSearch ({ channelId, authorId, guildId, keyword, hasFile, hasImage, hasVideo, hasSound, hasLink, hasEmbed, includePinned, includeNsfw, searchDelay, baseDeleteDelay }) {
let offset = 0;
let iterations = 0;
while (runState !== ST.IDLE) {
await _chkPause();
let API_SEARCH_URL = guildId === '@me'
? `/channels/${channelId}/messages/search?`
: `/guilds/${guildId}/messages/search?`;
const qs = new URLSearchParams({ sort_by: 'timestamp', sort_order: 'desc', offset: offset });
if (authorId) qs.set('author_id', authorId);
if (guildId !== '@me') qs.set('channel_id', channelId);
if (keyword) qs.set('content', keyword);
if (includeNsfw) qs.set('include_nsfw', 'true');
let urlStr = API_SEARCH_URL + qs.toString();
if (hasFile) urlStr += '&has=file';
if (hasImage) urlStr += '&has=image';
if (hasVideo) urlStr += '&has=video';
if (hasSound) urlStr += '&has=sound';
if (hasLink) urlStr += '&has=link';
if (hasEmbed) urlStr += '&has=embed';
const res = await _df(urlStr);
if (res.status === 202) {
const w = (await res.json()).retry_after * 1000 || searchDelay;
_log(`Indexing... ${(w/1000).toFixed(1)}s`, 'warn');
await _sleep(w);
continue;
}
if (!res.ok) {
if (res.status === 429) {
const w = (await res.json()).retry_after * 1000;
_log(`Rate limited. Cooling down...`, 'warn');
await _sleep(w * 1.5);
continue;
} else if (res.status === 403 || res.status === 404) {
_log(`No read access`, 'warn');
break;
}
_log(`Search failed: ${res.status}`, 'error');
break;
}
const data = await res.json();
stats.scanned = data.total_results || stats.scanned;
_updateStats();
if (!data.messages || data.messages.length === 0) {
if (data.total_results - offset > 0) {
offset += 25;
await _sleep(searchDelay);
continue;
}
break;
}
const discovered = data.messages.map(group => group.find(m => m.hit === true)).filter(Boolean);
const toDelete = discovered.filter(msg => msg.type === 0 || msg.type === 6 || msg.type === 19 || msg.type === 20 || (msg.pinned && includePinned));
const skipped = discovered.length - toDelete.length;
if (iterations === 0 && toDelete.length > 0) {
const previewMsgs = toDelete.slice(0, 5).map(m => {
let content = m.content || '';
if (m.attachments && m.attachments.length) content += ` [${m.attachments[0].filename}]`;
return `${m.author.username}: ${content}`;
}).join('\n');
const estTime = ((data.total_results / 25) * searchDelay) + (data.total_results * baseDeleteDelay);
const timeStr = msToHMS(estTime);
const prompt = `DeleteCord\n\nDelete ~${data.total_results} messages?\nTime: ${timeStr}\n\n${previewMsgs}`;
const confirmed = window.confirm(prompt);
if (!confirmed) {
runState = ST.IDLE;
throw new Error('ABORTED');
}
}
if (toDelete.length === 0) {
offset += skipped;
await _sleep(searchDelay);
continue;
}
iterations++;
for (const msg of toDelete) {
await _chkPause();
if (runState === ST.IDLE) throw new Error('ABORTED');
const ch = msg.channel_id || channelId;
let currentDeleteDelay = baseDeleteDelay;
const delRes = await _df(`/channels/${ch}/messages/${msg.id}`, { method: 'DELETE' });
const remaining = parseInt(delRes.headers.get('X-RateLimit-Remaining') ?? '-1');
const resetAfter = parseFloat(delRes.headers.get('X-RateLimit-Reset-After') ?? '0');
if (remaining === 0) {
currentDeleteDelay = (resetAfter * 1000) + 150;
} else if (remaining > 0) {
const idealDelay = (resetAfter * 1000 / remaining) * 1.15;
currentDeleteDelay = Math.max(currentDeleteDelay, idealDelay);
}
curDelay = currentDeleteDelay;
_updateLiveMetrics();
let logText = msg.content || '';
if (msg.attachments && msg.attachments.length > 0) logText += ` [${msg.attachments[0].filename}]`;
const snip = _trunc(logText || '[Empty]', 60);
if (delRes.ok || delRes.status === 404) {
stats.deleted++;
recordDelete();
_log(`✓ ${snip}`, 'success');
} else if (delRes.status === 429) {
const w = (await delRes.json()).retry_after * 1000;
_log(`Rate limit ${w}ms`, 'warn');
await _sleep(w * 1.5);
stats.failed++;
continue;
} else if (delRes.status === 403) {
stats.skipped++;
offset++;
_log(`Skipped ${msg.id}`, 'muted');
} else {
stats.failed++;
offset++;
_log(`Failed: ${msg.id}`, 'error');
}
_updateStats();
await _sleep(currentDeleteDelay);
}
offset += skipped;
await _sleep(searchDelay);
}
}
async function _startRun ({ authorId, channelId, guildId, mode, keyword, hasFile, hasImage, hasVideo, hasSound, hasLink, hasEmbed, includePinned, includeNsfw, searchDelay, deleteDelay }) {
if (mode === 'server') {
_log('Switch to Channel tab to delete', 'warn');
return;
}
stats = { deleted: 0, skipped: 0, failed: 0, scanned: 0 };
chProg = { done: 0, total: 1, name: '' };
_timestamps.length = 0;
curDelay = deleteDelay;
runState = ST.RUNNING;
_updateStats(); _updateUI(); _updateLiveMetrics();
try {
chProg.total = 1; _setChannel(channelId);
_log(`Scanning ${channelId}…`, 'info');
await _drainChannelViaSearch({ channelId, authorId, guildId, keyword, hasFile, hasImage, hasVideo, hasSound, hasLink, hasEmbed, includePinned, includeNsfw, searchDelay, baseDeleteDelay: deleteDelay });
if (runState !== ST.IDLE)
_log(
`Done • ${stats.deleted} deleted • ${stats.skipped} skipped • ${stats.failed} failed`,
'success'
);
} catch (e) {
if (e.message !== 'ABORTED') _log(`Error: ${e.message}`, 'error');
}
runState = ST.IDLE;
_updateUI(); _updateLiveMetrics();
}
const _sleep = ms => new Promise(r => setTimeout(r, ms));
const _trunc = (s, n) => s.length > n ? s.slice(0, n) + '…' : s;
const msToHMS = s => `${s / 3.6e6 | 0}h ${(s % 3.6e6) / 6e4 | 0}m ${(s % 6e4) / 1000 | 0}s`;
function _setChannel (name) { chProg.done++; chProg.name = name; _updateChBar(); }
async function _chkPause () {
while (runState === ST.PAUSED) await _sleep(100);
if (runState === ST.IDLE) throw new Error('ABORTED');
}
function _getIdsFromUrl () {
const p = location.pathname;
const dm = p.match(/\/channels\/@me\/(\d+)/);
const sv = p.match(/\/channels\/(\d+)\/(\d+)/);
if (dm) return { guildId: '@me', channelId: dm[1] };
if (sv) return { guildId: sv[1], channelId: sv[2] };
return { guildId: '', channelId: '' };
}
function _log (msg, type = 'info') {
const el = document.getElementById('dmd-log');
if (!el) return;
const palette = {
info: '#8e9297',
success: '#57f287',
warn: '#fee75c',
error: '#ed4245',
muted: '#555b62',
};
const line = document.createElement('div');
line.style.cssText = `color:${palette[type] ?? palette.info};padding:1px 0;font-size:11.5px;line-height:1.55;word-break:break-word;white-space:pre-wrap;`;
line.textContent = msg;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
while (el.children.length > 800) el.removeChild(el.firstChild);
}
function _updateStats () {
const g = id => document.getElementById(id);
const fmt = n => n.toLocaleString();
if (g('s-del')) g('s-del').textContent = fmt(stats.deleted);
if (g('s-ski')) g('s-ski').textContent = fmt(stats.skipped);
if (g('s-fai')) g('s-fai').textContent = fmt(stats.failed);
if (g('s-sca')) g('s-sca').textContent = fmt(stats.scanned);
}
function _updateLiveMetrics () {
const speed = document.getElementById('dmd-speed');
const delay = document.getElementById('dmd-cur-delay');
if (speed) speed.textContent = runState === ST.RUNNING ? `${msgsPerMin()}` : '—';
if (delay) delay.textContent = runState === ST.RUNNING ? `${Math.round(curDelay)}ms` : '—';
}
function _updateChBar () {
const bar = document.getElementById('dmd-ch-bar');
const txt = document.getElementById('dmd-ch-text');
if (!bar || !txt) return;
if (chProg.total > 0) {
bar.style.display = 'flex';
txt.textContent = chProg.total > 1
? `${chProg.done} / ${chProg.total} • ${_trunc(chProg.name, 30)}`
: _trunc(chProg.name, 44);
} else {
bar.style.display = 'none';
}
}
function _updateUI () {
const g = id => document.getElementById(id);
const runDot = g('dmd-run-dot');
const runLbl = g('dmd-run-lbl');
if (runState === ST.RUNNING) {
if (runDot) { runDot.className = 'dmd-dot green pulse'; }
if (runLbl) runLbl.textContent = 'Running';
_setBtnState('dmd-btn-start', false);
_setBtnState('dmd-btn-pause', true);
_setBtnState('dmd-btn-stop', true);
if (g('dmd-btn-pause')) g('dmd-btn-pause').textContent = '⏸ Pause';
} else if (runState === ST.PAUSED) {
if (runDot) { runDot.className = 'dmd-dot amber pulse'; }
if (runLbl) runLbl.textContent = 'Paused';
if (g('dmd-btn-pause')) g('dmd-btn-pause').textContent = '▶ Resume';
} else {
if (runDot) { runDot.className = 'dmd-dot gray'; }
if (runLbl) runLbl.textContent = 'Idle';
_setBtnState('dmd-btn-start', true);
_setBtnState('dmd-btn-pause', false);
_setBtnState('dmd-btn-stop', false);
if (g('dmd-btn-pause')) g('dmd-btn-pause').textContent = '⏸ Pause';
const cb = g('dmd-ch-bar');
if (cb) cb.style.display = 'none';
}
}
function _setBtnState (id, enabled) {
const el = document.getElementById(id);
if (!el) return;
el.disabled = !enabled;
el.style.opacity = enabled ? '1' : '0.32';
}
const PANEL_ID = 'dmd-panel';
function _buildPanel () {
if (document.getElementById(PANEL_ID)) return;
const css = document.createElement('style');
css.id = 'dmd-css';
css.textContent = `
#dmd-panel {
position:fixed; top:58px; right:14px; width:318px;
background:#111214; border:1px solid #232428; border-radius:12px;
z-index:99999; font-family:'gg sans','Noto Sans',Whitney,Helvetica,sans-serif;
box-shadow:0 8px 36px rgba(0,0,0,.65); overflow:hidden;
resize:both; min-width:258px; min-height:72px; color:#dcddde;
}
#dmd-bar {
display:flex; align-items:center; justify-content:space-between;
background:#0b0c0e; padding:10px 14px; cursor:move; user-select:none;
border-bottom:1px solid #1d1f23;
}
#dmd-bar-l { display:flex; align-items:center; gap:8px; }
#dmd-bar-title { font-size:13px; font-weight:700; color:#fff; letter-spacing:.01em; }
#dmd-bar-r { display:flex; gap:10px; }
#dmd-bar-r span { font-size:15px; opacity:.35; cursor:pointer; transition:opacity .15s; line-height:1; }
#dmd-bar-r span:hover { opacity:.8; }
.dmd-main-tabs { display:flex; background:#0b0c0e; border-bottom:1px solid #1d1f23; padding:0; gap:0; }
.dmd-main-tab {
flex:1; padding:10px 0; border:none; background:transparent; color:#8e9297;
font-size:11px; font-weight:600; cursor:pointer; transition:color .15s, border .15s;
font-family:inherit; border-bottom:2px solid transparent;
}
.dmd-main-tab:hover { color:#dcddde; }
.dmd-main-tab.on { color:#fff; border-bottom-color:#5865f2; }
#dmd-body { padding:13px 14px; overflow-y:auto; max-height:calc(100vh - 160px); }
.dmd-main-content { display:none; }
.dmd-main-content.active { display:block; }
.dmd-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
.dmd-dot.green { background:#57f287; }
.dmd-dot.amber { background:#fee75c; }
.dmd-dot.gray { background:#43454a; }
.dmd-dot.red { background:#ed4245; }
.dmd-dot.pulse { animation:dmd-blink 1.6s ease-in-out infinite; }
@keyframes dmd-blink { 0%,100%{opacity:1} 50%{opacity:.45} }
.dmd-lbl {
font-size:9.5px; font-weight:700; letter-spacing:.09em;
text-transform:uppercase; color:#43454a; margin-bottom:6px;
}
.dmd-in {
width:100%; box-sizing:border-box;
background:#1a1c1f; border:1px solid #2a2d31; border-radius:6px;
color:#dcddde; font-size:12px; padding:8px 10px; margin-bottom:5px;
outline:none; transition:border-color .15s; font-family:inherit;
}
.dmd-in:focus { border-color:#5865f2; }
.dmd-in::placeholder { color:#43454a; }
.dmd-tabs { display:flex; background:#0b0c0e; border-radius:7px; padding:3px; gap:2px; }
.dmd-tab {
flex:1; padding:6px 0; border:none; border-radius:5px;
background:transparent; color:#8e9297; font-size:11px; font-weight:600;
cursor:pointer; transition:background .15s, color .15s; font-family:inherit;
}
.dmd-tab:hover:not(.on) { color:#dcddde; }
.dmd-tab.on { background:#1a1c1f; color:#fff; }
.dmd-info {
font-size:11.5px; color:#8e9297; padding:9px 10px; line-height:1.55;
background:#1a1c1f; border:1px solid #2a2d31; border-radius:6px;
}
.dmd-hr { height:1px; background:#1d1f23; margin:10px 0; }
.dmd-ghost {
border:none; border-radius:5px; font-size:11px; font-weight:600;
padding:6px 10px; cursor:pointer; background:#1a1c1f; color:#8e9297;
transition:background .15s, color .15s; font-family:inherit; white-space:nowrap;
}
.dmd-ghost:hover { background:#2a2d31; color:#dcddde; }
.dmd-btn-row { display:flex; gap:6px; }
.dmd-btn {
flex:1; border:none; border-radius:6px; font-size:12px; font-weight:700;
padding:9px 0; cursor:pointer; transition:filter .15s, transform .1s;
font-family:inherit; letter-spacing:.02em;
}
.dmd-btn:hover:not(:disabled) { filter:brightness(1.12); transform:translateY(-1px); }
.dmd-btn:active:not(:disabled){ transform:translateY(0); }
.dmd-btn:disabled { cursor:not-allowed; }
.dmd-btn.red { background:#ed4245; color:#fff; }
.dmd-btn.ghost { background:#2a2d31; color:#8e9297; }
#dmd-strip {
display:flex; align-items:center; justify-content:space-between;
background:#1a1c1f; border:1px solid #2a2d31; border-radius:7px;
padding:8px 11px; margin-bottom:11px;
}
#dmd-strip-l { display:flex; align-items:center; gap:7px; }
#dmd-run-lbl { font-size:12px; font-weight:700; color:#dcddde; }
#dmd-strip-r { display:flex; gap:14px; }
.dmd-metric .v { font-size:13px; font-weight:700; color:#fff; font-variant-numeric:tabular-nums; text-align:right; }
.dmd-metric .k { font-size:9px; color:#43454a; text-transform:uppercase; letter-spacing:.06em; text-align:right; }
#dmd-cred-row { display:flex; align-items:center; gap:7px; margin-bottom:8px; }
#dmd-cred-label { font-size:11.5px; color:#8e9297; flex:1; min-width:0; }
#dmd-ch-bar {
display:none; align-items:center; gap:7px;
background:#1a1c1f; border:1px solid #2a2d31; border-radius:6px;
padding:6px 10px; margin-bottom:8px;
}
#dmd-ch-text { font-size:11px; color:#8e9297; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; }
#dmd-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:5px; margin-bottom:10px; }
.dmd-cell {
background:#1a1c1f; border:1px solid #2a2d31; border-radius:6px;
display:flex; flex-direction:column; align-items:center; padding:8px 3px;
}
.dmd-cell .n { font-size:15px; font-weight:700; color:#dcddde; line-height:1; font-variant-numeric:tabular-nums; }
.dmd-cell .l { font-size:8.5px; color:#43454a; text-transform:uppercase; letter-spacing:.07em; margin-top:3px; }
.dmd-cell.d .n { color:#57f287; }
.dmd-cell.f .n { color:#ed4245; }
.dmd-cell.s .n { color:#fee75c; }
#dmd-log {
background:#0b0c0e; border:1px solid #1d1f23; border-radius:6px;
padding:8px; height:138px; overflow-y:auto;
font-family:'Consolas','Courier New',monospace;
scrollbar-width:thin; scrollbar-color:#2a2d31 transparent;
}
`;
document.head.appendChild(css);
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<div id="dmd-bar">
<div id="dmd-bar-l">
<span style="font-size:16px;line-height:1;">⚡</span>
<span id="dmd-bar-title">DeleteCord</span>
</div>
<div id="dmd-bar-r">
<span id="dmd-min" title="Minimize">─</span>
<span id="dmd-cls" title="Close">✕</span>
</div>
</div>
<div class="dmd-main-tabs">
<button class="dmd-main-tab on" data-tab="main">Main</button>
<button class="dmd-main-tab" data-tab="filters">Filters</button>
<button class="dmd-main-tab" data-tab="scraper">Scraper</button>
</div>
<div id="dmd-body">
<div id="tab-main" class="dmd-main-content active">
<div id="dmd-strip">
<div id="dmd-strip-l">
<div class="dmd-dot gray" id="dmd-run-dot"></div>
<span id="dmd-run-lbl">Idle</span>
</div>
<div id="dmd-strip-r">
<div class="dmd-metric">
<div class="v" id="dmd-speed">—</div>
<div class="k">msgs/min</div>
</div>
<div class="dmd-metric">
<div class="v" id="dmd-cur-delay">—</div>
<div class="k">delay</div>
</div>
</div>
</div>
<div class="dmd-lbl">Credentials</div>
<div id="dmd-cred-row">
<div class="dmd-dot gray" id="dmd-cred-dot"></div>
<span id="dmd-cred-label">Click a channel…</span>
<button class="dmd-ghost" id="dmd-btn-fetchid">ID</button>
</div>
<input class="dmd-in" id="dmd-token" type="password" placeholder="Token" autocomplete="off"/>
<input class="dmd-in" id="dmd-author-id" placeholder="User ID"/>
<div class="dmd-hr"></div>
<div class="dmd-lbl">Channels</div>
<input class="dmd-in" id="dmd-channel-id" placeholder="Channel ID"/>
<input class="dmd-in" id="dmd-guild-id" placeholder="Server ID"/>
<button class="dmd-ghost" id="dmd-btn-fillurl" style="width:100%;text-align:center;margin-top:7px;">
Fill from URL
</button>
<div class="dmd-hr"></div>
<div id="dmd-grid">
<div class="dmd-cell d"><span class="n" id="s-del">0</span><span class="l">Deleted</span></div>
<div class="dmd-cell s"><span class="n" id="s-ski">0</span><span class="l">Skipped</span></div>
<div class="dmd-cell f"><span class="n" id="s-fai">0</span><span class="l">Failed</span></div>
<div class="dmd-cell"> <span class="n" id="s-sca">0</span><span class="l">Scanned</span></div>
</div>
<div class="dmd-lbl">Log</div>
<div id="dmd-log" style="height:160px;"></div>
<div id="dmd-ch-bar">
<span style="font-size:12px;">📢</span>
<span id="dmd-ch-text"></span>
</div>
<div class="dmd-btn-row" style="margin-top:10px;">
<button class="dmd-btn red" id="dmd-btn-start">Start</button>
<button class="dmd-btn ghost" id="dmd-btn-pause" disabled>Pause</button>
<button class="dmd-btn ghost" id="dmd-btn-stop" disabled>Stop</button>
</div>
</div>
<div id="tab-filters" class="dmd-main-content">
<div class="dmd-lbl">Search</div>
<input class="dmd-in" id="dmd-keyword" placeholder="Keyword filter" style="margin-bottom:10px;"/>
<div class="dmd-lbl">Content Type</div>
<div style="display:flex; flex-wrap:wrap; gap:10px; margin-bottom:10px;">
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-file"> File
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-image"> Image
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-video"> Video
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-sound"> Sound
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-link"> Link
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-has-embed"> Embed
</label>
</div>
<div class="dmd-lbl">Options</div>
<div style="display:flex; gap:10px; margin-bottom:10px;">
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-inc-pinned"> Pinned
</label>
<label style="color:#dcddde; font-size:11px; display:flex; align-items:center; gap:4px; cursor:pointer;">
<input type="checkbox" id="dmd-inc-nsfw" checked> NSFW
</label>
</div>
<div class="dmd-hr"></div>
<div class="dmd-lbl">Timing (ms)</div>
<div style="display:flex; gap:6px;">
<input class="dmd-in" id="dmd-search-delay" type="number" placeholder="Search" value="1000"/>
<input class="dmd-in" id="dmd-delete-delay" type="number" placeholder="Delete" value="1000"/>
</div>
</div>
<div id="tab-scraper" class="dmd-main-content">
<div class="dmd-lbl">Server Scraper</div>
<input class="dmd-in" id="dmd-guild-sv" placeholder="Server ID" style="margin-bottom:8px;"/>
<button class="dmd-ghost" id="dmd-btn-copy-chans" style="width:100%;margin-bottom:8px;">📋 Copy Channels</button>
<div class="dmd-lbl" style="margin-top:0;">Channels</div>
<textarea class="dmd-in" id="dmd-chlist-text" style="height:260px; resize:vertical; font-family:monospace; white-space:pre;" readonly placeholder="Navigate to server to auto-scrape channels..."></textarea>
</div>
</div>
`;
document.body.appendChild(panel);
_syncCredUI();
let mode = 'single';
panel.querySelectorAll('.dmd-main-tab').forEach(t => {
t.addEventListener('click', () => {
panel.querySelectorAll('.dmd-main-tab').forEach(x => x.classList.remove('on'));
t.classList.add('on');
document.querySelectorAll('.dmd-main-content').forEach(x => x.classList.remove('active'));
const tabId = `tab-${t.dataset.tab}`;
document.getElementById(tabId).classList.add('active');
});
});
document.getElementById('dmd-btn-copy-chans').addEventListener('click', () => {
const txt = document.getElementById('dmd-chlist-text');
if (txt && txt.value && !txt.value.includes("Navigate")) {
const guildId = document.getElementById('dmd-guild-sv').value || document.getElementById('dmd-guild-id').value;
const originalText = txt.value;
const urls = originalText.split('\n').map(line => {
const parts = line.split(' : ');
if (parts.length === 2) {
return `https://discord.com/channels/${guildId}/${parts[1].trim()}`;
}
return line;
}).join('\n');
txt.value = urls;
txt.select();
document.execCommand('copy');
txt.value = originalText;
_log('Copied to clipboard!', 'success');
const btn = document.getElementById('dmd-btn-copy-chans');
const oldText = btn.textContent;
btn.textContent = '✅';
setTimeout(() => btn.textContent = oldText, 2000);
} else {
_log('No channels to copy', 'warn');
}
});
{ let drag = false, ox = 0, oy = 0;
document.getElementById('dmd-bar').addEventListener('mousedown', e => {
if (e.target.id === 'dmd-min' || e.target.id === 'dmd-cls') return;
drag = true; ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop;
});
document.addEventListener('mousemove', e => {
if (!drag) return;
panel.style.right = 'auto';
panel.style.left = `${e.clientX - ox}px`;
panel.style.top = `${e.clientY - oy}px`;
});
document.addEventListener('mouseup', () => drag = false); }
let mini = false;
document.getElementById('dmd-min').addEventListener('click', () => {
mini = !mini;
document.getElementById('dmd-body').style.display = mini ? 'none' : 'block';
});
document.getElementById('dmd-cls').addEventListener('click', () => {
panel.remove(); document.getElementById('dmd-css')?.remove();
});
document.getElementById('dmd-btn-fetchid').addEventListener('click', async () => {
if (!creds.token && !document.getElementById('dmd-token').value.trim()) {
_log('No token detected', 'warn'); return;
}
_log('Fetching ID…', 'info');
const id = await _resolveAuthorId();
if (id) _log(`ID: ${id}`, 'success');
else _log('Failed', 'error');
});
document.getElementById('dmd-btn-fillurl').addEventListener('click', async () => {
const { guildId, channelId } = _getIdsFromUrl();
if (!channelId) { _log('Navigate to a channel first', 'warn'); return; }
document.getElementById('dmd-channel-id').value = channelId;
document.getElementById('dmd-guild-id').value = guildId;
if (guildId !== '@me') document.getElementById('dmd-guild-sv').value = guildId;
_log(`Ch: ${channelId} • Server: ${guildId}`, 'success');
const token = document.getElementById('dmd-token').value.trim();
if (token && guildId !== '@me') {
const chs = await _guildChannels(guildId);
if (chs.length) {
const el = document.getElementById('dmd-chlist-text');
if(el) el.value = chs.map(c => `#${c.name} : ${c.id}`).join('\n');
_log(`${chs.length} channels found`, 'success');
}
}
});
document.getElementById('dmd-btn-start').addEventListener('click', async () => {
const token = document.getElementById('dmd-token').value.trim();
const authorId = document.getElementById('dmd-author-id').value.trim();
if (!token) { _log('No token', 'error'); return; }
if (!authorId) { _log('No User ID', 'error'); return; }
creds.token = token; creds.authorId = authorId;
const channelId = document.getElementById('dmd-channel-id').value.trim();
const guildId = document.getElementById('dmd-guild-id').value.trim() || '@me';
if (!channelId) { _log('No Channel ID', 'error'); return; }
document.getElementById('dmd-log').innerHTML = '';
const searchDelay = parseInt(document.getElementById('dmd-search-delay').value) || 1000;
const deleteDelay = parseInt(document.getElementById('dmd-delete-delay').value) || 1000;
await _startRun({
authorId, channelId, guildId, mode: 'single',
keyword: document.getElementById('dmd-keyword').value.trim() || null,
hasFile: document.getElementById('dmd-has-file').checked,
hasImage: document.getElementById('dmd-has-image').checked,
hasVideo: document.getElementById('dmd-has-video').checked,
hasSound: document.getElementById('dmd-has-sound').checked,
hasLink: document.getElementById('dmd-has-link').checked,
hasEmbed: document.getElementById('dmd-has-embed').checked,
includePinned: document.getElementById('dmd-inc-pinned').checked,
includeNsfw: document.getElementById('dmd-inc-nsfw').checked,
searchDelay: searchDelay,
deleteDelay: deleteDelay
});
});
document.getElementById('dmd-btn-pause').addEventListener('click', () => {
if (runState === ST.RUNNING) {
runState = ST.PAUSED; _log('Paused', 'warn');
} else if (runState === ST.PAUSED) {
runState = ST.RUNNING; _log('Resumed', 'info');
}
_updateUI();
});
document.getElementById('dmd-btn-stop').addEventListener('click', () => {
runState = ST.IDLE; _log('Stopped', 'warn'); _updateUI(); _updateLiveMetrics();
});
setTimeout(() => {
const { guildId, channelId } = _getIdsFromUrl();
if (channelId) {
document.getElementById('dmd-channel-id').value = channelId;
document.getElementById('dmd-guild-id').value = guildId;
if (guildId !== '@me') document.getElementById('dmd-guild-sv').value = guildId;
}
_syncCredUI();
_log(creds.token ? 'Ready' : 'Click a channel', creds.token ? 'success' : 'info');
}, 400);
setInterval(_updateLiveMetrics, 1500);
_updateUI();
}
let _lastPath = location.pathname;
let _lastGuildScraped = null;
new MutationObserver(() => {
if (location.pathname === _lastPath || runState !== ST.IDLE) return;
_lastPath = location.pathname;
const { guildId, channelId } = _getIdsFromUrl();
if (!channelId) return;
const map = {
'dmd-channel-id': channelId,
'dmd-guild-id': guildId,
'dmd-guild-sv': guildId !== '@me' ? guildId : null,
};
for (const [id, val] of Object.entries(map)) {
const el = document.getElementById(id);
if (el && val) el.value = val;
}
if (guildId !== '@me' && guildId !== _lastGuildScraped) {
_lastGuildScraped = guildId;
_guildChannels(guildId).then(chs => {
const el = document.getElementById('dmd-chlist-text');
if (el && chs.length) {
el.value = chs.map(c => `#${c.name} : ${c.id}`).join('\n');
}
}).catch(()=>{});
}
}).observe(document.body, { childList: true, subtree: true });
function _addLauncher () {
if (document.getElementById('dmd-fab')) return;
const btn = document.createElement('button');
btn.id = 'dmd-fab';
btn.title = 'DeleteCord';
btn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2M10 11v6M14 11v6"/>
</svg>
`;
btn.style.cssText = `
position:fixed; bottom:24px; right:24px; width:52px; height:52px;
border-radius:50%; background: linear-gradient(135deg, #ed4245, #da373c);
color:#fff; font-size:20px; border:none; cursor:pointer; z-index:99999;
box-shadow: 0 4px 15px rgba(237,66,69,0.4);
display:flex; align-items:center; justify-content:center;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s;
`;
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.1)';
btn.style.boxShadow = '0 6px 20px rgba(237,66,69,0.6)';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1)';
btn.style.boxShadow = '0 4px 15px rgba(237,66,69,0.4)';
});
btn.addEventListener('click', () => {
document.getElementById(PANEL_ID) ? document.getElementById(PANEL_ID).remove() : _buildPanel();
});
document.body.appendChild(btn);
}
function _waitForDiscord () {
return new Promise(r => {
const t = setInterval(() => {
if (document.querySelector('[class*="sidebar"]') || document.querySelector('nav'))
{ clearInterval(t); r(); }
}, 500);
});
}
_waitForDiscord().then(_addLauncher);
})();