Discord Message Cleaner (Sélection améliorée)

Supprime vos messages dans un channel Discord avec sélection AVANT/APRÈS un message et effets visuels !

// ==UserScript==
// @name         Discord Message Cleaner (Sélection améliorée)
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Supprime vos messages dans un channel Discord avec sélection AVANT/APRÈS un message et effets visuels !
// @author       ErrorNoName_ / Meliodas
// @match        https://discord.com/channels/*
// @license MIT
// @grant        none
// ==/UserScript==
(function () {
    'use strict';

    let storage = (typeof localStorage !== "undefined") ? localStorage : null;
    let settings = {
        token: storage ? storage.getItem("token") || "" : "",
        userId: "",
        channelId: "",
        deleteInterval: 1000
    };

    // --- CSS pour surlignage et main ---
    let style = document.createElement("style");
    style.id = "dcCleaner-style";
    style.innerHTML = `
        .dcCleaner-hover { background: #3ba55d33 !important; transition: .15s }
        .dcCleaner-selected { background: #5865F299 !important; position: relative !important;}
        .dcCleaner-mainmark { pointer-events:none; position: absolute; left: 6px; top: 6px; font-size: 22px; z-index: 3; }
        .dcCleaner-erasing { opacity: 0.7; transition: opacity .15s; }
    `;
    document.head.appendChild(style);

    function log(msg) {
        const la = document.getElementById("dcCleaner-log");
        if (la) {
            const p = document.createElement("div");
            p.style.fontSize = "12px";
            p.textContent = `[${(new Date()).toLocaleTimeString()}] ${msg}`;
            la.appendChild(p);
            la.scrollTop = la.scrollHeight;
        }
        console.log("[Cleaner]", msg);
    }
    function clearLog() {
        const la = document.getElementById("dcCleaner-log");
        if (la) la.innerHTML = "";
    }

    function getTokenUndiscord() {
        try {
            const iframe = document.createElement("iframe");
            iframe.style.display = "none";
            document.body.appendChild(iframe);
            const LS = iframe.contentWindow.localStorage;
            const t = JSON.parse(LS.token);
            document.body.removeChild(iframe);
            return t;
        } catch (err) { return ""; }
    }

    async function autoFillUserId() {
        const t = document.getElementById("dcCleaner-token").value.trim();
        if (!t) { log("Veuillez renseigner votre token d'abord."); return; }
        try {
            const res = await fetch("https://discord.com/api/v9/users/@me", { headers: { Authorization: t } });
            if (!res.ok) throw new Error("Erreur d'API.");
            const data = await res.json();
            document.getElementById("dcCleaner-userId").value = data.id;
            log("User ID rempli : " + data.id);
        } catch (e) {
            log("Erreur récupération userId : " + e);
        }
    }

    function getChannelIdFromURL() {
        const m = window.location.href.match(/channels\/[\w@]+\/(\d+)/);
        if (m) return m[1];
        alert('Channel ID non trouvé. Vérifiez l’URL.');
        return "";
    }

    // === Système de sélection amélioré ===
    let selectType = null; // 'before' ou 'after'
    let lastMark = null;
    let activeMsgElem = null;

    function enableMessageSelect(type) {
        selectType = type; // "before" ou "after"
        overlay.style.display = "none";
        // Clean ancienne sélection
        clearAllSelectionVisuals();

        function onMouseOver(e) {
            const msgElem = e.target.closest("[id^='chat-messages-']");
            if (msgElem) msgElem.classList.add("dcCleaner-hover");
        }
        function onMouseOut(e) {
            const msgElem = e.target.closest("[id^='chat-messages-']");
            if (msgElem) msgElem.classList.remove("dcCleaner-hover");
        }
        function onClick(e) {
            const msgElem = e.target.closest("[id^='chat-messages-']");
            if (msgElem) {
                const msgId = msgElem.id.split("-").pop();

                // Pour éviter de marquer plusieurs fois
                clearAllSelectionVisuals();

                msgElem.classList.add("dcCleaner-selected");

                // Ajoute la main gauche
                let mark = document.createElement("span");
                mark.innerHTML = "👆";
                mark.className = "dcCleaner-mainmark";
                msgElem.prepend(mark);
                lastMark = mark;
                activeMsgElem = msgElem;

                if (selectType === "before") {
                    document.getElementById("dcCleaner-startMsgId").value = msgId;
                } else {
                    document.getElementById("dcCleaner-endMsgId").value = msgId;
                }

                // Retire les events
                document.removeEventListener("click", onClick, true);
                document.removeEventListener("mouseover", onMouseOver, true);
                document.removeEventListener("mouseout", onMouseOut, true);

                setTimeout(() => overlay.style.display = "flex", 100); // Rouvre l'UI

                log(`Message ${selectType === "before" ? "AVANT" : "APRÈS"} sélectionné : ${msgId}`);
                e.stopPropagation();
                e.preventDefault();
            }
        }
        document.addEventListener("mouseover", onMouseOver, true);
        document.addEventListener("mouseout", onMouseOut, true);
        document.addEventListener("click", onClick, true);

        // Affichage explication visuelle temporaire (une bulle ?)
        log("Survolez puis cliquez sur un message.");
    }
    function clearAllSelectionVisuals() {
        document.querySelectorAll(".dcCleaner-selected").forEach(node => node.classList.remove("dcCleaner-selected"));
        document.querySelectorAll(".dcCleaner-mainmark").forEach(node => node.remove());
    }

    // := UI =:
    const overlay = document.createElement("div");
    overlay.id = "dcCleaner-overlay";
    overlay.style = "position:fixed;top:0;left:0;width:100vw;height:100vh;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.9);z-index:11000;";

    const panel = document.createElement("div");
    panel.style = [
        "background:#36393f", "padding:20px", "border-radius:10px",
        "max-width:400px", "min-width:260px", "color:#fff",
        "box-shadow:0 0 17px #000b", "font-family:Segoe UI,sans-serif", "font-size:15px"
    ].join(';');
    overlay.appendChild(panel);

    panel.innerHTML += `<h3 style="margin:0 0 7px 0;">Discord Cleaner <span style="font-size:12px;color:#43b581">(API recherche avancé)</span></h3>
      <div style="font-size:13px;margin-bottom:8px;color:#fff;font-weight:400;line-height:1.2;">
        ⚠️ <b>Risque de sanction Discord.</b> Utilisez à vos risques.<br>
        <span style="color:#aaa">Requiert un token utilisateur & votre UserID Discord.<br>Respectez les limites !</span>
      </div>`;

    function createField(label, id, opts = {}) {
        const d = document.createElement("div");
        d.style.marginBottom = "11px";
        const l = document.createElement("label");
        l.innerText = label + " ";
        l.style.fontSize = "13px";
        l.htmlFor = id;
        d.appendChild(l);
        const inp = document.createElement("input");
        inp.type = opts.type || "text";
        inp.id = id;
        inp.value = opts.def || "";
        inp.autocomplete = "off";
        inp.style = "width:97%;padding:5px 6px;margin-top:2px;border:none;border-radius:4px;font-size:13px;background:#202225;color:#fff;";
        d.appendChild(inp);
        if (opts.btn) {
            const b = document.createElement("button");
            b.innerText = opts.btn.txt;
            b.style = "margin-left:6px;padding:2px 9px;font-size:12px;border:none;border-radius:5px;background:#5865F2;color:#fff;cursor:pointer;";
            b.onclick = opts.btn.action;
            d.appendChild(b);
        }
        return d;
    }

    panel.appendChild(createField("Token:", "dcCleaner-token", {
        def: settings.token,
        btn: { txt: "Auto", action: () => {
                let tk = getTokenUndiscord();
                if (tk) { document.getElementById("dcCleaner-token").value = tk; log("Token récupéré !"); }
                else { log("Impossible de récupérer le token automatiquement."); }
            }
        }
    }));
    panel.appendChild(createField("User ID:", "dcCleaner-userId", {
        btn: { txt: "Me", action: autoFillUserId }
    }));
    panel.appendChild(createField("Channel ID:", "dcCleaner-channelId", {
        btn: { txt: "Auto", action: () => {
                let cid = getChannelIdFromURL();
                document.getElementById("dcCleaner-channelId").value = cid;
                log("Channel ID rempli : " + cid);
            }
        }
    }));
    panel.appendChild(createField("Message ID de départ (supprimer avant/égal):", "dcCleaner-startMsgId", {
        btn: { txt: "📌 Choisir", action: () => { enableMessageSelect("before"); }}
    }));
    panel.appendChild(createField("Message ID de fin (supprimer après/égal):", "dcCleaner-endMsgId", {
        btn: { txt: "📌 Choisir", action: () => { enableMessageSelect("after"); }}
    }));

    let intervalDiv = document.createElement("div");
    intervalDiv.style = "margin-bottom:11px;";
    intervalDiv.innerHTML = `<label style="font-size:13px;">Intervalle (ms): </label>
      <input type="range" id="dcCleaner-interval" min="300" max="5000" value="${settings.deleteInterval}" style="width:55%;vertical-align:middle;margin-right:8px">
      <span id="dcCleaner-interval-value" style="font-size:12px;">${settings.deleteInterval} ms</span>`;
    intervalDiv.querySelector("#dcCleaner-interval").addEventListener("input", function () {
        document.getElementById("dcCleaner-interval-value").innerText = this.value + " ms";
    });
    panel.appendChild(intervalDiv);

    const logDiv = document.createElement("div");
    logDiv.id = "dcCleaner-log";
    logDiv.style = "width:100%;height:110px;overflow-y:auto;background:#23272a;border-radius:4px;padding:5px 7px 4px 7px;font-size:12px;color:#fff;margin-bottom:13px;margin-top:4px;box-sizing:border-box;";
    panel.appendChild(logDiv);

    const btnDiv = document.createElement("div");
    btnDiv.style = "text-align:right;margin-top:9px;";
    const delBtn = document.createElement("button");
    delBtn.innerHTML = "🗑️ Supprimer mes messages";
    delBtn.style = "margin-right:11px;padding:7px 13px;border:none;border-radius:5px;background:linear-gradient(135deg,#43b581,#1abc9c);color:#fff;cursor:pointer;font-size:14px;font-weight:600;";
    const cancelBtn = document.createElement("button");
    cancelBtn.innerText = "Annuler";
    cancelBtn.style = "padding:7px 13px;border:none;border-radius:5px;background:linear-gradient(135deg,#7289da,#5b6eae);color:#fff;font-size:14px;";
    btnDiv.appendChild(delBtn);
    btnDiv.appendChild(cancelBtn);
    panel.appendChild(btnDiv);

    cancelBtn.onclick = () => { overlay.style.display = "none"; };
    document.body.appendChild(overlay);

    if (!document.getElementById("dcCleaner-mainBtn")) {
        const mainBtn = document.createElement("button");
        mainBtn.id = "dcCleaner-mainBtn";
        mainBtn.title = "Ouvrir Discord Cleaner";
        mainBtn.style = [
            "position:fixed", "bottom:20px", "right:20px", "z-index:99999", "padding:7px",
            "font-size:17px", "border:none", "border-radius:50%", "cursor:pointer", "background:transparent"
        ].join(';');
        mainBtn.innerHTML = `<img src="https://victornpb.github.io/undiscord/images/icon128.png" width="26" height="26" alt="Cleaner">`;
        mainBtn.onclick = function () {
            document.getElementById("dcCleaner-token").value = settings.token;
            document.getElementById("dcCleaner-userId").value = settings.userId;
            document.getElementById("dcCleaner-channelId").value = getChannelIdFromURL() || settings.channelId;
            document.getElementById("dcCleaner-interval").value = settings.deleteInterval;
            document.getElementById("dcCleaner-interval-value").innerText = settings.deleteInterval + " ms";
            clearLog();
            overlay.style.display = "flex";
        };
        document.body.appendChild(mainBtn);
    }

    // ===== Début suppression =====
    async function startDeletionProcess() {
        settings.token = document.getElementById("dcCleaner-token").value.trim();
        settings.userId = document.getElementById("dcCleaner-userId").value.trim();
        settings.channelId = document.getElementById("dcCleaner-channelId").value.trim();
        settings.deleteInterval = +document.getElementById("dcCleaner-interval").value;
        const startMsgId = document.getElementById("dcCleaner-startMsgId").value.trim(); // avant ou égal
        const endMsgId   = document.getElementById("dcCleaner-endMsgId").value.trim();   // après ou égal

        if (storage && settings.token) storage.setItem("token", settings.token);
        if (!settings.token || !settings.userId || !settings.channelId) {
            log("Paramètres manquants !");
            return;
        }
        if (!confirm("⚠️ Supprimer tous vos messages (dans ce channel, selon votre sélection) ?")) return;
        log("Démarrage de la suppression...");
        clearAllSelectionVisuals();

        let isDM = window.location.pathname.split('/')[2] === "@me";
        const API_SEARCH_URL = isDM ?
            `https://discord.com/api/v9/channels/${settings.channelId}/messages/search?` :
            `https://discord.com/api/v9/guilds/${window.location.pathname.split('/')[2]}/messages/search?channel_id=${settings.channelId}&`;

        let offset = 0;
        let delCount = 0;
        let totalFound = 0;
        const delay = ms => new Promise(r => setTimeout(r, ms));
        while (true) {
            let searchParams = new URLSearchParams({
                author_id: settings.userId,
                offset: offset,
                sort_by: 'timestamp',
                sort_order: 'desc',
                include_nsfw: 'true'
            });
            let searchUrl = API_SEARCH_URL + searchParams.toString();
            log("Recherche offset=" + offset + "...");
            let resp;
            try {
                resp = await fetch(searchUrl, { headers: { 'Authorization': settings.token } });
            } catch (e) {
                log("Erreur de requête lors de la recherche : " + e);
                break;
            }
            if (!resp.ok) {
                if (resp.status === 429) {
                    let j = await resp.json().catch(() => ({}));
                    let wait = j.retry_after ? Math.ceil(j.retry_after * 1000) : 5000;
                    log("Rate limit. Attente " + wait + " ms.");
                    await delay(wait);
                    continue;
                } else {
                    const txt = await resp.text().catch(() => "");
                    log(`Erreur de recherche (status=${resp.status}) : ${txt}`);
                    break;
                }
            }
            let data;
            try { data = await resp.json(); }
            catch (e) { log("Erreur parsing JSON : " + e); break; }

            if (!data || !data.messages || data.messages.length === 0) {
                log("Aucun (plus) de message à supprimer !");
                break;
            }
            if (data.total_results > totalFound) {
                totalFound = data.total_results;
                log("Messages trouvés (approx.) : " + totalFound);
            }

            // Filtrage selon les bornes
            let discoveredMessages = data.messages.map(thread => thread.find(m => m.hit)).filter(Boolean);
            if (startMsgId) discoveredMessages = discoveredMessages.filter(m => BigInt(m.id) <= BigInt(startMsgId));
            if (endMsgId)   discoveredMessages = discoveredMessages.filter(m => BigInt(m.id) >= BigInt(endMsgId));
            if (discoveredMessages.length === 0) {
                log("Aucun résultat exploitable ici => offset+=25");
                offset += 25;
                await delay(settings.deleteInterval); continue;
            }

            for (let msg of discoveredMessages) {
                try {
                    const delUrl = `https://discord.com/api/v9/channels/${settings.channelId}/messages/${msg.id}`;
                    let res = await fetch(delUrl, {
                        method: 'DELETE',
                        headers: {
                            'Authorization': settings.token,
                            'Content-Type': 'application/json'
                        }
                    });
                    if (res.ok) {
                        delCount++;
                        log(`Supprimé : ${msg.id} (Total = ${delCount})`);
                        // Effet LETTRE PAR LETTRE
                        let msgEl = document.querySelector(`[id$="-${msg.id}"]`);
                        if (msgEl) {
                            let msgContent = msgEl.querySelector('[data-slate-string="true"], [class*="markup"]');
                            if (msgContent) {
                                let txt = msgContent.textContent;
                                let i = 0;
                                msgEl.classList.add("dcCleaner-erasing");
                                let interval = setInterval(()=>{
                                    msgContent.textContent = txt.slice(0, txt.length-i++);
                                    if (i > txt.length) {
                                        clearInterval(interval);
                                        msgEl.style.opacity = '0.15';
                                    }
                                }, 24);
                            }
                        }
                    } else {
                        const txt = await res.text().catch(() => "");
                        log(`Erreur DELETE (status=${res.status}) sur ${msg.id} : ${txt}`);
                    }
                    await delay(settings.deleteInterval);
                } catch (e) {
                    log(`Erreur API : ${e}`);
                }
            }
            offset += 25;
            await delay(settings.deleteInterval);
        }
        log(`Fin. ${delCount} messages supprimés.`);
        alert("Terminé. " + delCount + " messages supprimés.");
    }

    delBtn.onclick = startDeletionProcess;
    log("Discord Cleaner initialisé.");
})();