DeleteCord

Adds a button to the Discord browser UI to mass delete messages from Discord channels and direct messages

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();