Greasy Fork

来自缓存

Greasy Fork is available in English.

Discord Plus+ V1 — 批量删消息、消息记录、Ghost Ping 检测与导出

🗑 使用 Discord API v10 批量删除任意频道或私聊中的消息(智能限速、重试、日期/内容过滤)。👁 实时记录被删消息。👻 自动检测 Ghost Ping 并弹出提示。📦 将频道历史导出为 HTML/JSON/TXT。🔑 一键获取 Token + 账户信息。全能 Discord 工具包——数据不离开您的浏览器。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               🛠️ Discord Plus+ V1 — Bulk Delete, Msg Logger, Ghost Ping & Export
// @name:vi            Discord Plus+ V1 — Xóa tin nhắn hàng loạt, theo dõi xóa, ghost ping & xuất file
// @name:zh-CN         Discord Plus+ V1 — 批量删消息、消息记录、Ghost Ping 检测与导出
// @name:zh-TW         Discord Plus+ V1 — 批量刪訊息、訊息記錄、Ghost Ping 偵測與匯出
// @name:ru            Discord Plus+ V1 — Массовое удаление, логгер сообщений, Ghost Ping и экспорт
// @name:ja            Discord Plus+ V1 — 一括削除・メッセージログ・Ghost Ping 検出・エクスポート
// @name:ko            Discord Plus+ V1 — 대량 삭제, 메시지 로거, Ghost Ping 감지 및 내보내기
// @name:es            Discord Plus+ V1 — Borrado masivo, registro de mensajes, Ghost Ping y exportación
// @name:pt-BR         Discord Plus+ V1 — Exclusão em massa, logger de mensagens, Ghost Ping e exportação
// @name:fr            Discord Plus+ V1 — Suppression en masse, journal, Ghost Ping et export
// @name:de            Discord Plus+ V1 — Massenlöschung, Nachrichten-Logger, Ghost Ping & Export
// @name:tr            Discord Plus+ V1 — Toplu Silme, Mesaj Kaydedici, Ghost Ping ve Dışa Aktarma
// @name:id            Discord Plus+ V1 — Hapus Massal, Logger Pesan, Ghost Ping & Ekspor
// @name:pl            Discord Plus+ V1 — Masowe usuwanie, logger wiadomości, Ghost Ping i eksport
// @name:th            Discord Plus+ V1 — ลบข้อความจำนวนมาก, บันทึกข้อความ, Ghost Ping และส่งออก
// @name:ar            Discord Plus+ V1 — الحذف الجماعي, تسجيل الرسائل, Ghost Ping والتصدير

// @description        🗑 Bulk delete YOUR messages in any channel or DM using Discord API v10 (smart rate-limit, retry, date/content filters). 👁 Log deleted messages in real-time. 👻 Auto-detect ghost pings with toast alerts. 📦 Export channel history to HTML/JSON/TXT. 🔑 One-click token detector + account info. All-in-one Discord web toolkit — no data leaves your browser. Works with Tampermonkey & Violentmonkey.
// @description:vi     🗑 Xóa hàng loạt TIN NHẮN CỦA BẠN trong bất kỳ kênh hoặc DM nào bằng Discord API v10 (giới hạn tốc độ thông minh, thử lại, bộ lọc ngày/nội dung). 👁 Ghi lại tin nhắn bị xóa theo thời gian thực. 👻 Tự động phát hiện ghost ping với thông báo. 📦 Xuất lịch sử kênh sang HTML/JSON/TXT. 🔑 Phát hiện token một cú nhấp + thông tin tài khoản. Bộ công cụ Discord web all-in-one — không có dữ liệu rời khỏi trình duyệt của bạn.
// @description:zh-CN  🗑 使用 Discord API v10 批量删除任意频道或私聊中的消息(智能限速、重试、日期/内容过滤)。👁 实时记录被删消息。👻 自动检测 Ghost Ping 并弹出提示。📦 将频道历史导出为 HTML/JSON/TXT。🔑 一键获取 Token + 账户信息。全能 Discord 工具包——数据不离开您的浏览器。
// @description:ru     🗑 Массовое удаление ВАШИХ сообщений в любом канале или ЛС через Discord API v10 (умный rate-limit, повторные попытки, фильтры по дате и содержимому). 👁 Логирование удалённых сообщений в реальном времени. 👻 Автодетект ghost ping с уведомлениями. 📦 Экспорт истории канала в HTML/JSON/TXT. 🔑 Автоопределение токена + информация об аккаунте.
// @description:ja     🗑 Discord API v10でチャンネルまたはDM内のメッセージを一括削除(スマートなレートリミット・リトライ・日付/内容フィルタ)。👁 削除されたメッセージをリアルタイムで記録。👻 Ghost Pingを自動検出しトースト通知。📦 チャンネル履歴をHTML/JSON/TXTへエクスポート。🔑 ワンクリックでトークン取得&アカウント情報表示。
// @description:ko     🗑 Discord API v10으로 채널/DM에서 메시지 대량 삭제(스마트 rate-limit, 재시도, 날짜/내용 필터). 👁 삭제된 메시지 실시간 기록. 👻 Ghost Ping 자동 감지 + 토스트 알림. 📦 채널 기록을 HTML/JSON/TXT로 내보내기. 🔑 원클릭 토큰 감지 + 계정 정보.
// @description:zh-TW  🗑 使用 Discord API v10 批量刪除任意頻道或私聊中的訊息(智能限速、重試、日期/內容過濾)。👁 即時記錄被刪訊息。👻 自動偵測 Ghost Ping 並彈出提示。📦 將頻道歷史匯出為 HTML/JSON/TXT。🔑 一鍵獲取 Token + 帳戶資訊。全能 Discord 工具包——資料不離開您的瀏覽器。
// @description:es     🗑 Elimina en masa TUS mensajes en cualquier canal o DM con Discord API v10 (límite de velocidad inteligente, reintentos, filtros de fecha/contenido). 👁 Registra mensajes eliminados en tiempo real. 👻 Detecta automáticamente ghost pings con alertas. 📦 Exporta el historial del canal a HTML/JSON/TXT. 🔑 Detector de token con un clic + info de cuenta. Cero datos salen de tu navegador.
// @description:pt-BR  🗑 Exclua em massa SUAS mensagens em qualquer canal ou DM com Discord API v10 (rate-limit inteligente, tentativas, filtros de data/conteúdo). 👁 Registre mensagens deletadas em tempo real. 👻 Detecte automaticamente ghost pings com alertas. 📦 Exporte o histórico do canal para HTML/JSON/TXT. 🔑 Detector de token com um clique + info da conta. Nenhum dado sai do seu navegador.
// @description:fr     🗑 Supprimez en masse VOS messages dans n'importe quel salon ou DM via Discord API v10 (rate-limit intelligent, nouvelles tentatives, filtres date/contenu). 👁 Enregistrez les messages supprimés en temps réel. 👻 Détectez automatiquement les ghost pings. 📦 Exportez l'historique du salon en HTML/JSON/TXT. 🔑 Détection de token en un clic + infos du compte. Aucune donnée ne quitte votre navigateur.
// @description:de     🗑 Massenlöschung DEINER Nachrichten in jedem Kanal oder DM mit Discord API v10 (intelligentes Rate-Limit, Wiederholungen, Datum-/Inhaltsfilter). 👁 Gelöschte Nachrichten in Echtzeit protokollieren. 👻 Ghost Pings automatisch erkennen mit Benachrichtigung. 📦 Kanalverlauf als HTML/JSON/TXT exportieren. 🔑 Ein-Klick-Token-Erkennung + Kontoinformationen. Keine Daten verlassen Ihren Browser.
// @description:tr     🗑 Discord API v10 ile herhangi bir kanal veya DM'deki MESAJLARINIZI toplu silin (akıllı rate-limit, yeniden deneme, tarih/içerik filtreleri). 👁 Silinen mesajları gerçek zamanlı kaydedin. 👻 Ghost ping'leri otomatik tespit edin. 📦 Kanal geçmişini HTML/JSON/TXT olarak dışa aktarın. 🔑 Tek tıkla token tespiti + hesap bilgisi. Hiçbir veri tarayıcınızdan çıkmaz.
// @description:id     🗑 Hapus massal PESAN ANDA di channel atau DM mana pun menggunakan Discord API v10 (rate-limit cerdas, percobaan ulang, filter tanggal/konten). 👁 Catat pesan yang dihapus secara real-time. 👻 Deteksi ghost ping otomatis dengan notifikasi. 📦 Ekspor riwayat channel ke HTML/JSON/TXT. 🔑 Deteksi token satu klik + info akun. Tidak ada data yang keluar dari browser Anda.
// @description:pl     🗑 Masowe usuwanie TWOICH wiadomości na dowolnym kanale lub DM przez Discord API v10 (inteligentny rate-limit, ponowne próby, filtry daty/treści). 👁 Rejestruj usunięte wiadomości w czasie rzeczywistym. 👻 Automatyczne wykrywanie ghost pingów z powiadomieniami. 📦 Eksportuj historię kanału do HTML/JSON/TXT. 🔑 Wykrywanie tokena jednym kliknięciem + informacje o koncie.
// @description:th     🗑 ลบข้อความของคุณจำนวนมากในช่องหรือ DM ใดก็ได้ด้วย Discord API v10 (จำกัดอัตราอัจฉริยะ, ลองใหม่, กรองวันที่/เนื้อหา) 👁 บันทึกข้อความที่ถูกลบแบบเรียลไทม์ 👻 ตรวจจับ ghost ping อัตโนมัติพร้อมแจ้งเตือน 📦 ส่งออกประวัติช่องเป็น HTML/JSON/TXT 🔑 ตรวจจับ token คลิกเดียว + ข้อมูลบัญชี ไม่มีข้อมูลออกจากเบราว์เซอร์ของคุณ
// @description:ar     🗑 احذف رسائلك بشكل جماعي في أي قناة أو DM باستخدام Discord API v10 (حد معدل ذكي، إعادة المحاولة، فلاتر التاريخ/المحتوى). 👁 سجّل الرسائل المحذوفة في الوقت الفعلي. 👻 اكتشف Ghost Ping تلقائيًا مع إشعارات. 📦 صدّر تاريخ القناة بصيغة HTML/JSON/TXT. 🔑 كشف التوكن بنقرة واحدة + معلومات الحساب. لا تغادر أي بيانات متصفحك.

// @namespace          http://greasyfork.icu/users/1510019
// @version            1.0.0
// @author             2pixel
// @license            MIT
// @icon               https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png
// @icon64             https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png
// @homepageURL        https://discord.gg/Gvmd7deFtS
// @supportURL         https://discord.gg/Gvmd7deFtS

// @match              https://discord.com/*
// @match              https://discordapp.com/*
// @exclude            https://discord.com/developers/*

// @grant              GM_addStyle
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_xmlhttpRequest
// @connect            discord.com
// @connect            discordapp.com

// @run-at             document-idle
// @compatible         chrome   Tested on Chrome 120+ with Tampermonkey
// @compatible         firefox  Tested on Firefox 120+ with Tampermonkey / Violentmonkey
// @compatible         edge     Tested on Edge 120+ with Tampermonkey
// @compatible         brave    Recommended (Manifest V3 compatible)
// ==/UserScript==

'use strict';

// ─── CONSTANTS ───────────────────────────────────────────────────────────────
const VERSION       = '1.0.0';
const API           = 'https://discord.com/api/v10';
const DISCORD_EPOCH = 1420070400000n;
const COMMUNITY     = 'https://discord.gg/Gvmd7deFtS';
const MAX_RETRIES   = 5;
const PANEL_ID      = 'dp2_panel';
const TOGGLE_ID     = 'dp2_toggle';

// ─── STATE ───────────────────────────────────────────────────────────────────
const S = {
  token:       null,
  activeTab:   'delete',
  delRunning:  false,
  delStopped:  false,
  delCount:    0,
  failCount:   0,
  scanCount:   0,
  deletedLog:  [],   // { id, content, author, channel, time }
  ghostPings:  [],   // { author, channel, content, time }
  observers:   [],
};

// ─── UTILS ───────────────────────────────────────────────────────────────────
const sleep  = ms => new Promise(r => setTimeout(r, ms));

function snowflakeFromDate(d) {
  return ((BigInt(d.getTime()) - DISCORD_EPOCH) << 22n).toString();
}
function esc(s) {
  return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtTime(d) {
  return new Date(d).toLocaleTimeString('en-GB', { hour12:false });
}
function fmtDate(d) {
  return new Date(d).toLocaleDateString('en-GB') + ' ' + fmtTime(d);
}

// Multi-strategy token extraction
function grabToken() {
  // Strategy 1: iframe sandbox (most reliable)
  try {
    const f = document.createElement('iframe');
    f.style.display = 'none';
    document.body.appendChild(f);
    const t = f.contentWindow.localStorage.getItem('token');
    document.body.removeChild(f);
    if (t) return t.replace(/"/g,'');
  } catch(_) {}
  // Strategy 2: direct localStorage
  try {
    const t = localStorage.getItem('token');
    if (t) return t.replace(/"/g,'');
  } catch(_) {}
  // Strategy 3: webpack module cache
  try {
    const wpReq = window.webpackChunkdiscord_app?.push?.([[Symbol()],{},e=>e]);
    if (wpReq) {
      for (const id of Object.keys(wpReq.m || {})) {
        try {
          const mod = wpReq(id);
          if (mod?.default?.getToken) {
            const t = mod.default.getToken();
            if (t) return t;
          }
        } catch(_) {}
      }
    }
  } catch(_) {}
  return null;
}

function getChannelId() {
  return location.pathname.match(/channels\/(?:@me|\d+)\/(\d+)/)?.[1] || null;
}
function getGuildId() {
  return location.pathname.match(/channels\/(\d+)\//)?.[1] || null;
}

// ─── API LAYER ───────────────────────────────────────────────────────────────
async function apiCall(method, path, body, retries = 0) {
  if (!S.token) throw new Error('No token set');
  const opts = {
    method,
    headers: {
      'Authorization': S.token,
      'Content-Type':  'application/json',
      'X-Discord-Locale': 'en-US',
      'X-Super-Properties': btoa(unescape(encodeURIComponent(JSON.stringify({
        os:'Windows', browser:'Chrome', device:'',
        browser_version:'122.0.0.0', os_version:'10',
        release_channel:'stable', client_build_number:263192,
      })))),
    },
  };
  if (body) opts.body = JSON.stringify(body);
  let res;
  try { res = await fetch(`${API}${path}`, opts); }
  catch(e) {
    if (retries < MAX_RETRIES) { await sleep(1500); return apiCall(method, path, body, retries+1); }
    throw e;
  }
  if (res.status === 429) {
    const d = await res.json().catch(()=>({}));
    const w = Math.ceil((d.retry_after||2)*1000)+300;
    appendLog(`⏳ Rate limited — waiting ${(w/1000).toFixed(1)}s`, 'warn');
    await sleep(w);
    if (retries < MAX_RETRIES) return apiCall(method, path, body, retries+1);
    throw new Error('Too many rate-limit retries');
  }
  if (res.status === 401) throw new Error('Invalid token — refresh Discord');
  if (res.status === 403) throw new Error('No permission');
  if (res.status === 404) return null;
  return res;
}

async function getMe() {
  const r = await apiCall('GET','/users/@me');
  return r ? r.json() : null;
}

async function searchMsgs({ guildId, channelId, authorId, minId, maxId, content, hasLink, hasFile, nsfw }) {
  const p = new URLSearchParams();
  if (authorId) p.set('author_id', authorId);
  if (minId)    p.set('min_id', minId);
  if (maxId)    p.set('max_id', maxId);
  if (content)  p.set('content', content);
  if (hasLink)  p.set('has','link');
  if (hasFile)  p.set('has','file');
  p.set('include_nsfw', nsfw ? 'true':'false');
  p.set('limit','25');

  const ep = guildId
    ? `/guilds/${guildId}/messages/search?${p}`
    : `/channels/${channelId}/messages/search?${p}`;

  const r = await apiCall('GET', ep);
  if (!r) return null;
  if (r.status === 202) { await sleep(1600); return searchMsgs(...arguments); }
  return r.json();
}

async function deleteMsg(channelId, msgId) {
  const r = await apiCall('DELETE', `/channels/${channelId}/messages/${msgId}`);
  return r !== null;
}

// ─── FEATURE: BULK DELETE ────────────────────────────────────────────────────
async function runDelete(opts) {
  const { channelId, authorId, guildId, minDate, maxDate, content, hasLink, hasFile, nsfw, delayMs } = opts;
  S.delRunning = true; S.delStopped = false;
  S.delCount = S.failCount = S.scanCount = 0;

  setStatus('running');
  appendLog('🗑 Bulk delete started', 'brand');
  appendLog(`Channel: ${channelId}${guildId ? ` | Guild: ${guildId}` : ''}`, 'info');

  const minId = minDate ? snowflakeFromDate(minDate) : undefined;
  let   maxId = maxDate ? snowflakeFromDate(maxDate) : undefined;

  let round = 0;
  while (!S.delStopped) {
    round++;
    appendLog(`Round ${round} — searching…`, 'info');
    let data;
    try {
      data = await searchMsgs({ guildId, channelId, authorId, minId, maxId, content, hasLink, hasFile, nsfw });
    } catch(e) { appendLog('Search error: '+e.message,'error'); break; }
    await sleep(950);

    if (!data) { appendLog('Nothing returned.','warn'); break; }
    const msgs = (data.messages||[]).flat();
    S.scanCount += msgs.length;
    uiSync();

    if (!msgs.length) { appendLog('✅ No more messages found.','ok'); break; }
    const toDelete = authorId ? msgs.filter(m=>m.author?.id===authorId) : msgs;
    if (!toDelete.length) { appendLog('No matches in batch.','ok'); break; }
    appendLog(`Found ${toDelete.length} to delete`, 'info');

    for (const msg of toDelete) {
      if (S.delStopped) break;
      try {
        const ok = await deleteMsg(msg.channel_id||channelId, msg.id);
        if (ok) { S.delCount++; appendLog(`✓ ${msg.id}`, 'ok'); }
        else { appendLog(`Already gone: ${msg.id}`, 'warn'); }
      } catch(e) {
        S.failCount++;
        appendLog(`✗ ${msg.id}: ${e.message}`, 'error');
      }
      uiSync();
      await sleep(delayMs + Math.floor(Math.random()*250));
    }

    const oldest = toDelete.reduce((a,b)=>BigInt(a.id)<BigInt(b.id)?a:b);
    maxId = (BigInt(oldest.id)-1n).toString();
  }

  S.delRunning = false;
  setStatus(S.delStopped ? '' : 'done');
  appendLog(
    S.delStopped
      ? '⏹ Stopped by user.'
      : `✅ Done! Deleted: ${S.delCount} | Failed: ${S.failCount}`,
    S.delStopped ? 'warn' : 'brand'
  );
  toggleBtn('start', true);
  toggleBtn('stop', false);
}

// ─── FEATURE: DELETED MSG LOGGER ─────────────────────────────────────────────
function startLogger() {
  const obs = new MutationObserver(muts => {
    for (const m of muts) {
      for (const node of m.removedNodes) {
        if (node.nodeType !== 1) continue;
        const msgEl = node.id?.startsWith('chat-messages-') ? node
          : node.querySelector?.('[id^="chat-messages-"]');
        if (!msgEl) continue;

        const contentEl = msgEl.querySelector('[id^="message-content-"]');
        const authorEl  = msgEl.querySelector('h3 span[class*="username"]')
                       || msgEl.querySelector('[class*="username"]');
        const content   = contentEl?.innerText?.trim() || '';
        const author    = authorEl?.innerText?.trim()  || 'Unknown';
        if (content.length < 1) continue;

        const entry = { id: msgEl.id, content, author, channel: location.pathname.split('/').pop(), time: Date.now() };
        S.deletedLog.unshift(entry);
        if (S.deletedLog.length > 500) S.deletedLog.pop();
        renderLogList();

        // Ghost ping detection
        if (/@(everyone|here|\w+)/.test(content)) {
          S.ghostPings.unshift({ author, channel: entry.channel, content, time: entry.time });
          if (S.ghostPings.length > 200) S.ghostPings.pop();
          renderGhostList();
          showToast(`👻 Ghost ping by ${author}`, content.slice(0,80), '#7b1fa2');
        }
      }
    }
  });
  obs.observe(document.body, { childList:true, subtree:true });
  S.observers.push(obs);
}

// ─── FEATURE: EXPORT ─────────────────────────────────────────────────────────
async function exportChannel(channelId, format, limit) {
  appendLog(`Fetching up to ${limit} messages…`, 'info');
  let all = [], before;
  let remaining = limit;

  while (remaining > 0 && !S.delStopped) {
    const batch = Math.min(remaining, 100);
    const qs = `limit=${batch}${before ? `&before=${before}` : ''}`;
    const r  = await apiCall('GET', `/channels/${channelId}/messages?${qs}`);
    if (!r) break;
    const d = await r.json();
    if (!Array.isArray(d)||!d.length) break;
    all = all.concat(d);
    before = d[d.length-1].id;
    remaining -= d.length;
    appendLog(`Fetched ${all.length}…`, 'info');
    await sleep(600);
  }

  appendLog(`Collected ${all.length} messages.`, 'ok');
  let blob, filename;

  if (format === 'json') {
    blob = new Blob([JSON.stringify(all,null,2)], { type:'application/json' });
    filename = `discord-${channelId}.json`;
  } else if (format === 'txt') {
    const lines = all.map(m=>`[${fmtDate(m.timestamp)}] ${m.author.username}: ${m.content}`).join('\n');
    blob = new Blob([lines], { type:'text/plain' });
    filename = `discord-${channelId}.txt`;
  } else {
    const rows = all.map(m=>`<tr><td>${esc(fmtDate(m.timestamp))}</td><td style="color:#5865F2;font-weight:700">${esc(m.author.username)}</td><td>${esc(m.content)}</td></tr>`).join('');
    const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Export · ${esc(channelId)}</title><style>
      body{font-family:Whitney,sans-serif;background:#313338;color:#dbdee1;margin:0;padding:20px}
      h2{color:#fff;font-size:18px;margin-bottom:4px}p{color:#949ba4;font-size:12px;margin-bottom:16px}
      table{border-collapse:collapse;width:100%}th,td{padding:8px 12px;border-bottom:1px solid #3f4248;font-size:13px;text-align:left;vertical-align:top}
      th{color:#949ba4;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
      tr:hover td{background:#3e4148}td:first-child{white-space:nowrap;color:#6d6f78;width:160px}
    </style></head><body><h2>Discord Export · #${esc(channelId)}</h2>
    <p>${all.length} messages · ${fmtDate(Date.now())}</p>
    <table><thead><tr><th>Time</th><th>Author</th><th>Content</th></tr></thead><tbody>${rows}</tbody></table>
    </body></html>`;
    blob = new Blob([html], { type:'text/html' });
    filename = `discord-${channelId}.html`;
  }

  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  a.click();
  appendLog(`✅ Saved as ${filename}`, 'ok');
}

// ─── TOAST ────────────────────────────────────────────────────────────────────
function showToast(title, body, color='#5865F2') {
  document.getElementById('dp2_toast')?.remove();
  const el = document.createElement('div');
  el.id = 'dp2_toast';
  Object.assign(el.style, {
    position:'fixed', bottom:'90px', right:'80px',
    background: color, color:'#fff', padding:'10px 16px',
    borderRadius:'12px', fontSize:'12px', fontWeight:'700',
    zIndex:'1000001', pointerEvents:'none', maxWidth:'260px',
    boxShadow:'0 8px 24px rgba(0,0,0,.4)', lineHeight:'1.4',
    animation:'dp2fade 3s forwards',
  });
  el.innerHTML = `<div>${esc(title)}</div><div style="font-weight:400;opacity:.85;font-size:11px">${esc(body)}</div>`;
  document.body.appendChild(el);
  setTimeout(()=>el.remove(), 3100);
}

// ─── CSS ──────────────────────────────────────────────────────────────────────
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@600;700;800;900&display=swap');

@keyframes dp2fade {
  0%   { opacity:1; transform:translateY(0); }
  75%  { opacity:1; }
  100% { opacity:0; transform:translateY(-8px); }
}
@keyframes dp2pulse {
  0%,100% { opacity:1; }
  50%      { opacity:.35; }
}
@keyframes dp2spin {
  to { transform:rotate(360deg); }
}
@keyframes dp2bar {
  0%   { margin-left:-40%; width:40%; }
  60%  { margin-left:100%; width:40%; }
  100% { margin-left:100%; width:40%; }
}

#${PANEL_ID}, #${PANEL_ID} * {
  box-sizing:border-box;
  font-family:'Nunito', system-ui, sans-serif;
}

/* ── Float toggle button ──────────────────────────────────── */
#${TOGGLE_ID} {
  position:fixed; bottom:90px; right:18px;
  width:56px; height:36px; border-radius:999px;
  background:rgba(255,255,255,0.16);
  border:1px solid rgba(255,255,255,0.22);
  box-shadow:0 8px 24px rgba(0,0,0,.25);
  z-index:99998; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  transition:transform .18s, box-shadow .18s;
}
#${TOGGLE_ID}:hover { transform:translateY(-2px); box-shadow:0 12px 30px rgba(0,0,0,.32); }
#${TOGGLE_ID}:active { transform:scale(.97); }
.dp2-tog-icon { font-size:20px; transition:transform .3s; }
#${TOGGLE_ID}.dp2-active .dp2-tog-icon { transform:rotate(20deg); }

/* ── Panel ────────────────────────────────────────────────── */
#${PANEL_ID} {
  position:fixed; top:0; left:0;
  width:370px; max-width:94vw;
  max-height:min(580px, calc(100vh - 120px));
  display:flex; flex-direction:column;
  background:rgba(30,32,40,0.82);
  backdrop-filter:blur(36px) saturate(180%);
  -webkit-backdrop-filter:blur(36px) saturate(180%);
  border:1px solid rgba(255,255,255,0.10);
  border-radius:24px;
  box-shadow:0 24px 64px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.04) inset;
  z-index:99999; overflow:hidden;
  opacity:0; pointer-events:none;
  transform:translateY(18px) scale(.97);
  transition:opacity .32s ease, transform .38s cubic-bezier(.25,.46,.45,.94);
}
#${PANEL_ID}.dp2-show { opacity:1; pointer-events:all; transform:translateY(0) scale(1); }
#${PANEL_ID}.dp2-dragging { transition:none !important; }

/* ── Header ───────────────────────────────────────────────── */
.dp2-hdr {
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  padding:13px 15px 12px; cursor:move; user-select:none;
  display:flex; align-items:center; justify-content:space-between;
  flex:0 0 auto; border-radius:24px 24px 0 0;
}
.dp2-hdr-l { display:flex; align-items:center; gap:10px; }
.dp2-logo {
  width:38px; height:38px;
  background:rgba(255,255,255,0.16);
  border-radius:12px;
  display:flex; align-items:center; justify-content:center;
  font-size:20px;
}
.dp2-hdr-title { color:#fff; font-size:15px; font-weight:900; letter-spacing:.2px; line-height:1.2; }
.dp2-hdr-sub { color:rgba(255,255,255,.72); font-size:10px; font-weight:700; letter-spacing:.3px; margin-top:1px; }
.dp2-drag-dot { color:rgba(255,255,255,.7); font-size:22px; cursor:move; }

.dp2-win-btns { display:flex; gap:5px; align-items:center; }
.dp2-win-btn {
  width:11px; height:11px; border-radius:50%;
  border:none; cursor:pointer; padding:0;
  transition:filter .15s;
}
.dp2-win-btn:hover { filter:brightness(1.4); }
.dp2-win-btn.close    { background:#ff5f56; }
.dp2-win-btn.minimize { background:#27293d; }

/* ── Stat pills ───────────────────────────────────────────── */
.dp2-pills {
  display:flex; gap:6px; padding:8px 14px;
  background:rgba(0,0,0,.18); border-bottom:1px solid rgba(255,255,255,.07);
  flex:0 0 auto;
}
.dp2-pill {
  flex:1; display:flex; align-items:center; justify-content:center; gap:4px;
  background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.08);
  border-radius:999px; padding:5px 10px;
  font-size:11px; font-weight:800; color:#dbdee1;
}
.dp2-pill span { font-size:13px; font-weight:900; color:#fff; }

/* ── Tabs ─────────────────────────────────────────────────── */
.dp2-tabs {
  display:flex; gap:0; padding:0 14px;
  background:rgba(0,0,0,.14); border-bottom:1px solid rgba(255,255,255,.07);
  flex:0 0 auto; overflow-x:auto;
  scrollbar-width:none;
}
.dp2-tabs::-webkit-scrollbar { display:none; }
.dp2-tab {
  padding:9px 10px 8px; font-size:11px; font-weight:800;
  color:rgba(255,255,255,.35); cursor:pointer; white-space:nowrap;
  border-bottom:2px solid transparent; letter-spacing:.2px;
  transition:color .15s, border-color .15s;
}
.dp2-tab:hover { color:rgba(255,255,255,.65); }
.dp2-tab.active { color:#fff; border-bottom-color:#5865F2; }

/* ── Body ─────────────────────────────────────────────────── */
.dp2-body {
  padding:14px; overflow-y:auto; flex:1 1 auto;
  scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.15) transparent;
  display:flex; flex-direction:column; gap:10px;
}
.dp2-body::-webkit-scrollbar { width:6px; }
.dp2-body::-webkit-scrollbar-thumb { background:rgba(255,255,255,.15); border-radius:999px; }

.dp2-pane { display:none; flex-direction:column; gap:10px; }
.dp2-pane.active { display:flex; }

/* ── Section label ────────────────────────────────────────── */
.dp2-lbl {
  font-size:9.5px; font-weight:900; letter-spacing:.9px;
  text-transform:uppercase; color:rgba(255,255,255,.3); margin-bottom:4px;
}

/* ── Cards ────────────────────────────────────────────────── */
.dp2-card {
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.09);
  border-radius:16px; padding:12px;
}

/* ── Inputs ───────────────────────────────────────────────── */
.dp2-field { position:relative; flex:1; }
.dp2-field label {
  display:block; font-size:9.5px; font-weight:800; letter-spacing:.6px;
  text-transform:uppercase; color:rgba(255,255,255,.3); margin-bottom:4px;
}
.dp2-row { display:flex; gap:7px; }
.dp2-input {
  width:100%; background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.10);
  border-radius:10px; padding:8px 10px;
  color:#dbdee1; font-family:inherit; font-size:12px;
  font-weight:700; outline:none;
  transition:border-color .15s, box-shadow .15s;
}
.dp2-input:focus { border-color:#5865F2; box-shadow:0 0 0 3px rgba(88,101,242,.2); }
.dp2-input::placeholder { color:rgba(255,255,255,.18); font-weight:600; }
.dp2-input.error { border-color:#ed4245; }
.dp2-input[type="password"] { letter-spacing:3px; }
.dp2-input[type="password"]::placeholder { letter-spacing:normal; }
.dp2-inline-btn {
  position:absolute; right:7px; top:50%; transform:translateY(-50%);
  background:rgba(88,101,242,.25); border:none; border-radius:6px;
  color:#949cf7; font-size:9.5px; font-weight:900; font-family:inherit;
  padding:2px 6px; cursor:pointer; letter-spacing:.3px;
  transition:background .15s;
}
.dp2-inline-btn:hover { background:rgba(88,101,242,.45); }

/* ── Checkbox grid ────────────────────────────────────────── */
.dp2-chk-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; }
.dp2-chk {
  display:flex; align-items:center; gap:7px;
  background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08);
  border-radius:10px; padding:8px 10px; cursor:pointer;
  transition:border-color .15s;
}
.dp2-chk:hover { border-color:rgba(255,255,255,.15); }
.dp2-chk.on { border-color:#5865F244; background:rgba(88,101,242,.1); }
.dp2-chk-box {
  width:14px; height:14px; border-radius:4px;
  border:1.5px solid rgba(255,255,255,.2); background:rgba(0,0,0,.25);
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  transition:all .15s;
}
.dp2-chk.on .dp2-chk-box { background:#5865F2; border-color:#5865F2; }
.dp2-chk.on .dp2-chk-box::after {
  content:''; width:5px; height:3px;
  border-left:1.5px solid #fff; border-bottom:1.5px solid #fff;
  transform:rotate(-45deg) translate(.5px,-.5px);
}
.dp2-chk-lbl { font-size:11.5px; font-weight:700; color:rgba(255,255,255,.45); }
.dp2-chk.on .dp2-chk-lbl { color:#dbdee1; }

/* ── Slider ───────────────────────────────────────────────── */
.dp2-slider-row { display:flex; align-items:center; gap:10px; }
.dp2-slider {
  flex:1; -webkit-appearance:none;
  height:3px; border-radius:2px;
  background:rgba(255,255,255,.12); outline:none; cursor:pointer;
}
.dp2-slider::-webkit-slider-thumb {
  -webkit-appearance:none; width:14px; height:14px; border-radius:50%;
  background:#5865F2; cursor:pointer;
  box-shadow:0 0 8px rgba(88,101,242,.6);
  transition:transform .15s;
}
.dp2-slider::-webkit-slider-thumb:hover { transform:scale(1.2); }
.dp2-slider-val {
  font-size:11.5px; font-weight:800;
  color:#949cf7; min-width:42px; text-align:right;
}

/* ── Stats row ────────────────────────────────────────────── */
.dp2-stats-row {
  display:grid; grid-template-columns:1fr 1fr 1fr;
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:14px; padding:10px;
  gap:0;
}
.dp2-stat { text-align:center; }
.dp2-stat-val {
  font-size:20px; font-weight:900;
  color:#fff; line-height:1;
}
.dp2-stat-lbl {
  font-size:9px; font-weight:800; text-transform:uppercase;
  letter-spacing:.6px; color:rgba(255,255,255,.3); margin-top:3px;
}

/* ── Progress bar ─────────────────────────────────────────── */
.dp2-bar-track {
  height:3px; background:rgba(255,255,255,.08);
  border-radius:2px; overflow:hidden; margin-top:6px;
}
.dp2-bar-fill {
  height:100%;
  background:linear-gradient(90deg, #5865F2, #949cf7);
  border-radius:2px; width:0; transition:width .3s;
}
.dp2-bar-fill.running { animation:dp2bar 1.5s ease infinite; }

/* ── Log ──────────────────────────────────────────────────── */
.dp2-log {
  background:rgba(0,0,0,.3);
  border:1px solid rgba(255,255,255,.07);
  border-radius:10px; padding:8px 10px;
  height:80px; overflow-y:auto;
  font-family:'JetBrains Mono','Courier New',monospace;
  font-size:10px; color:rgba(255,255,255,.3);
}
.dp2-log::-webkit-scrollbar { width:3px; }
.dp2-log::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border-radius:2px; }
.dp2-ll { margin:1px 0; line-height:1.55; }
.dp2-ll.info  { color:rgba(255,255,255,.4); }
.dp2-ll.ok    { color:#57f287; }
.dp2-ll.warn  { color:#fee75c; }
.dp2-ll.error { color:#ed4245; }
.dp2-ll.brand { color:#949cf7; }

/* ── Buttons ──────────────────────────────────────────────── */
.dp2-btn-row { display:flex; gap:7px; }
.dp2-btn {
  flex:1; padding:10px 12px; border:none; border-radius:12px;
  font-family:inherit; font-size:12px; font-weight:900; cursor:pointer;
  transition:all .15s; letter-spacing:.2px;
  display:flex; align-items:center; justify-content:center; gap:6px;
}
.dp2-btn:active { transform:scale(.97); }
.dp2-btn-primary {
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  color:#fff; box-shadow:0 4px 16px rgba(88,101,242,.35);
}
.dp2-btn-primary:hover { filter:brightness(1.1); box-shadow:0 6px 22px rgba(88,101,242,.5); }
.dp2-btn-primary:disabled { background:rgba(255,255,255,.08); color:rgba(255,255,255,.25); box-shadow:none; cursor:not-allowed; }
.dp2-btn-secondary {
  background:rgba(255,255,255,.08);
  border:1px solid rgba(255,255,255,.10);
  color:rgba(255,255,255,.5);
}
.dp2-btn-secondary:hover { background:rgba(255,255,255,.12); color:#dbdee1; }
.dp2-btn-danger {
  background:rgba(237,66,69,.15); color:#ed4245;
  border:1px solid rgba(237,66,69,.25);
}
.dp2-btn-danger:hover { background:rgba(237,66,69,.25); border-color:#ed4245; }
.dp2-btn-green {
  background:linear-gradient(135deg, #3ba55d, #2d7d46);
  color:#fff; box-shadow:0 4px 14px rgba(59,165,93,.3);
}
.dp2-btn-green:hover { filter:brightness(1.08); }

/* ── Toggle cards (3-col) ─────────────────────────────────── */
.dp2-tc-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:7px; }
.dp2-tc {
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.09);
  border-radius:14px; padding:10px 9px;
  display:flex; flex-direction:column; gap:7px;
  transition:transform .15s, border-color .2s;
}
.dp2-tc.on { border-color:#5865F244; background:rgba(88,101,242,.1); }
.dp2-tc:hover { transform:translateY(-1px); }
.dp2-tc-top { display:flex; align-items:center; justify-content:space-between; }
.dp2-tc-ico { font-size:18px; line-height:1; }
.dp2-tc-bot { display:flex; align-items:center; justify-content:space-between; }
.dp2-tc-title { font-size:11px; font-weight:900; color:#dbdee1; }
.dp2-tc-state { font-size:10px; font-weight:800; color:rgba(255,255,255,.3); letter-spacing:.3px; }
.dp2-tc.on .dp2-tc-state { color:#949cf7; }

/* Mac-style switch */
.dp2-sw {
  width:38px; height:22px; border-radius:999px; border:none;
  background:rgba(255,255,255,.15); position:relative; cursor:pointer;
  flex-shrink:0; transition:background .18s;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);
}
.dp2-sw.on { background:#5865F2; }
.dp2-sw-thumb {
  position:absolute; top:2px; left:2px;
  width:18px; height:18px; border-radius:50%;
  background:#fff; box-shadow:0 3px 8px rgba(0,0,0,.2);
  transition:transform .18s;
}
.dp2-sw.on .dp2-sw-thumb { transform:translateX(16px); }

/* ── Warning box ──────────────────────────────────────────── */
.dp2-warn {
  background:rgba(254,231,92,.08);
  border:1px solid rgba(254,231,92,.18);
  border-radius:10px; padding:8px 10px;
  font-size:10.5px; font-weight:700;
  color:rgba(254,231,92,.75); line-height:1.5;
}

/* ── Message lists ────────────────────────────────────────── */
.dp2-list {
  display:flex; flex-direction:column; gap:5px;
  max-height:240px; overflow-y:auto;
}
.dp2-list::-webkit-scrollbar { width:4px; }
.dp2-list::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border-radius:2px; }
.dp2-list-empty {
  text-align:center; color:rgba(255,255,255,.2);
  font-size:11.5px; padding:24px 0; font-weight:700; line-height:1.6;
}
.dp2-li {
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:10px; padding:8px 10px; font-size:11.5px;
}
.dp2-li-head { display:flex; justify-content:space-between; margin-bottom:3px; }
.dp2-li-author { font-weight:900; color:#949cf7; font-size:11px; }
.dp2-li-time { font-size:10px; color:rgba(255,255,255,.25); font-family:monospace; }
.dp2-li-body { color:rgba(255,255,255,.55); line-height:1.45; word-break:break-word; font-weight:600; }
.dp2-li.ghost { border-color:rgba(123,31,162,.4); background:rgba(123,31,162,.1); }
.dp2-li.ghost .dp2-li-author { color:#c084fc; }

/* ── Select ───────────────────────────────────────────────── */
.dp2-select {
  width:100%; background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.10);
  border-radius:10px; padding:8px 10px;
  color:#dbdee1; font-family:inherit; font-size:12px;
  font-weight:700; outline:none; appearance:none; cursor:pointer;
}
.dp2-select:focus { border-color:#5865F2; box-shadow:0 0 0 3px rgba(88,101,242,.2); }

/* ── About card ───────────────────────────────────────────── */
.dp2-about {
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.09);
  border-radius:18px; padding:18px; text-align:center;
}
.dp2-about-logo {
  width:52px; height:52px; margin:0 auto 12px;
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  border-radius:16px; display:flex; align-items:center; justify-content:center;
  font-size:26px; box-shadow:0 6px 20px rgba(88,101,242,.4);
}
.dp2-about-title { font-size:18px; font-weight:900; color:#fff; }
.dp2-about-sub { font-size:10.5px; color:rgba(255,255,255,.35); margin:4px 0 14px; font-weight:700; }
.dp2-feat-list { text-align:left; margin-bottom:14px; }
.dp2-feat-list li {
  list-style:none; font-size:11.5px; font-weight:700;
  color:rgba(255,255,255,.55); padding:3px 0; line-height:1.45;
}
.dp2-feat-list li::before { content:'→ '; color:#5865F2; font-weight:900; }
.dp2-community-btn {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  width:100%; padding:11px 16px; border-radius:12px;
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  color:#fff; font-size:13px; font-weight:900;
  text-decoration:none; cursor:pointer;
  box-shadow:0 4px 16px rgba(88,101,242,.35);
  transition:filter .15s, transform .15s;
}
.dp2-community-btn:hover { filter:brightness(1.1); transform:translateY(-1px); }
.dp2-foot {
  padding:9px 14px;
  background:rgba(0,0,0,.15);
  border-top:1px solid rgba(255,255,255,.06);
  border-radius:0 0 24px 24px;
  text-align:center; flex:0 0 auto;
}
.dp2-foot-txt { font-size:10px; font-weight:700; color:rgba(255,255,255,.25); }

/* ── User info card ───────────────────────────────────────── */
.dp2-user-card {
  background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.08);
  border-radius:12px; padding:12px;
  font-size:12px; font-weight:700;
}
.dp2-status-dot {
  display:inline-block; width:8px; height:8px; border-radius:50%;
  background:rgba(255,255,255,.15); margin-right:4px;
  vertical-align:middle; transition:background .3s, box-shadow .3s;
}
.dp2-status-dot.running { background:#fee75c; box-shadow:0 0 6px #fee75c; animation:dp2pulse 1.2s ease infinite; }
.dp2-status-dot.ok      { background:#57f287; box-shadow:0 0 6px #57f287; }
`);

// ─── BUILD UI ─────────────────────────────────────────────────────────────────
function buildPanel() {
  const tog = document.createElement('div');
  tog.id = TOGGLE_ID;
  const togIcon = document.createElement('span');
  togIcon.className = 'dp2-tog-icon';
  togIcon.textContent = '🛠️';
  tog.appendChild(togIcon);
  document.body.appendChild(tog);

  const panel = document.createElement('div');
  panel.id = PANEL_ID;
  panel.innerHTML = `
    <!-- Header -->
    <div class="dp2-hdr">
      <div class="dp2-hdr-l">
        <div class="dp2-logo">🛠️</div>
        <div>
          <div class="dp2-hdr-title">Discord Plus+ <span style="font-size:10px;opacity:.6;font-weight:700">V1</span></div>
          <div class="dp2-hdr-sub">DELETE · LOG · GHOST · EXPORT · TOKEN</div>
        </div>
      </div>
      <div style="display:flex;align-items:center;gap:8px">
        <span class="dp2-drag-dot">⋮</span>
        <div class="dp2-win-btns">
          <button class="dp2-win-btn minimize" id="dp2-min"></button>
          <button class="dp2-win-btn close"    id="dp2-close"></button>
        </div>
      </div>
    </div>

    <!-- Stats pills -->
    <div class="dp2-pills">
      <div class="dp2-pill">🗑 Deleted: <span id="dp2-s-del">0</span></div>
      <div class="dp2-pill">✗ Failed: <span id="dp2-s-fail">0</span></div>
      <div class="dp2-pill">🔍 Scanned: <span id="dp2-s-scan">0</span></div>
    </div>

    <!-- Tabs -->
    <div class="dp2-tabs">
      <div class="dp2-tab active" data-tab="delete">🗑 Delete</div>
      <div class="dp2-tab" data-tab="logger">👁 Logger</div>
      <div class="dp2-tab" data-tab="ghost">👻 Ghost</div>
      <div class="dp2-tab" data-tab="export">📦 Export</div>
      <div class="dp2-tab" data-tab="token">🔑 Token</div>
      <div class="dp2-tab" data-tab="about">ℹ About</div>
    </div>

    <!-- Body -->
    <div class="dp2-body">

      <!-- ═══ DELETE ═══ -->
      <div class="dp2-pane active" id="dp2-pane-delete">

        <div class="dp2-card">
          <div class="dp2-lbl">Authentication</div>
          <div class="dp2-field">
            <label>Token</label>
            <input class="dp2-input" id="dp2-token" type="password" placeholder="auto-detected or paste here">
            <button class="dp2-inline-btn" id="dp2-auto-tok">AUTO</button>
          </div>
          <div class="dp2-warn" style="margin-top:8px">⚠ Token never leaves your browser. NEVER share it.</div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Target</div>
          <div class="dp2-row">
            <div class="dp2-field">
              <label>Channel ID</label>
              <input class="dp2-input" id="dp2-channel" placeholder="from URL">
              <button class="dp2-inline-btn" id="dp2-auto-ch">AUTO</button>
            </div>
            <div class="dp2-field">
              <label>Guild ID (opt)</label>
              <input class="dp2-input" id="dp2-guild" placeholder="server">
              <button class="dp2-inline-btn" id="dp2-auto-guild">AUTO</button>
            </div>
          </div>
          <div class="dp2-field" style="margin-top:7px">
            <label>Author ID (blank = all)</label>
            <input class="dp2-input" id="dp2-author" placeholder="your user ID">
            <button class="dp2-inline-btn" id="dp2-auto-me">ME</button>
          </div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Filters</div>
          <div class="dp2-row" style="margin-bottom:7px">
            <div class="dp2-field"><label>From date</label><input class="dp2-input" id="dp2-from" type="date"></div>
            <div class="dp2-field"><label>To date</label><input class="dp2-input" id="dp2-to" type="date"></div>
          </div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Contains text</label>
            <input class="dp2-input" id="dp2-content" placeholder="keyword…">
          </div>
          <div class="dp2-chk-grid">
            <div class="dp2-chk" id="chk-link"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Has Link</span></div>
            <div class="dp2-chk" id="chk-file"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Has File</span></div>
            <div class="dp2-chk" id="chk-nsfw"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">NSFW</span></div>
            <div class="dp2-chk" id="chk-pin"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Pinned</span></div>
          </div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Delete delay <span id="dp2-status-lbl" style="margin-left:6px;font-size:9px;color:rgba(255,255,255,.3)"><span class="dp2-status-dot" id="dp2-sdot"></span>Idle</span></div>
          <div class="dp2-slider-row">
            <input class="dp2-slider" id="dp2-delay" type="range" min="300" max="3000" value="750" step="50">
            <span class="dp2-slider-val" id="dp2-delay-lbl">750ms</span>
          </div>
        </div>

        <div class="dp2-stats-row">
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-del">0</div><div class="dp2-stat-lbl">Deleted</div></div>
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-fail" style="color:#ed4245">0</div><div class="dp2-stat-lbl">Failed</div></div>
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-scan" style="color:rgba(255,255,255,.4)">0</div><div class="dp2-stat-lbl">Scanned</div></div>
        </div>
        <div class="dp2-bar-track"><div class="dp2-bar-fill" id="dp2-bar"></div></div>

        <div class="dp2-log" id="dp2-log"></div>

        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-btn">Clear</button>
          <button class="dp2-btn dp2-btn-primary"   id="dp2-start-btn">🗑 Start</button>
          <button class="dp2-btn dp2-btn-danger"    id="dp2-stop-btn" style="display:none">⏹ Stop</button>
        </div>
      </div>

      <!-- ═══ LOGGER ═══ -->
      <div class="dp2-pane" id="dp2-pane-logger">
        <div class="dp2-card">
          <div class="dp2-lbl">Deleted Message Log <span id="dp2-log-cnt" style="color:#949cf7"></span></div>
          <p style="font-size:11px;color:rgba(255,255,255,.3);font-weight:700;margin-bottom:8px">
            Captures messages removed from the DOM while you're in a channel.
          </p>
          <div class="dp2-list" id="dp2-del-list"><div class="dp2-list-empty">No deleted messages captured yet.<br>Open a channel and wait.</div></div>
        </div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-log">Clear</button>
          <button class="dp2-btn dp2-btn-green"     id="dp2-export-log">📥 Export JSON</button>
        </div>
      </div>

      <!-- ═══ GHOST ═══ -->
      <div class="dp2-pane" id="dp2-pane-ghost">
        <div class="dp2-card">
          <div class="dp2-lbl">Ghost Ping Tracker <span id="dp2-ghost-cnt" style="color:#c084fc"></span></div>
          <p style="font-size:11px;color:rgba(255,255,255,.3);font-weight:700;margin-bottom:8px">
            Detects @mentions deleted immediately — shows toast notification.
          </p>
          <div class="dp2-list" id="dp2-ghost-list"><div class="dp2-list-empty">No ghost pings detected.</div></div>
        </div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-ghost">Clear</button>
        </div>
      </div>

      <!-- ═══ EXPORT ═══ -->
      <div class="dp2-pane" id="dp2-pane-export">
        <div class="dp2-card">
          <div class="dp2-lbl">Export Channel Messages</div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Channel ID</label>
            <input class="dp2-input" id="dp2-exp-ch" placeholder="channel to export">
            <button class="dp2-inline-btn" id="dp2-exp-auto">AUTO</button>
          </div>
          <div class="dp2-row" style="margin-bottom:8px">
            <div class="dp2-field">
              <label>Format</label>
              <select class="dp2-select" id="dp2-exp-fmt">
                <option value="html">HTML (pretty)</option>
                <option value="json">JSON (raw)</option>
                <option value="txt">TXT (plain)</option>
              </select>
            </div>
            <div class="dp2-field">
              <label>Limit</label>
              <input class="dp2-input" id="dp2-exp-lim" type="number" value="500" min="1" max="10000">
            </div>
          </div>
          <div class="dp2-warn">⚠ Token must be set in the Delete tab first.</div>
        </div>
        <div class="dp2-log" id="dp2-exp-log"></div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-primary" id="dp2-exp-start">📦 Export</button>
        </div>
      </div>

      <!-- ═══ TOKEN ═══ -->
      <div class="dp2-pane" id="dp2-pane-token">
        <div class="dp2-card">
          <div class="dp2-lbl">Your Token</div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Token (hidden)</label>
            <input class="dp2-input" id="dp2-tok-disp" type="password" placeholder="not loaded" readonly>
          </div>
          <div class="dp2-btn-row" style="margin-bottom:8px">
            <button class="dp2-btn dp2-btn-secondary" id="dp2-tok-detect">🔍 Detect</button>
            <button class="dp2-btn dp2-btn-secondary" id="dp2-tok-copy">📋 Copy</button>
            <button class="dp2-btn dp2-btn-danger"    id="dp2-tok-clear">🗑 Clear</button>
          </div>
          <div class="dp2-warn">⚠ <strong>NEVER share your token.</strong> It gives full access to your account. If exposed — logout, change password, enable 2FA immediately.</div>
        </div>
        <div class="dp2-user-card" id="dp2-userinfo" style="color:rgba(255,255,255,.3);font-weight:700">
          Detect token to see account info.
        </div>
      </div>

      <!-- ═══ ABOUT ═══ -->
      <div class="dp2-pane" id="dp2-pane-about">
        <div class="dp2-about">
          <div class="dp2-about-logo">🛠️</div>
          <div class="dp2-about-title">Discord Plus+ V1</div>
          <div class="dp2-about-sub">v${VERSION} · The all-in-one Discord web toolkit</div>
          <ul class="dp2-feat-list">
            <li>Bulk Message Deleter — Discord API v10, smart rate-limit & retry</li>
            <li>Deleted Message Logger — real-time DOM capture, 500 msg buffer</li>
            <li>Ghost Ping Detector — auto-alert with toast notification</li>
            <li>Message Exporter — HTML / JSON / TXT download</li>
            <li>Token Detector — 3-strategy extraction + account info</li>
            <li>Draggable panel — tabbed UI, glassmorphism design</li>
            <li>Alt+D shortcut — toggle panel anytime</li>
            <li>100% local — zero external requests</li>
          </ul>
          <a class="dp2-community-btn" href="${COMMUNITY}" target="_blank" rel="noopener">
            💬 Join Community · discord.gg/Gvmd7deFtS
          </a>
          <div style="font-size:10px;color:rgba(255,255,255,.2);margin-top:10px;font-weight:700">
            ⚠ Self-bot actions may violate Discord TOS. Use at your own risk.<br>
            MIT License · Works with Tampermonkey & Violentmonkey
          </div>
        </div>
      </div>

    </div><!-- /body -->

    <!-- Footer -->
    <div class="dp2-foot">
      <div class="dp2-foot-txt">© Discord Plus+ v${VERSION} by 2pixel · Alt+D to toggle</div>
    </div>
  `;

  document.body.appendChild(panel);
  return { panel, tog };
}

// ─── WIRE EVENTS ──────────────────────────────────────────────────────────────
function wire(panel, tog) {
  // Drag
  const hdr = panel.querySelector('.dp2-hdr');
  let drag = false, ox = 0, oy = 0, cx, cy;
  const vw = innerWidth, vh = innerHeight;
  cx = vw - 390; cy = Math.max(8, vh - 620);
  panel.style.transform = `translate3d(${cx}px,${cy}px,0)`;

  hdr.addEventListener('pointerdown', e => {
    drag = true;
    panel.classList.add('dp2-dragging');
    const r = panel.getBoundingClientRect();
    ox = e.clientX - r.left; oy = e.clientY - r.top;
    try { hdr.setPointerCapture(e.pointerId); } catch(_){}
    e.preventDefault();
  }, { passive: false });
  hdr.addEventListener('pointermove', e => {
    if (!drag) return;
    e.preventDefault();
    const maxX = innerWidth - panel.offsetWidth - 8;
    const maxY = innerHeight - panel.offsetHeight - 8;
    cx = Math.max(8, Math.min(maxX, e.clientX - ox));
    cy = Math.max(8, Math.min(maxY, e.clientY - oy));
    panel.style.transform = `translate3d(${cx}px,${cy}px,0)`;
  }, { passive: false });
  hdr.addEventListener('pointerup',   () => { drag = false; panel.classList.remove('dp2-dragging'); });
  hdr.addEventListener('pointercancel', () => { drag = false; panel.classList.remove('dp2-dragging'); });

  // Toggle
  let vis = false;
  tog.addEventListener('click', () => {
    vis = !vis;
    panel.classList.toggle('dp2-show', vis);
    tog.classList.toggle('dp2-active', vis);
  });

  // Close / Minimize
  panel.querySelector('#dp2-close').onclick = () => { vis = false; panel.classList.remove('dp2-show'); tog.classList.remove('dp2-active'); };
  panel.querySelector('#dp2-min').onclick   = () => {
    const b = panel.querySelector('.dp2-body');
    const f = panel.querySelector('.dp2-foot');
    const p = panel.querySelector('.dp2-pills');
    const t = panel.querySelector('.dp2-tabs');
    const hidden = b.style.display === 'none';
    [b, f, p, t].forEach(el => { if(el) el.style.display = hidden ? '' : 'none'; });
  };

  // Tabs
  panel.querySelectorAll('.dp2-tab').forEach(tab => {
    tab.onclick = () => {
      panel.querySelectorAll('.dp2-tab').forEach(t=>t.classList.remove('active'));
      panel.querySelectorAll('.dp2-pane').forEach(p=>p.classList.remove('active'));
      tab.classList.add('active');
      panel.querySelector(`#dp2-pane-${tab.dataset.tab}`).classList.add('active');
    };
  });

  // Checkboxes
  panel.querySelectorAll('.dp2-chk').forEach(c => c.onclick = ()=>c.classList.toggle('on'));

  // Delay slider
  const slider = panel.querySelector('#dp2-delay');
  const slLbl  = panel.querySelector('#dp2-delay-lbl');
  slider.oninput = () => { slLbl.textContent = slider.value + 'ms'; };

  // ── Delete tab ──
  const tokenIn = panel.querySelector('#dp2-token');

  panel.querySelector('#dp2-auto-tok').onclick = () => {
    const t = grabToken();
    if (t) { tokenIn.value = t; S.token = t; appendLog('Token auto-detected ✓', 'ok'); }
    else   appendLog('Could not detect — paste manually', 'warn');
  };
  panel.querySelector('#dp2-auto-ch').onclick = () => {
    const ch = getChannelId();
    if (ch) { panel.querySelector('#dp2-channel').value = ch; appendLog(`Channel: ${ch}`, 'ok'); }
    else   appendLog('Navigate to a channel first', 'warn');
  };
  panel.querySelector('#dp2-auto-guild').onclick = () => {
    const g = getGuildId();
    if (g) { panel.querySelector('#dp2-guild').value = g; appendLog(`Guild: ${g}`, 'ok'); }
    else   appendLog('Not in a guild', 'warn');
  };
  panel.querySelector('#dp2-auto-me').onclick = async () => {
    const t = tokenIn.value.trim() || grabToken();
    if (!t) { appendLog('Set token first', 'warn'); return; }
    S.token = t;
    try {
      const me = await getMe();
      panel.querySelector('#dp2-author').value = me.id;
      appendLog(`Author: ${me.id} (${me.username})`, 'ok');
    } catch(e) { appendLog('Error: '+e.message, 'error'); }
  };

  panel.querySelector('#dp2-clear-btn').onclick = () => {
    if (S.delRunning) return;
    ['#dp2-token','#dp2-channel','#dp2-guild','#dp2-author','#dp2-content','#dp2-from','#dp2-to']
      .forEach(s => { panel.querySelector(s).value = ''; });
    panel.querySelectorAll('.dp2-chk').forEach(c=>c.classList.remove('on'));
    panel.querySelector('#dp2-log').innerHTML = '';
    setStatus('');
    S.delCount = S.failCount = S.scanCount = 0;
    uiSync();
  };

  panel.querySelector('#dp2-stop-btn').onclick = () => { S.delStopped = true; S.delRunning = false; };

  panel.querySelector('#dp2-start-btn').onclick = async () => {
    if (S.delRunning) return;
    const token = tokenIn.value.trim() || grabToken();
    const ch    = panel.querySelector('#dp2-channel').value.trim();
    if (!token) { appendLog('Token required', 'error'); return; }
    if (!ch)    { panel.querySelector('#dp2-channel').classList.add('error'); appendLog('Channel ID required', 'error'); return; }
    panel.querySelectorAll('.dp2-input').forEach(i=>i.classList.remove('error'));
    S.token = token;
    toggleBtn('start', false);
    toggleBtn('stop', true);
    await runDelete({
      channelId: ch,
      authorId:  panel.querySelector('#dp2-author').value.trim() || null,
      guildId:   panel.querySelector('#dp2-guild').value.trim()  || null,
      content:   panel.querySelector('#dp2-content').value.trim() || null,
      minDate:   panel.querySelector('#dp2-from').value ? new Date(panel.querySelector('#dp2-from').value+'T00:00:00') : null,
      maxDate:   panel.querySelector('#dp2-to').value   ? new Date(panel.querySelector('#dp2-to').value+'T23:59:59') : null,
      hasLink:   panel.querySelector('#chk-link').classList.contains('on'),
      hasFile:   panel.querySelector('#chk-file').classList.contains('on'),
      nsfw:      panel.querySelector('#chk-nsfw').classList.contains('on'),
      delayMs:   parseInt(slider.value),
    });
    toggleBtn('stop', false);
    toggleBtn('start', true);
  };

  // ── Logger tab ──
  panel.querySelector('#dp2-clear-log').onclick = () => { S.deletedLog = []; renderLogList(); };
  panel.querySelector('#dp2-export-log').onclick = () => {
    const b = new Blob([JSON.stringify(S.deletedLog, null, 2)], { type:'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(b);
    a.download = 'deleted-messages.json';
    a.click();
  };

  // ── Ghost tab ──
  panel.querySelector('#dp2-clear-ghost').onclick = () => { S.ghostPings = []; renderGhostList(); };

  // ── Export tab ──
  panel.querySelector('#dp2-exp-auto').onclick = () => {
    const ch = getChannelId();
    if (ch) panel.querySelector('#dp2-exp-ch').value = ch;
  };
  panel.querySelector('#dp2-exp-start').onclick = async () => {
    const t = tokenIn.value.trim() || grabToken();
    if (!t) { showToast('Error', 'Set token in Delete tab first', '#ed4245'); return; }
    S.token = t;
    const ch  = panel.querySelector('#dp2-exp-ch').value.trim();
    const fmt = panel.querySelector('#dp2-exp-fmt').value;
    const lim = parseInt(panel.querySelector('#dp2-exp-lim').value) || 500;
    if (!ch) return;
    await exportChannel(ch, fmt, lim);
  };

  // ── Token tab ──
  panel.querySelector('#dp2-tok-detect').onclick = async () => {
    const t = grabToken();
    if (!t) { showToast('Error', 'Could not detect token', '#ed4245'); return; }
    S.token = t;
    tokenIn.value = t;
    panel.querySelector('#dp2-tok-disp').value = t;
    try {
      const me = await getMe();
      panel.querySelector('#dp2-userinfo').innerHTML = `
        <div style="color:#fff;font-size:14px;font-weight:900;margin-bottom:6px">${esc(me.username)}<span style="color:rgba(255,255,255,.3)">#${me.discriminator||'0'}</span></div>
        <div style="color:rgba(255,255,255,.4);margin-bottom:2px">ID: <span style="color:#949cf7">${me.id}</span></div>
        <div style="color:rgba(255,255,255,.4);margin-bottom:2px">Email: <span style="color:#949cf7">${esc(me.email||'hidden')}</span></div>
        <div style="color:rgba(255,255,255,.4)">2FA: <span style="color:${me.mfa_enabled?'#57f287':'#ed4245'}">${me.mfa_enabled?'Enabled ✓':'Disabled ✗'}</span></div>
      `;
    } catch(e) {
      panel.querySelector('#dp2-userinfo').innerHTML = `<span style="color:#ed4245">${esc(e.message)}</span>`;
    }
  };
  panel.querySelector('#dp2-tok-copy').onclick = () => {
    const t = S.token || panel.querySelector('#dp2-tok-disp').value;
    if (t) navigator.clipboard.writeText(t).then(()=>showToast('Copied!','Token copied — keep it safe','#3ba55d'));
  };
  panel.querySelector('#dp2-tok-clear').onclick = () => {
    S.token = null;
    tokenIn.value = '';
    panel.querySelector('#dp2-tok-disp').value = '';
    panel.querySelector('#dp2-userinfo').textContent = 'Detect token to see account info.';
    panel.querySelector('#dp2-userinfo').style.color = 'rgba(255,255,255,.3)';
  };

  // Alt+D shortcut
  document.addEventListener('keydown', e => {
    if (e.altKey && e.key === 'D') {
      vis = !vis;
      panel.classList.toggle('dp2-show', vis);
      tog.classList.toggle('dp2-active', vis);
    }
  });

  // Auto-detect on load
  setTimeout(() => {
    const t = grabToken();
    if (t) { S.token = t; tokenIn.value = t; }
  }, 1500);
}

// ─── UI STATE HELPERS ─────────────────────────────────────────────────────────
function uiSync() {
  const set = (id, v) => { const el = document.getElementById(id); if(el) el.textContent = v; };
  set('dp2-s-del',  S.delCount);
  set('dp2-s-fail', S.failCount);
  set('dp2-s-scan', S.scanCount);
  set('dp2-v-del',  S.delCount);
  set('dp2-v-fail', S.failCount);
  set('dp2-v-scan', S.scanCount);
}

function appendLog(msg, type='info') {
  ['#dp2-log','#dp2-exp-log'].forEach(sel => {
    const el = document.querySelector(sel);
    if (!el) return;
    const t = new Date().toLocaleTimeString('en-GB',{hour12:false});
    const line = document.createElement('div');
    line.className = 'dp2-ll ' + type;
    line.textContent = `[${t}] ${msg}`;
    el.appendChild(line);
    el.scrollTop = el.scrollHeight;
    while (el.childElementCount > 300) el.removeChild(el.firstChild);
  });
}

function setStatus(state) {
  const dot = document.getElementById('dp2-sdot');
  const lbl = document.getElementById('dp2-status-lbl');
  const bar = document.getElementById('dp2-bar');
  if (dot) dot.className = 'dp2-status-dot' + (state ? ' '+state : '');
  if (lbl) {
    const labels = { running:'Running…', done:'Done ✓', '':'Idle' };
    lbl.innerHTML = `<span class="dp2-status-dot${state?' '+state:''}" id="dp2-sdot"></span>${labels[state]||'Idle'}`;
  }
  if (bar) {
    bar.classList.toggle('running', state==='running');
    if (state==='done') bar.style.width='100%';
    if (!state) bar.style.width='0';
  }
}

function toggleBtn(id, show) {
  const el = document.getElementById(id==='start' ? 'dp2-start-btn' : 'dp2-stop-btn');
  if (!el) return;
  if (id==='start') el.disabled = !show;
  el.style.display = show ? '' : 'none';
}

function renderLogList() {
  const list = document.getElementById('dp2-del-list');
  const cnt  = document.getElementById('dp2-log-cnt');
  if (!list) return;
  if (cnt) cnt.textContent = `(${S.deletedLog.length})`;
  if (!S.deletedLog.length) { list.innerHTML = '<div class="dp2-list-empty">No deleted messages captured yet.<br>Open a channel and wait.</div>'; return; }
  list.innerHTML = S.deletedLog.slice(0,100).map(e=>`
    <div class="dp2-li">
      <div class="dp2-li-head">
        <span class="dp2-li-author">${esc(e.author)}</span>
        <span class="dp2-li-time">${fmtTime(e.time)}</span>
      </div>
      <div class="dp2-li-body">${esc(e.content.slice(0,200))}${e.content.length>200?'…':''}</div>
    </div>`).join('');
}

function renderGhostList() {
  const list = document.getElementById('dp2-ghost-list');
  const cnt  = document.getElementById('dp2-ghost-cnt');
  if (!list) return;
  if (cnt) cnt.textContent = `(${S.ghostPings.length})`;
  if (!S.ghostPings.length) { list.innerHTML = '<div class="dp2-list-empty">No ghost pings detected.</div>'; return; }
  list.innerHTML = S.ghostPings.slice(0,50).map(e=>`
    <div class="dp2-li ghost">
      <div class="dp2-li-head">
        <span class="dp2-li-author">👻 ${esc(e.author)}</span>
        <span class="dp2-li-time">${fmtTime(e.time)}</span>
      </div>
      <div class="dp2-li-body">${esc(e.content.slice(0,200))}</div>
    </div>`).join('');
}

// ─── BOOT ─────────────────────────────────────────────────────────────────────
function boot() {
  if (document.getElementById(PANEL_ID)) return;
  const { panel, tog } = buildPanel();
  wire(panel, tog);
  setTimeout(() => startLogger(), 2000);
  console.log(`%cDiscord Plus+ v${VERSION} loaded 🛠️`, 'color:#949cf7;font-weight:bold;font-size:13px;background:#1e2028;padding:4px 10px;border-radius:8px');
  console.log(`%cCommunity: ${COMMUNITY}`, 'color:#5865F2;font-size:11px');
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 1200));
} else {
  setTimeout(boot, 1200);
}