Undiscord

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();