Undiscord

Highly efficient message deletion script. Now with customizable delay and batch sleep settings.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name            Undiscord
// @namespace       Forked from "https://github.com/victornpb/undiscord", https://github.com/BeBubbled
// @version         8.4.1
// @description     Highly efficient message deletion script. Now with customizable delay and batch sleep settings.
// @author          BeBubble
// @license         MIT
// @match           *://discord.com/*
// @match           *://*.discord.com/*
// @grant           none
// ==/UserScript==

(function () {
    'use strict';

    console.log("[Undiscord] Script loaded successfully. Initializing...");

    const css = `
        #undiscord-toggle-btn {
            display: flex; align-items: center; justify-content: center;
            background: transparent; color: var(--interactive-normal);
            border: none; padding: 0 8px; margin: 0; cursor: pointer;
            transition: color 0.2s ease; height: 24px;
        }
        #undiscord-toggle-btn:hover { color: var(--interactive-hover); }
        #undiscord-toggle-btn svg { width: 24px; height: 24px; }

        #undiscord-ui {
            display: none; position: fixed; z-index: 2147483647; top: 60px; right: 20px; width: 440px;
            background: var(--background-secondary, #2b2d31); color: var(--text-normal, #dbdee1);
            border: 1px solid var(--background-tertiary, #1e1f22); border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.4); font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
            flex-direction: column; overflow: hidden;
        }

        .u-header {
            padding: 16px; background: var(--background-tertiary, #1e1f22); display: flex;
            justify-content: space-between; align-items: center; border-bottom: 1px solid var(--background-modifier-accent, #111214);
            user-select: none;
        }
        .u-header b { font-size: 16px; color: var(--header-primary, #f2f3f5); }
        .u-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }

        .u-label { font-size: 12px; font-weight: 700; color: var(--header-secondary, #b5bac1); text-transform: uppercase; margin-bottom: -6px; }
        .u-label.warning { color: var(--text-danger, #fa777a); }

        .u-input {
            background: var(--input-background, #1e1f22); border: 1px solid transparent;
            color: var(--text-normal, #dbdee1); padding: 10px; border-radius: 4px; box-sizing: border-box; font-size: 14px;
            transition: border-color 0.2s; outline: none; width: 100%;
        }
        .u-input:focus { border-color: var(--text-link, #5865f2); }

        .u-log {
            background: var(--background-primary, #111214); height: 160px; overflow-y: auto;
            font-size: 12px; font-family: 'Consolas', 'Courier New', monospace; padding: 8px;
            border-radius: 4px; border: 1px solid var(--background-tertiary, #1e1f22); word-break: break-all; line-height: 1.4;
        }
        .u-log::-webkit-scrollbar { width: 8px; }
        .u-log::-webkit-scrollbar-track { background: transparent; }
        .u-log::-webkit-scrollbar-thumb { background: var(--scrollbar-auto-thumb, #313338); border-radius: 4px; }

        .u-btn {
            cursor: pointer; background: var(--brand-experiment, #5865f2); color: white; border: none;
            padding: 10px; border-radius: 4px; font-weight: 600; font-size: 14px; transition: background 0.2s;
        }
        .u-btn:hover:not(:disabled) { background: var(--brand-experiment-hover, #4752c4); }
        .u-btn:disabled { background: var(--background-modifier-accent, #4e5058); color: #80848e; cursor: not-allowed; }
        .u-btn-stop { background: var(--button-danger-background, #da373c); margin-top: 4px; }
        .u-btn-stop:hover:not(:disabled) { background: var(--button-danger-background-hover, #a1282c); }
        .u-btn-secondary { background: var(--button-secondary-background, #4e5058); font-size: 12px; padding: 8px;}
        .u-btn-secondary:hover:not(:disabled) { background: var(--button-secondary-background-hover, #6d6f78); }

        .flex-row { display: flex; gap: 8px; }
        .flex-row input { flex: 1; min-width: 0; }
        .flex-row button { background: var(--button-positive-background, #23a559); font-size: 12px; padding: 0 12px; white-space: nowrap; }
        .flex-row button:hover { background: var(--button-positive-background-hover, #1da24a); }

        /* 修复了这里:去掉了负边距,增加了正确的间距 */
        .delay-label { font-size: 11px; font-weight: bold; color: var(--header-secondary, #b5bac1); text-align: center; padding-bottom: 2px; text-transform: uppercase;}
    `;

    function initCore() {
        if (document.getElementById('undiscord-style')) return;

        const s = document.createElement('style');
        s.id = 'undiscord-style';
        s.textContent = css;
        document.head.appendChild(s);

        const container = document.createElement('div');
        container.id = 'undiscord-ui';
        container.innerHTML = `
            <div class="u-header">
                <b>Undiscord</b>
                <button id="close" style="background:none;color:var(--interactive-normal);border:none;cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;">×</button>
            </div>
            <div class="u-body">
                <label class="u-label warning">AUTH TOKEN (Plaintext - Do not share!)</label>
                <div class="flex-row">
                    <input id="token" class="u-input" type="text" placeholder="Leave empty or fetch ->">
                    <button id="get-token-btn" class="u-btn">Fetch Token</button>
                </div>

                <div class="flex-row" style="margin-top: 4px;">
                    <div style="flex:1; display:flex; flex-direction:column; gap:4px;">
                        <label class="u-label">GUILD ID (Or @me)</label>
                        <input id="guildId" class="u-input" placeholder="Server ID">
                    </div>
                    <div style="flex:1; display:flex; flex-direction:column; gap:4px;">
                        <label class="u-label">CHANNEL ID</label>
                        <input id="channelId" class="u-input" placeholder="Channel ID">
                    </div>
                </div>

                <label class="u-label">AUTHOR ID</label>
                <input id="authorId" class="u-input" placeholder="Your User ID">

                <label class="u-label" style="margin-top: 4px; color: #00b0f4;">ADVANCED DELAYS</label>
                <div class="flex-row">
                    <div style="flex:1; display:flex; flex-direction:column; gap:2px;">
                        <span class="delay-label">Delay (ms)</span>
                        <input id="deleteDelay" class="u-input" type="number" value="1500" title="Delay between each deletion in ms">
                    </div>
                    <div style="flex:1; display:flex; flex-direction:column; gap:2px;">
                        <span class="delay-label">Sleep After</span>
                        <input id="batchSize" class="u-input" type="number" value="50" title="Amount of messages to delete before pausing">
                    </div>
                    <div style="flex:1; display:flex; flex-direction:column; gap:2px;">
                        <span class="delay-label">Sleep Time (ms)</span>
                        <input id="batchDelay" class="u-input" type="number" value="5000" title="How long to pause in ms">
                    </div>
                </div>

                <button id="getInfo" class="u-btn u-btn-secondary">Auto-fill Current Server/DM & Channel IDs</button>
                <button id="start" class="u-btn">Start (Search & Destroy)</button>
                <button id="stop" class="u-btn u-btn-stop" disabled>Stop / Pause</button>
                <div id="log" class="u-log"></div>
            </div>
        `;
        document.body.appendChild(container);

        const log = (m, err=false, info=false) => {
            const d = document.createElement('div');
            d.style.color = err ? 'var(--text-danger, #fa777a)' : (info ? '#00b0f4' : 'var(--text-muted, #b5bac1)');
            d.textContent = `[${new Date().toLocaleTimeString()}] ${m}`;
            const lb = container.querySelector('#log');
            lb.appendChild(d);
            lb.scrollTop = lb.scrollHeight;
        };

        const getModernToken = () => {
            let extractedToken = "";
            try {
                window.webpackChunkdiscord_app.push([
                    [Symbol('undiscord')], {},
                    (req) => {
                        for (const key in req.c) {
                            const module = req.c[key].exports;
                            if (!module) continue;
                            if (module.default && typeof module.default.getToken === 'function') {
                                const t = module.default.getToken();
                                if (typeof t === 'string' && t.length > 20) { extractedToken = t; return; }
                            }
                            if (typeof module.getToken === 'function') {
                                const t = module.getToken();
                                if (typeof t === 'string' && t.length > 20) { extractedToken = t; return; }
                            }
                        }
                    }
                ]);
            } catch(e) {}
            if (extractedToken) return extractedToken;

            try {
                const iframe = document.createElement('iframe');
                iframe.style.display = 'none';
                document.body.appendChild(iframe);
                const t = iframe.contentWindow.localStorage.getItem('token');
                document.body.removeChild(iframe);
                if (typeof t === 'string' && t.length > 20) { return t.replace(/^"|"$/g, ''); }
            } catch(e) {}
            return "";
        };

        container.querySelector('#close').onclick = () => { container.style.display = 'none'; };

        container.querySelector('#get-token-btn').onclick = () => {
            log("Attempting to extract Token...");
            const token = getModernToken();
            if (token) {
                container.querySelector('#token').value = token;
                log("✅ Token successfully fetched and filled!");
            } else {
                log("❌ Auto-fetch failed. Grab manually via F12 -> Network.", true);
            }
        };

        container.querySelector('#getInfo').onclick = () => {
            const m = location.href.match(/channels\/([\w@]+)\/(\d+)/);
            if (m) {
                container.querySelector('#guildId').value = m[1];
                container.querySelector('#channelId').value = m[2];
                log("✅ Current location IDs filled.");
            } else {
                log("❌ Failed to parse IDs from URL. Ensure you are viewing a channel or DM.", true);
            }
        };

        let running = false;
        container.querySelector('#stop').onclick = () => { running = false; log("Stopping sequence initiated...", true); };

        container.querySelector('#start').onclick = async () => {
            let tokenInput = container.querySelector('#token').value.trim().replace(/^"|"$/g, '');
            if (!tokenInput) tokenInput = getModernToken();

            const guildId = container.querySelector('#guildId').value.trim();
            const channelId = container.querySelector('#channelId').value.trim();
            let authorId = container.querySelector('#authorId').value.trim();

            // User Parameters
            const deleteDelay = parseInt(container.querySelector('#deleteDelay').value, 10) || 1500;
            const batchSize = parseInt(container.querySelector('#batchSize').value, 10) || 0;
            const batchDelay = parseInt(container.querySelector('#batchDelay').value, 10) || 5000;

            if (!tokenInput || !guildId) return log("❌ Error: Missing Token or Server (Guild) ID.", true);
            container.querySelector('#token').value = tokenInput;

            if (!authorId) {
                try {
                    const meRes = await fetch('https://discord.com/api/v9/users/@me', { headers: { 'Authorization': tokenInput } });
                    if (meRes.status === 401) return log("❌ Error: Token validation failed (401).", true);
                    const me = await meRes.json();
                    if (!me.id) throw new Error("Invalid Token Response");
                    authorId = me.id;
                    container.querySelector('#authorId').value = authorId;
                } catch (e) {
                    return log("❌ Error: Network issue or invalid token.", true);
                }
            }

            running = true;
            container.querySelector('#start').disabled = true;
            container.querySelector('#stop').disabled = false;

            const isHistoryMode = (guildId === '@me');
            if (isHistoryMode) {
                log("🚀 DMs detected. Using [History API] for zero-residue wipe...", false, true);
            } else {
                log("🚀 Server detected. Using [Search API] for deep scanning...", false, true);
            }

            let offset = 0;
            let historyLastId = null;
            let messagesDeletedSinceLastSleep = 0;

            while (running) {
                try {
                    let apiUrl = "";

                    if (isHistoryMode) {
                        if (!channelId) { log("❌ Error: Channel ID is required for DMs.", true); break; }
                        apiUrl = `https://discord.com/api/v9/channels/${channelId}/messages?limit=100`;
                        if (historyLastId) apiUrl += `&before=${historyLastId}`;
                    } else {
                        apiUrl = `https://discord.com/api/v9/guilds/${guildId}/messages/search?author_id=${authorId}&offset=${offset}`;
                        if (channelId) apiUrl += `&channel_id=${channelId}`;
                    }

                    const res = await fetch(apiUrl, { headers: { 'Authorization': tokenInput } });

                    if (res.status === 202) {
                        const data = await res.json();
                        log(`⏳ Indexing... Waiting ${data.retry_after}s...`, true);
                        await new Promise(r => setTimeout(r, data.retry_after * 1000));
                        continue;
                    }
                    if (res.status === 429) {
                        const wait = (await res.json()).retry_after || 5;
                        log(`⚠️ Global Rate limit. Waiting ${wait}s...`, true);
                        await new Promise(r => setTimeout(r, wait * 1000));
                        continue;
                    }
                    if (res.status === 401 || res.status === 403) {
                        log(`❌ Error: API Access Denied (${res.status}).`, true);
                        break;
                    }

                    const data = await res.json();
                    let messagesToDelete = [];

                    if (isHistoryMode) {
                        if (!Array.isArray(data) || data.length === 0) {
                            log("✅ Reached the beginning of the chat history. Cleanup complete!", false, true);
                            break;
                        }
                        historyLastId = data[data.length - 1].id;
                        messagesToDelete = data.filter(m => m.author && m.author.id === authorId);

                        if (messagesToDelete.length === 0) {
                            log(`🔎 Scrolling up...`);
                            await new Promise(r => setTimeout(r, 1000));
                            continue;
                        }
                    } else {
                        if(!data || typeof data.total_results === 'undefined' || data.total_results === 0) {
                             log("✅ No matching messages found. Cleanup complete!", false, true);
                             break;
                        }
                        messagesToDelete = data.messages ? data.messages.map(group => group.find(m => m.hit === true)).filter(m => m) : [];
                        if (messagesToDelete.length === 0) {
                            log("✅ No more matching messages found. Cleanup complete!", false, true);
                            break;
                        }
                    }

                    for (const msg of messagesToDelete) {
                        if (!running) break;

                        const delRes = await fetch(`https://discord.com/api/v9/channels/${msg.channel_id}/messages/${msg.id}`, {
                            method: 'DELETE',
                            headers: { 'Authorization': tokenInput }
                        });

                        if (delRes.status === 204) {
                            log(`🗑️ Deleted: ${msg.content.slice(0, 25)}...`);
                            messagesDeletedSinceLastSleep++;
                        } else if (delRes.status === 429) {
                            const wait = (await delRes.json()).retry_after || 2;
                            log(`⚠️ Deleting too fast. Waiting ${wait}s...`, true);
                            await new Promise(r => setTimeout(r, wait * 1000));
                            continue;
                        } else if (delRes.status === 401 || delRes.status === 403) {
                            log(`❌ Missing perms to delete msg (ID: ${msg.id})`, true);
                        }

                        if (batchSize > 0 && messagesDeletedSinceLastSleep >= batchSize) {
                            log(`💤 Batch limit reached (${batchSize}). Sleeping for ${batchDelay}ms...`, false, true);
                            await new Promise(r => setTimeout(r, batchDelay));
                            messagesDeletedSinceLastSleep = 0;
                        } else {
                            await new Promise(r => setTimeout(r, deleteDelay));
                        }
                    }

                    await new Promise(r => setTimeout(r, 1500));

                } catch (e) {
                    log("❌ Error occurred: " + e, true);
                    break;
                }
            }

            running = false;
            if(container.querySelector('#start')) {
                container.querySelector('#start').disabled = false;
                container.querySelector('#stop').disabled = true;
            }
            log("🛑 Process stopped.");
        };
    }

    function injectToolbarButton() {
        const toolbar = document.querySelector('[class*="toolbar_"]');

        if (toolbar && !document.getElementById('undiscord-toggle-btn')) {
            const toggleBtn = document.createElement('button');
            toggleBtn.id = 'undiscord-toggle-btn';
            toggleBtn.title = 'Undiscord (Bulk Delete)';
            toggleBtn.setAttribute('aria-label', 'Undiscord');

            toggleBtn.innerHTML = `
                <svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
                    <path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
                    <path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
                </svg>
            `;

            toggleBtn.onclick = () => {
                const ui = document.getElementById('undiscord-ui');
                if (ui) ui.style.display = (ui.style.display === 'flex') ? 'none' : 'flex';
            };

            toolbar.prepend(toggleBtn);
        }
    }

    setTimeout(() => {
        initCore();
        injectToolbarButton();
    }, 2000);

    setInterval(() => {
        injectToolbarButton();
    }, 1500);

})();