Disappear

Delete all your messages in specified Discord DMs, groups, and servers.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Disappear
// @namespace    https://github.com/vil/disappear
// @version      1.0
// @description  Delete all your messages in specified Discord DMs, groups, and servers.
// @author       Vili (https://vili.dev)
// @match        https://discord.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // Helper to parse header string to Headers object
    function createHeadersFromString(headerStr) {
        const headers = new Headers();
        if (headerStr) {
            const headerPairs = headerStr.split('\r\n');
            headerPairs.forEach(headerPair => {
                const P = headerPair.indexOf(':');
                if (P > 0) {
                    const key = headerPair.substring(0, P).trim();
                    const value = headerPair.substring(P + 1).trim();
                    if (key && value) { // Ensure key and value are not empty
                        try {
                            headers.append(key, value);
                        } catch (e) {
                            console.warn(`[Disappear] Could not append header: ${key}: ${value}`, e);
                            // Some headers might be problematic for the Headers object if they are not valid
                        }
                    }
                }
            });
        }
        return headers;
    }

    // --- Configuration ---
    const SCRIPT_PREFIX = 'disappear'; // Used for CSS classes and local storage
    const API_BASE_URL = 'https://discord.com/api/v9';
    const MESSAGE_FETCH_LIMIT = 100; // Max messages to fetch per request (Discord limit)
    let MIN_DELAY_MS = 1000; // Minimum delay between delete operations
    let MAX_DELAY_MS = 3000; // Maximum delay
    const MAX_MESSAGE_DELETE_ATTEMPTS = 3; // Max attempts to delete a single message
    const MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES = 3; // Max consecutive full fetches with no user messages before stopping channel scan

    // --- State ---
    let authToken = '';
    let currentUserId = null;
    let isDeleting = false; // To control the deletion loop
    let isExporting = false; // To control the export loop

    // --- UI Elements ---
    let controlButton;
    let modalContainer;
    let statusDiv;

    // --- Helper Function for Control Button State ---
    function updateControlButtonIndicator(isWorking) {
        if (!controlButton) return;
        const workingClassName = `${SCRIPT_PREFIX}-working-indicator`;
        if (isWorking) {
            controlButton.innerHTML = '🗑️ Disappear (Working...)';
            controlButton.classList.add(workingClassName);
        } else {
            controlButton.innerHTML = '🗑️ Disappear';
            controlButton.classList.remove(workingClassName);
        }
    }

    // --- API Interaction ---
    function discordApiRequest(method, endpoint, data) {
        return new Promise((resolve, reject) => {
            if (!authToken) {
                updateStatus('<span style="color: red;">Auth Token is not set!</span>', true);
                return reject({ message: 'Auth Token is not set!', status: 0, responseText: null });
            }
            GM_xmlhttpRequest({
                method: method,
                url: `${API_BASE_URL}${endpoint}`,
                headers: {
                    "Authorization": authToken,
                    "Content-Type": "application/json"
                },
                data: data ? JSON.stringify(data) : null,
                onload: function(response) {
                    const responseHeaders = createHeadersFromString(response.responseHeaders);
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            resolve({ data: JSON.parse(response.responseText), headers: responseHeaders });
                        } catch (e) { // Handle cases where response might not be JSON (e.g., 204 No Content for DELETE)
                            resolve({ data: null, headers: responseHeaders });
                        }
                    } else if (response.status === 401) {
                        updateStatus('<span style="color: red;">Invalid Auth Token! Please check and re-enter.</span>', true);
                        reject({
                            message: `API Error ${response.status}: Unauthorized. Invalid token?`,
                            status: response.status,
                            responseText: response.responseText,
                            headers: responseHeaders
                        });
                    } else if (response.status === 429) { // Rate limit
                        updateStatus('<span style="color: orange;">Rate limited by Discord.</span>', true);
                        reject({
                            message: 'Rate limited by Discord API.',
                            status: response.status,
                            headers: responseHeaders,
                            responseText: response.responseText
                        });
                    }
                    else { // Other errors (e.g. 403, 404, 500)
                        updateStatus(`<span style="color: red;">API Error ${response.status}. Check console.</span>`, true);
                        console.error('[Disappear] API Error Response:', response);
                        reject({
                            message: `API Error ${response.status}: ${response.statusText}`,
                            status: response.status,
                            responseText: response.responseText,
                            headers: responseHeaders // Also pass headers for other errors if available
                        });
                    }
                },
                onerror: function(error) {
                    updateStatus('<span style="color: red;">Network error. Check console.</span>', true);
                    console.error('[Disappear] Network Error:', error);
                    reject({
                        message: 'Network error during API request.',
                        status: 0, // Or some other indicator for network error
                        responseText: null,
                        networkError: true,
                        headers: new Headers() // Provide empty headers for network errors
                    });
                }
            });
        });
    }

    async function fetchAuthenticatedUser() {
        if (currentUserId) return currentUserId;
        try {
            updateStatus('Fetching user ID...');
            const { data } = await discordApiRequest('GET', '/users/@me');
            if (data && data.id) {
                currentUserId = data.id;
                updateStatus('User ID fetched successfully.');
                console.log('[Disappear] Current User ID:', currentUserId);
                return currentUserId;
            } else {
                throw new Error('Could not retrieve user ID.');
            }
        } catch (error) {
            console.error('[Disappear] Error fetching user ID:', error);
            updateStatus('<span style="color: red;">Failed to fetch User ID. Token might be invalid.</span>', true);
            throw error;
        }
    }


    async function fetchChannels() {
        updateStatus('Fetching channels...');
        console.log('[Disappear]', 'Fetching channels...');
        try {
            // Fetch DMs (includes user DMs and group DMs)
            const { data: dmChannels } = await discordApiRequest('GET', '/users/@me/channels');
            const channels = dmChannels.map(ch => ({
                id: ch.id,
                name: ch.recipients && ch.recipients.length > 0 ? ch.recipients.map(r => r.username).join(', ') : (ch.name || 'Unnamed Group DM'),
                type: ch.type === 1 ? 'DM' : (ch.type === 3 ? 'Group DM' : 'Unknown DM Type') // 1: DM, 3: Group DM
            }));

            // Fetch Guilds (servers)
            const { data: guilds } = await discordApiRequest('GET', '/users/@me/guilds');
            for (const guild of guilds) {
                // For each guild, fetch its channels
                // Note: This can be a lot of requests if the user is in many servers.
                // @TODO Consider adding an option to only fetch channels for selected servers later.
                try {
                    const { data: guildChannels } = await discordApiRequest('GET', `/guilds/${guild.id}/channels`);
                    guildChannels.forEach(gc => {
                        // We are primarily interested in text channels where messages can be sent
                        if (gc.type === 0 || gc.type === 2 || gc.type === 5 || gc.type === 10 || gc.type === 11 || gc.type === 12) { // Text, Voice (text), Announcement, Thread types
                             channels.push({
                                id: gc.id,
                                name: `${gc.name} (${guild.name})`,
                                type: 'Server Channel'
                            });
                        }
                    });
                } catch (guildChannelError) {
                    console.warn(`[Disappear] Could not fetch channels for guild ${guild.name} (ID: ${guild.id}):`, guildChannelError);
                }
            }
            updateStatus('Channels fetched.');
            console.log('[Disappear] Fetched channels:', channels);
            return channels;
        } catch (error) {
            console.error('[Disappear] Error fetching channels:', error);
            updateStatus('<span style="color: red;">Failed to fetch channels.</span>', true);
            throw error;
        }
    }

    async function fetchMessages(channelId, authorId, beforeMessageId = null) {
        const logMessage = beforeMessageId ? 
            `Scanning for messages (before ${beforeMessageId}) in channel ${channelId}...` :
            `Scanning for initial messages in channel ${channelId}...`;
        updateStatus(logMessage, true); // Append this specific log
        console.log('[Disappear]', `Fetching messages for channel ${channelId}, author ${authorId}, before ${beforeMessageId || 'latest'}`);
        let endpoint = `/channels/${channelId}/messages?limit=${MESSAGE_FETCH_LIMIT}`;
        if (beforeMessageId) {
            endpoint += `&before=${beforeMessageId}`;
        }

        try {
            const { data: allMessages, headers } = await discordApiRequest('GET', endpoint);
            
            if (!allMessages || allMessages.length === 0) {
                // No messages returned at all from the API for this segment
                return { userMessages: [], oldestMessageIdInBatch: null, hasMore: false, headers, skipped: false };
            }

            // Filter messages by the current user
            const userMessages = allMessages.filter(msg => msg.author.id === authorId);
            const oldestMessageIdInBatch = allMessages[allMessages.length - 1].id;
            const hasMoreMessagesInChannel = allMessages.length === MESSAGE_FETCH_LIMIT;

            console.log('[Disappear]', `Fetched ${allMessages.length} raw messages, ${userMessages.length} by user ${authorId}. Oldest in batch: ${oldestMessageIdInBatch}`);
            return { userMessages, oldestMessageIdInBatch, hasMore: hasMoreMessagesInChannel, headers, skipped: false };
        } catch (error) {
            if (error.status === 429) { // Rate limit
                await handleRateLimit(new Headers(Object.entries(error.headers).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v]))); // GM_xmlhttpRequest headers are different
                 // After handling rate limit, throw a specific error or return a value indicating a retry is needed for the fetch itself
                 // This allows the calling function (processChannel) to decide to retry fetching this same segment.
                 throw { ...error, needsRetryFetch: true }; 
            }
            console.error('[Disappear] Error fetching messages:', error);
            updateStatus(`<span style="color: red;">Failed to fetch messages for channel ${channelId}.</span>`, true);
            if (error.message && (error.message.includes('403') || error.message.includes('Missing Access'))) {
                 console.warn(`[Disappear] Missing access to channel ${channelId}. Skipping.`);
                 return { userMessages: [], oldestMessageIdInBatch: null, hasMore: false, headers: null, skipped: true };
            }
            throw error; // Re-throw other errors
        }
    }

    async function deleteMessage(channelId, messageId) {
        console.log('[Disappear]', `Attempting delete for message ${messageId} in channel ${channelId}...`);
        try {
            const { headers } = await discordApiRequest('DELETE', `/channels/${channelId}/messages/${messageId}`);
            // Handle rate limits after successful delete too, as the limit might be hit on the NEXT request
            if (headers && headers.get('x-ratelimit-remaining') === '0') {
                 const delay = (parseFloat(headers.get('x-ratelimit-reset-after')) || (MIN_DELAY_MS/1000) + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS)/1000) * 1000;
                 console.warn(`[Disappear] Approaching rate limit (on success), waiting ${delay.toFixed(0)}ms`);
                 await new Promise(resolve => setTimeout(resolve, delay));
            } else {
                // Normal delay
                const delay = MIN_DELAY_MS + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
            return { success: true, headers };
        } catch (error) {
            // Check for system message error (Discord code 50021)
            if (error.status === 403 && error.responseText) {
                try {
                    const errorData = JSON.parse(error.responseText);
                    if (errorData.code === 50021 && errorData.message === "Cannot execute action on a system message") {
                        console.warn(`[Disappear] System message (${messageId}). Code 50021. Skipping.`);
                        updateStatus(`<span style="color: #ccaa00;">System msg ${messageId}. Skipping.</span>`, true);
                        return { success: false, systemMessage: true };
                    }
                } catch (parseError) {
                    console.warn('[Disappear] Could not parse error.responseText for 403 error:', parseError, error.responseText);
                }
            }

            // Handle "Unknown Message" error (Discord code 10008)
            if (error.status === 404 && error.responseText) {
                try {
                    const errorData = JSON.parse(error.responseText);
                    if (errorData.code === 10008) {
                        console.warn(`[Disappear] Unknown Message (${messageId}). Code 10008. Already deleted or inaccessible. Skipping.`);
                        updateStatus(`<span style="color: #ccaa00;">Msg ${messageId} not found (10008). Already deleted or inaccessible. Skipping.</span>`, true);
                        return { success: false, unknownMessage: true }; // Special flag for this case
                    }
                } catch (parseError) {
                    console.warn('[Disappear] Could not parse error.responseText for 404 error:', parseError, error.responseText);
                }
            }

            if (error.status === 429) { // Rate limit
                 await handleRateLimit(new Headers(Object.entries(error.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                 return { success: false, rateLimited: true }; // Indicate retry is needed by processChannel
            }

            // For other errors, log and re-throw to be handled by processChannel's retry logic
            console.error(`[Disappear] Error deleting message ${messageId}:`, error.status, error.message, error.responseText);
            // Status update for this will be in processChannel's catch block
            throw error; // This error will be caught by the `catch (deleteError)` in `processChannel`
        }
    }

    async function fetchAllUserMessagesForExport(channelId, authorId, channelName) {
        if (!isExporting) {
            updateStatus(`Export stopped for channel ${channelId}.`);
            return { messages: [], skipped: false, error: false };
        }
        // Set initial status for the channel export, subsequent logs from fetchMessages will append.
        updateStatus(`Preparing to export messages from channel ${channelName} (${channelId})...`, false);
        console.log('[Disappear]', `Fetching all messages for export from channel ${channelId} (${channelName}) for author ${authorId}`);

        let allUserMessagesInChannel = [];
        let lastFetchedMessageId = null;
        let consecutiveFetchFailures = 0;
        let consecutiveEmptyUserMessageFetches = 0;

        while (isExporting) {
            try {
                const {
                    userMessages,
                    oldestMessageIdInBatch,
                    hasMore,
                    headers,
                    skipped
                } = await fetchMessages(channelId, authorId, lastFetchedMessageId);

                if (skipped) {
                    updateStatus(`Skipped channel ${channelName} (${channelId}) for export due to missing access.`);
                    return { messages: [], skipped: true, error: false };
                }
                consecutiveFetchFailures = 0;

                if (userMessages && userMessages.length > 0) {
                    consecutiveEmptyUserMessageFetches = 0;
                    userMessages.forEach(msg => {
                        allUserMessagesInChannel.push({
                            message_id: msg.id,
                            timestamp: msg.timestamp,
                            edited_timestamp: msg.edited_timestamp || null,
                            content: msg.content,
                            attachments: msg.attachments,
                            embeds: msg.embeds,
                            author_id: msg.author.id,
                            author_username: msg.author.username,
                            channel_id: channelId,
                            channel_name: channelName
                        });
                    });
                    updateStatus(`Fetched ${userMessages.length} more messages from ${channelName}. Total for channel: ${allUserMessagesInChannel.length}. Scanning older...`, true);
                } else {
                    if (hasMore) {
                        consecutiveEmptyUserMessageFetches++;
                        updateStatus(`No user messages in this segment of ${channelName} (Scan ${consecutiveEmptyUserMessageFetches}/${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES}). Scanning older...`, true);
                        if (consecutiveEmptyUserMessageFetches >= MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES) {
                            updateStatus(`No user messages found after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} scans in ${channelName}. Assuming all user messages fetched.`, true);
                            break;
                        }
                    }
                }

                if (!hasMore) {
                    updateStatus(`All user messages likely fetched from ${channelName}. Total: ${allUserMessagesInChannel.length}.`, true);
                    break;
                }

                if (!oldestMessageIdInBatch) {
                    updateStatus(`No oldest message ID to paginate with in ${channelName} for export. Stopping channel scan.`, true);
                    break;
                }
                lastFetchedMessageId = oldestMessageIdInBatch;

                const delay = (MIN_DELAY_MS / 2 + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS) / 2);
                await new Promise(resolve => setTimeout(resolve, Math.max(250, delay / 2))); // Shorter delay for fetching

            } catch (fetchError) {
                consecutiveFetchFailures++;
                console.error(`[Disappear] Error during message export fetch for ${channelName} (Attempt ${consecutiveFetchFailures}):`, fetchError);

                if (fetchError.needsRetryFetch) {
                    updateStatus(`<span style="color: orange;">Rate limited fetching messages for ${channelName} (export). Retrying...</span>`, true);
                    if (!fetchError.status || fetchError.status !== 429) {
                        await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS));
                    }
                    continue;
                }

                if (fetchError.status === 429) {
                    await handleRateLimit(new Headers(Object.entries(fetchError.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                } else if (consecutiveFetchFailures >= 3) {
                    updateStatus(`<span style="color: red;">Too many errors exporting from ${channelName}. Skipping channel.</span>`, true);
                    return { messages: allUserMessagesInChannel, skipped: true, error: true };
                } else {
                    updateStatus(`<span style="color: orange;">Error exporting from ${channelName}. Retrying... (Attempt ${consecutiveFetchFailures})</span>`, true);
                    await new Promise(resolve => setTimeout(resolve, (MAX_DELAY_MS / 2) * consecutiveFetchFailures));
                }
            }
        }
        if (!isExporting) {
            updateStatus(`Export stopped by user during scan of ${channelName}.`, true);
        }
        return { messages: allUserMessagesInChannel, skipped: false, error: false };
    }

    async function processChannel(channelId, settings) {
        if (!isDeleting) {
            updateStatus(`Deletion stopped for channel ${channelId}.`);
            return;
        }
        // Set initial status for the channel, subsequent logs from fetchMessages will append.
        updateStatus(`Processing channel ${channelId}... (This may take a while, fetching message segments)`, false);
        console.log('[Disappear]', `Processing channel ${channelId}...`);

        if (!currentUserId) {
            updateStatus('<span style="color: red;">User ID not available. Cannot process channel.</span>', true);
            return;
        }

        let lastProcessedMessageId = null; // This will be the ID of the oldest message from the *previous* successful fetch operation
        let messagesDeletedInChannel = 0;
        let consecutiveFetchFailures = 0;
        let consecutiveEmptyUserMessageFetches = 0; // New counter

        while (isDeleting) {
            try {
                const { userMessages, oldestMessageIdInBatch, hasMore, headers, skipped } = await fetchMessages(channelId, currentUserId, lastProcessedMessageId);

                if (skipped) {
                    updateStatus(`Skipped channel ${channelId} due to missing access.`);
                    return; // Stop processing this channel
                }
                consecutiveFetchFailures = 0; // Reset on successful fetch (even if no user messages)

                if (userMessages && userMessages.length > 0) {
                    consecutiveEmptyUserMessageFetches = 0; // Reset if we found user messages
                    for (const message of userMessages) {
                        if (!isDeleting) {
                            updateStatus('Deletion process stopped globally.');
                            return;
                        }

                        // Proactively skip non-standard message types, even if authored by the user
                        const userDeletableTypes = [0, 19, 20, 21, 23]; // 0:Default, 19:Reply, 20:ChatInputCommand, 21:ThreadStarterMessage, 23:ContextMenuCommand
                        if (!userDeletableTypes.includes(message.type)) {
                            console.log(`[Disappear] Skipping message ${message.id} (type ${message.type}) in channel ${channelId}: Authored by user, but not a standard deletable message type.`);
                            updateStatus(`Skipping your msg ${message.id} (type ${message.type}) as it's not a standard deletable type.`, true);
                            continue; // Move to the next message
                        }

                        let attempts = 0;
                        let processedSuccessfully = false; // True if deleted or intentionally skipped (e.g. system message)

                        while (attempts < MAX_MESSAGE_DELETE_ATTEMPTS && !processedSuccessfully && isDeleting) {
                            try {
                                updateStatus(`Deleting msg ${message.id} (Ch:${channelId}) (Attempt ${attempts + 1}/${MAX_MESSAGE_DELETE_ATTEMPTS}, Total Ch Del: ${messagesDeletedInChannel})`);
                                const deleteResult = await deleteMessage(channelId, message.id);

                                if (deleteResult.success) {
                                    messagesDeletedInChannel++;
                                    updateStatus(`Deleted msg ${message.id} (Total Ch Del: ${messagesDeletedInChannel}). Waiting...`, true);
                                    processedSuccessfully = true;
                                } else if (deleteResult.systemMessage) {
                                    // Status already updated by deleteMessage
                                    processedSuccessfully = true; // Mark as "handled" to exit the retry loop
                                } else if (deleteResult.unknownMessage) { // Handle Unknown Message (10008)
                                    // Status already updated by deleteMessage
                                    processedSuccessfully = true; // Mark as "handled" to exit the retry loop
                                } else if (deleteResult.rateLimited) {
                                    updateStatus(`Rate limited on msg ${message.id}. Retrying after pause...`, true);
                                    // deleteMessage already paused. Loop will retry this message.
                                    // Do not increment attempts here.
                                } else {
                                    // This case should ideally not be reached if deleteMessage covers all returns or throws.
                                    attempts++;
                                    console.warn(`[Disappear] Unknown outcome from deleteMessage for ${message.id}. Attempt ${attempts}.`);
                                    if (attempts >= MAX_MESSAGE_DELETE_ATTEMPTS) {
                                        updateStatus(`<span style="color: red;">Unknown issue, msg ${message.id} skipped after ${attempts} attempts.</span>`, true);
                                        processedSuccessfully = true; // Give up
                                    } else if (isDeleting) {
                                        updateStatus(`<span style="color: orange;">Unknown issue with msg ${message.id}. Retrying (Att ${attempts + 1})...</span>`, true);
                                        await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS)); // Small delay
                                    }
                                }
                            } catch (deleteError) { // Catches errors THROWN by deleteMessage
                                attempts++;
                                const errStatus = deleteError.status || 'N/A';
                                const errMsg = deleteError.message || 'Unknown error';
                                console.warn(`[Disappear] Error deleting message ${message.id} (Attempt ${attempts}/${MAX_MESSAGE_DELETE_ATTEMPTS}): Status ${errStatus}, ${errMsg}`, deleteError.responseText || '');
                                
                                if (attempts >= MAX_MESSAGE_DELETE_ATTEMPTS) {
                                    updateStatus(`<span style="color: red;">Failed to delete msg ${message.id} after ${attempts} attempts (Status: ${errStatus}). Skipping.</span>`, true);
                                    processedSuccessfully = true; // Give up on this message
                                } else if (isDeleting) {
                                    updateStatus(`<span style="color: orange;">Error on msg ${message.id} (Status: ${errStatus}). Retrying (Att ${attempts + 1}/${MAX_MESSAGE_DELETE_ATTEMPTS})...</span>`, true);
                                    // Escalating delay for retries on the same message
                                    await new Promise(resolve => setTimeout(resolve, (MIN_DELAY_MS / 2 + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS)/2) * attempts));
                                }
                            }
                        } // End of while-retry loop for a single message

                        if (!processedSuccessfully && isDeleting) {
                             // This primarily catches cases where loop exited due to !isDeleting during retries
                             console.log(`[Disappear] Message ${message.id} in channel ${channelId} was not successfully processed.`);
                        } else if (!processedSuccessfully && !isDeleting) {
                            updateStatus(`Stopped while trying to process message ${message.id}.`, true);
                        }
                    } // End of for-loop for messages in current batch
                } else { // No user messages in this batch
                    if (hasMore) { // Batch was full, but no user messages
                        consecutiveEmptyUserMessageFetches++;
                        updateStatus(`No user messages in this segment of ${channelId} (Attempt ${consecutiveEmptyUserMessageFetches}/${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES}). Scanning older...`, true);
                        if (consecutiveEmptyUserMessageFetches >= MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES) {
                            updateStatus(`No user messages found after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} consecutive empty scans in ${channelId}. Assuming all user messages are processed.`, true);
                            console.log(`[Disappear] Stopping channel ${channelId} after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} consecutive empty user message fetches.`);
                            break; // Exit while loop for this channel
                        }
                    } else {
                        // If !hasMore and userMessages.length is 0, the condition below will catch it.
                        // No need to increment consecutiveEmptyUserMessageFetches here.
                    }
                }

                if (!hasMore) { // No more messages in the channel according to API (current batch was not full)
                    updateStatus(`All your messages likely deleted in channel ${channelId}. Total for channel: ${messagesDeletedInChannel}`);
                    console.log(`[Disappear] No more messages (hasMore=false) or oldestMessageIdInBatch is null for channel ${channelId}.`);
                    break; // Exit while loop for this channel
                }

                if (!oldestMessageIdInBatch) {
                    // This case should ideally be covered by !hasMore if API returns empty and it's the end.
                    // If oldestMessageIdInBatch is null but hasMore was somehow true, it's an issue.
                    updateStatus(`No oldest message ID to paginate with in ${channelId}, but API indicated more. Stopping channel to be safe.`);
                    console.warn(`[Disappear] Inconsistent state: hasMore is true but no oldestMessageIdInBatch for channel ${channelId}.`);
                    break;
                }

                // Prepare for the next fetch iteration
                lastProcessedMessageId = oldestMessageIdInBatch;

            } catch (fetchOrProcessError) {
                consecutiveFetchFailures++;
                console.error(`[Disappear] Error during fetch/process for channel ${channelId} (Attempt ${consecutiveFetchFailures}):`, fetchOrProcessError);

                if (fetchOrProcessError.needsRetryFetch) { // Specific error from fetchMessages indicating a rate limit retry for the fetch itself
                    updateStatus(`<span style="color: orange;">Rate limited on fetching messages for ${channelId}. Automatic retry after pause...</span>`, true);
                    // fetchMessages already paused due to GM_xmlhttpRequest 429, or its internal handleRateLimit on other errors.
                    // We just continue the loop, and it will try fetching the same segment (using same lastProcessedMessageId).
                    // No need for additional manual pause here as handleRateLimit in fetchMessages should have done it.
                    // If fetchMessages didn't pause (e.g. error was not 429 but some other retryable), ensure a small delay.
                     if (!fetchOrProcessError.status || fetchOrProcessError.status !== 429) {
                         await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS)); // Small safety delay if not a 429
                     }
                    continue; // Retry fetching this segment
                }

                if (fetchOrProcessError.status === 429) { // General rate limit (e.g. from delete that propagated)
                    await handleRateLimit(new Headers(Object.entries(fetchOrProcessError.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                } else if (consecutiveFetchFailures >= 3) {
                    updateStatus(`<span style="color: red;">Too many errors for channel ${channelId}. Skipping this channel.</span>`, true);
                    console.error(`[Disappear] Too many consecutive errors for channel ${channelId}. Skipping this channel.`);
                    break; // Stop processing this channel
                } else {
                    updateStatus(`<span style="color: orange;">Error in channel ${channelId}. Retrying after a delay... (Attempt ${consecutiveFetchFailures})</span>`, true);
                    await new Promise(resolve => setTimeout(resolve, MAX_DELAY_MS * consecutiveFetchFailures));
                }
            }
        }
        if (isDeleting) { // Only log as completed if deletion wasn't stopped globally
            console.log(`[Disappear] Finished processing channel ${channelId}. Deleted ${messagesDeletedInChannel} messages.`);
            updateStatus(`Finished scan for channel ${channelId}. Total messages deleted in this channel: ${messagesDeletedInChannel}.`, true);
        }
    }

    function handleRateLimit(responseHeaders) { // Changed to accept responseHeaders object
        let retryAfter = 1; // Default retry after 1 second
        if (responseHeaders) {
            // Try to get from 'retry-after' (standard HTTP header, in seconds)
            const httpRetryAfter = responseHeaders.get('retry-after');
            if (httpRetryAfter) {
                retryAfter = parseFloat(httpRetryAfter);
            } else {
                // Try to get from 'x-ratelimit-reset-after' (Discord specific, in seconds with fractional)
                const discordRetryAfter = responseHeaders.get('x-ratelimit-reset-after');
                if (discordRetryAfter) {
                    retryAfter = parseFloat(discordRetryAfter);
                }
            }
        }
        // Ensure retryAfter is a sensible number, not NaN, and add a small buffer
        retryAfter = isNaN(retryAfter) ? 1 : Math.max(0.5, retryAfter) + 0.25; // Minimum 0.5s + buffer

        const waitMs = retryAfter * 1000;
        updateStatus(`<span style="color: orange;">Rate limited. Waiting ${retryAfter.toFixed(2)}s...</span>`, true);
        console.warn(`[Disappear] Rate limited. Waiting ${waitMs}ms (derived from headers: retry-after or x-ratelimit-reset-after)`);
        return new Promise(resolve => setTimeout(resolve, waitMs));
    }

    function convertToCSV(dataArray) {
        if (!dataArray || dataArray.length === 0) {
            return "";
        }
        const headers = Object.keys(dataArray[0]);
        const csvRows = [];
        csvRows.push(headers.join(',')); // Add header row

        for (const row of dataArray) {
            const values = headers.map(header => {
                let cell = row[header];
                if (cell === null || cell === undefined) {
                    cell = "";
                } else if (typeof cell === 'object') {
                    cell = JSON.stringify(cell); // Stringify arrays/objects like attachments/embeds
                } else {
                    cell = String(cell);
                }
                // Escape double quotes by doubling them, and wrap if it contains comma, double quote, or newline
                if (cell.includes('"') || cell.includes(',') || cell.includes('\n') || cell.includes('\r')) {
                    cell = `"${cell.replace(/"/g, '""')}"`;
                }
                return cell;
            });
            csvRows.push(values.join(','));
        }
        return csvRows.join('\n');
    }

    function triggerDownload(data, format, filename) {
        let mimeType;
        let fileExtension;
        let content;

        if (format === 'json') {
            mimeType = 'application/json';
            fileExtension = 'json';
            content = JSON.stringify(data, null, 2);
        } else if (format === 'csv') {
            mimeType = 'text/csv';
            fileExtension = 'csv';
            content = convertToCSV(data);
        } else {
            console.error("[Disappear] Unsupported download format:", format);
            updateStatus(`<span style="color: red;">Unsupported download format: ${format}</span>`, true);
            return;
        }

        const blob = new Blob([content], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${filename}.${fileExtension}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        updateStatus(`Download started for ${filename}.${fileExtension}`, true);
    }

    // --- UI Creation and Management ---
    function updateStatus(message, append = false) {
        if (statusDiv) {
            if (append) {
                const timestamp = new Date().toLocaleTimeString();
                statusDiv.innerHTML += `<div>[${timestamp}] ${message}</div>`;
                statusDiv.scrollTop = statusDiv.scrollHeight; // Scroll to bottom
            } else {
                statusDiv.innerHTML = message;
            }
        }
        console.log('[Disappear Status]', message.replace(/<[^>]*>?/gm, '')); // Log cleaned message
    }

    function addControlButton() {
        controlButton = document.createElement('button');
        controlButton.innerHTML = '🗑️ Disappear';
        controlButton.id = `${SCRIPT_PREFIX}-control-button`;
        // Basic styling
        controlButton.style.position = 'fixed';
        controlButton.style.top = '15px';
        controlButton.style.right = '15px';
        controlButton.style.zIndex = '9999';
        controlButton.style.backgroundColor = '#7289da'; // Discord blurple
        controlButton.style.color = 'white';
        controlButton.style.border = 'none';
        controlButton.style.padding = '10px 15px';
        controlButton.style.borderRadius = '5px';
        controlButton.style.cursor = 'pointer';
        controlButton.title = 'Open Disappear Options'; // Tooltip

        controlButton.onclick = showModal;
        document.body.appendChild(controlButton);
    }

    function createModalUI(channels) {
        if (modalContainer) modalContainer.remove();

        modalContainer = document.createElement('div');
        modalContainer.id = `${SCRIPT_PREFIX}-modal`;
        // Basic styling (can be improved with GM_addStyle)
        modalContainer.style.position = 'fixed';
        modalContainer.style.top = '50%';
        modalContainer.style.left = '50%';
        modalContainer.style.transform = 'translate(-50%, -50%)';
        modalContainer.style.backgroundColor = '#36393f'; // Discord's dark theme color
        modalContainer.style.padding = '20px';
        modalContainer.style.borderRadius = '8px';
        modalContainer.style.zIndex = '10000';
        modalContainer.style.color = 'white';
        modalContainer.style.maxHeight = '80vh';
        modalContainer.style.overflowY = 'auto';
        modalContainer.style.border = '1px solid #202225';


        modalContainer.innerHTML = `
            <h2 style="margin-top:0; border-bottom: 1px solid #4f545c; padding-bottom:10px;">Disappear by Vili (vili.dev)</h2>
            <p><strong>Warning:</strong> This tool permanently deletes messages. Use with caution. Sharing your auth token is risky.</p>
            
            <div style="margin-bottom: 15px;">
                <label for="${SCRIPT_PREFIX}-auth-token">Auth Token (Required):</label>
                <input type="password" id="${SCRIPT_PREFIX}-auth-token" value="${authToken || ''}" style="width: 100%; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px; margin-top:5px;">
                <small>Find this via Inspect Element -> Application -> Local Storage, find discord.com, look for 'token'. Be careful!</small>
            </div>

            <div style="margin-bottom: 15px;">
                <label for="${SCRIPT_PREFIX}-min-delay">Min Delay (ms):</label>
                <input type="number" id="${SCRIPT_PREFIX}-min-delay" value="${MIN_DELAY_MS}" style="width: 80px; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px;">
                <label for="${SCRIPT_PREFIX}-max-delay" style="margin-left:10px;">Max Delay (ms):</label>
                <input type="number" id="${SCRIPT_PREFIX}-max-delay" value="${MAX_DELAY_MS}" style="width: 80px; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px;">
            </div>

            <p>Select channels/DMs/groups to process:</p>
            <button id="${SCRIPT_PREFIX}-select-all" style="margin-bottom:5px;">Select All</button>
            <button id="${SCRIPT_PREFIX}-deselect-all" style="margin-bottom:5px; margin-left:5px;">Deselect All</button>

            <div id="${SCRIPT_PREFIX}-channel-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #4f545c; padding: 10px; background-color: #2f3136; border-radius:3px;">
                ${channels.length > 0 ? channels.map(ch => `
                    <div style="margin-bottom: 5px;">
                        <input type="checkbox" id="${SCRIPT_PREFIX}-ch-${ch.id}" data-channel-id="${ch.id}" data-channel-name="${ch.name}" checked style="margin-right:5px;">
                        <label for="${SCRIPT_PREFIX}-ch-${ch.id}" title="${ch.id}">${ch.name} (${ch.type})</label>
                    </div>
                `).join('') : '<p>No channels loaded yet. Click "Refresh Channels" or ensure token is valid.</p>'}
            </div>
            <br>
            <button id="${SCRIPT_PREFIX}-refresh-channels" style="margin-right: 10px;">Refresh Channels</button>
            <button id="${SCRIPT_PREFIX}-start-button" class="danger ${SCRIPT_PREFIX}-action-button">Start Deletion</button>
            <button id="${SCRIPT_PREFIX}-stop-button" class="${SCRIPT_PREFIX}-action-button" style="display: none;">Stop Process</button>
            <button id="${SCRIPT_PREFIX}-export-button" class="${SCRIPT_PREFIX}-action-button">Export Messages</button>
            <select id="${SCRIPT_PREFIX}-export-format" style="background-color: #202225; color: white; border: 1px solid #000; border-radius:3px;">
                <option value="json">JSON</option>
                <option value="csv">CSV</option>
            </select>
            <button id="${SCRIPT_PREFIX}-close-button" class="${SCRIPT_PREFIX}-action-button" style="float:right;">Close</button>
            <button id="${SCRIPT_PREFIX}-hide-button" class="${SCRIPT_PREFIX}-action-button" style="float:right;">Hide</button>
            <div id="${SCRIPT_PREFIX}-status-container" style="margin-top: 15px; padding:10px; background-color: #2f3136; border-radius:3px; min-height: 50px; max-height:250px; overflow-y:auto; border: 1px solid #000;">
                 <div id="${SCRIPT_PREFIX}-status">Enter Auth Token and click "Refresh Channels".</div>
            </div>
        `;

        document.body.appendChild(modalContainer);
        statusDiv = document.getElementById(`${SCRIPT_PREFIX}-status`); // Initialize statusDiv

        document.getElementById(`${SCRIPT_PREFIX}-hide-button`).onclick = () => {
            if (modalContainer) {
                modalContainer.style.display = 'none';
            }
        };

        document.getElementById(`${SCRIPT_PREFIX}-close-button`).onclick = () => {
            if (isDeleting || isExporting) {
                const processName = isDeleting ? "Deletion" : "Export";
                if (!confirm(`${processName} is in progress. Are you sure you want to close? This will stop the current process.`)) {
                    return;
                }
                stopCurrentOperation(); // This will set the appropriate flag to false
            }
            if (modalContainer) {
                modalContainer.remove();
                modalContainer = null; // Explicitly nullify to ensure fresh creation next time
            }
        };
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).onclick = startDeletionProcess;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).onclick = stopCurrentOperation;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).onclick = startExportProcess;
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).onclick = async () => {
            authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
             if (!authToken) {
                updateStatus('<span style="color: red;">Please enter Auth Token first!</span>', true);
                return;
            }
            localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);
            try {
                await fetchAuthenticatedUser(); // Fetch user ID first
                const fetchedChannels = await fetchChannels();
                // Re-render the channel list part of the modal
                const channelListDiv = document.getElementById(`${SCRIPT_PREFIX}-channel-list`);
                if (channelListDiv) {
                    channelListDiv.innerHTML = fetchedChannels.length > 0 ? fetchedChannels.map(ch => `
                        <div style="margin-bottom: 5px;">
                            <input type="checkbox" id="${SCRIPT_PREFIX}-ch-${ch.id}" data-channel-id="${ch.id}" data-channel-name="${ch.name}" checked style="margin-right:5px;">
                            <label for="${SCRIPT_PREFIX}-ch-${ch.id}" title="${ch.id}">${ch.name} (${ch.type})</label>
                        </div>
                    `).join('') : '<p>No channels found or unable to fetch.</p>';
                }
                 updateStatus(`Fetched ${fetchedChannels.length} channels. Ready to select and start.`, true);
            } catch (error) {
                updateStatus(`<span style="color: red;">Error refreshing channels: ${error.message}</span>`, true);
            }
        };

        document.getElementById(`${SCRIPT_PREFIX}-select-all`).onclick = () => {
            document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]`).forEach(cb => cb.checked = true);
        };
        document.getElementById(`${SCRIPT_PREFIX}-deselect-all`).onclick = () => {
            document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]`).forEach(cb => cb.checked = false);
        };


        // Load saved token if available
        const savedToken = localStorage.getItem(`${SCRIPT_PREFIX}_authToken`);
        if (savedToken) {
            document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value = savedToken;
            authToken = savedToken; // Update global authToken
        }
         // Load saved delays
        MIN_DELAY_MS = parseInt(localStorage.getItem(`${SCRIPT_PREFIX}_minDelay`) || MIN_DELAY_MS);
        MAX_DELAY_MS = parseInt(localStorage.getItem(`${SCRIPT_PREFIX}_maxDelay`) || MAX_DELAY_MS);
        document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value = MIN_DELAY_MS;
        document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value = MAX_DELAY_MS;
    }

    function showModal() {
        if (modalContainer && document.body.contains(modalContainer)) {
            modalContainer.style.display = 'block';
            // Ensure statusDiv is valid if the modal is being reshown
            if (!statusDiv) {
                statusDiv = document.getElementById(`${SCRIPT_PREFIX}-status`);
            }
        } else {
            // If modalContainer was removed (by close button or never created)
            // or if it exists in variable but not in DOM (e.g., edge case)
            if (modalContainer) { // Clean up if it exists but isn't in DOM, or to be certain
                modalContainer.remove();
            }
            createModalUI([]); // Create it fresh, this also initializes statusDiv
            // createModalUI already sets an initial status message.
            // If a different one is needed here, it can be set.
            // For now, createModalUI's default is fine.
        }
    }

    async function startDeletionProcess() {
        if (isDeleting) {
            updateStatus('<span style="color: orange;">Deletion is already in progress.</span>', true);
            return;
        }

        authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
        if (!authToken) {
            updateStatus('<span style="color: red;">Auth Token is required!</span>', true);
            return;
        }
        localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);

        MIN_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value) || 1000;
        MAX_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value) || 3000;
        if (MIN_DELAY_MS < 200) MIN_DELAY_MS = 200; // Safety floor
        if (MAX_DELAY_MS < MIN_DELAY_MS) MAX_DELAY_MS = MIN_DELAY_MS + 200;
        localStorage.setItem(`${SCRIPT_PREFIX}_minDelay`, MIN_DELAY_MS);
        localStorage.setItem(`${SCRIPT_PREFIX}_maxDelay`, MAX_DELAY_MS);

        const selectedChannelElements = Array.from(document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]:checked`));
        const channelsToProcess = selectedChannelElements.map(el => ({
            id: el.dataset.channelId,
            name: el.dataset.channelName
        }));

        if (channelsToProcess.length === 0) {
            updateStatus('<span style="color: orange;">No channels selected for deletion.</span>', true);
            return;
        }

        if (!confirm(`Are you sure you want to delete ALL YOUR MESSAGES in ${channelsToProcess.length} selected channel(s)/DM(s)? This action is IRREVERSIBLE.`)) {
            updateStatus('Deletion cancelled by user.', true);
            return;
        }

        isDeleting = true;
        updateControlButtonIndicator(true); // Indicate working
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).style.display = 'inline-block';
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).disabled = true;

        // Clear previous logs and start fresh for this operation session
        if (statusDiv) statusDiv.innerHTML = '';
        updateStatus(`Starting deletion process for ${channelsToProcess.length} channel(s)...`, true);
        console.log('[Disappear]', 'Selected channels for deletion:', channelsToProcess.map(c => c.name));

        try {
            if (!currentUserId) {
                await fetchAuthenticatedUser();
            }
            if (!currentUserId) {
                 throw new Error("Could not obtain User ID. Cannot proceed.");
            }

            for (const channel of channelsToProcess) {
                if (!isDeleting) {
                    updateStatus('Deletion stopped by user during channel iteration.', true);
                    break;
                }
                updateStatus(`--- Starting channel: ${channel.name} (ID: ${channel.id}) ---`, true);
                await processChannel(channel.id, {});
                if (isDeleting) {
                    updateStatus(`--- Finished channel: ${channel.name} ---`, true);
                }
            }
        } catch (error) {
            console.error('[Disappear] Critical error during deletion process:', error);
            updateStatus(`<span style="color: red;">A critical error occurred: ${error.message}. Process halted. Check console.</span>`, true);
        } finally {
            if (isDeleting) { // This means it ran to completion or an error occurred while still 'isDeleting'
                updateStatus('Deletion process run completed. Check logs for details on each channel.', true);
            } else if (!isDeleting && channelsToProcess.length > 0) { // This means it was stopped by user
                 updateStatus('Deletion process was stopped by user or an early error. Check logs.', true);
            }
            isDeleting = false;
            updateControlButtonIndicator(false); // Revert indicator
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
        }
    }

    function stopCurrentOperation() {
        if (isDeleting) {
            isDeleting = false;
            updateStatus('<span style="color: orange;">Stopping deletion process... Please wait for current operations to finish.</span>', true);
            console.log('[Disappear] Stop command received for DELETION. Process will halt after the current message/batch.');
        } else if (isExporting) {
            isExporting = false;
            updateStatus('<span style="color: orange;">Stopping export process... Please wait for current fetch to finish.</span>', true);
            console.log('[Disappear] Stop command received for EXPORT. Process will halt after the current fetch segment.');
        } else {
            updateStatus('No process is currently running.');
        }
        // UI updates for buttons are primarily handled in the finally blocks of startDeletionProcess/startExportProcess
        // However, we can ensure the stop button is hidden if no process was found to be running.
        if (!isDeleting && !isExporting) {
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            // Also ensure other buttons are enabled if modal is open
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
        }
    }

    async function startExportProcess() {
        if (isDeleting) {
            updateStatus('<span style="color: orange;">Deletion is in progress. Please stop it before exporting.</span>', true);
            return;
        }
        if (isExporting) {
            updateStatus('<span style="color: orange;">Export is already in progress.</span>', true);
            return;
        }

        authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
        if (!authToken) {
            updateStatus('<span style="color: red;">Auth Token is required!</span>', true);
            return;
        }
        localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);

        MIN_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value) || 1000;
        MAX_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value) || 3000;
        localStorage.setItem(`${SCRIPT_PREFIX}_minDelay`, MIN_DELAY_MS);
        localStorage.setItem(`${SCRIPT_PREFIX}_maxDelay`, MAX_DELAY_MS);

        const selectedChannelElements = Array.from(document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]:checked`));
        const channelsToExport = selectedChannelElements.map(el => ({
            id: el.dataset.channelId,
            name: el.dataset.channelName
        }));

        if (channelsToExport.length === 0) {
            updateStatus('<span style="color: orange;">No channels selected for export.</span>', true);
            return;
        }

        const exportFormat = document.getElementById(`${SCRIPT_PREFIX}-export-format`).value;

        if (!confirm(`Are you sure you want to fetch and export ALL YOUR MESSAGES from ${channelsToExport.length} selected channel(s)/DM(s) as ${exportFormat.toUpperCase()}? This might take a while and generate many API requests.`)) {
            updateStatus('Export cancelled by user.', true);
            return;
        }

        isExporting = true;
        updateControlButtonIndicator(true); // Indicate working
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).style.display = 'inline-block';
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).disabled = true;

        // Clear previous logs and start fresh for this operation session
        if (statusDiv) statusDiv.innerHTML = '';
        updateStatus(`Starting export process for ${channelsToExport.length} channel(s) as ${exportFormat.toUpperCase()}...`, true);
        console.log('[Disappear]', 'Selected channels for export:', channelsToExport.map(c => c.name));

        let allExportedMessages = [];

        try {
            if (!currentUserId) {
                await fetchAuthenticatedUser();
            }
            if (!currentUserId) {
                throw new Error("Could not obtain User ID. Cannot proceed with export.");
            }

            for (const channel of channelsToExport) {
                if (!isExporting) {
                    updateStatus('Export stopped by user during channel iteration.', true);
                    break;
                }
                updateStatus(`--- Starting export for channel: ${channel.name} (ID: ${channel.id}) ---`, true);
                const { messages: channelMessages, skipped, error: channelError } = await fetchAllUserMessagesForExport(channel.id, currentUserId, channel.name);

                if (skipped && channelError) {
                    updateStatus(`<span style="color: red;">--- Skipped channel ${channel.name} due to errors during export. ---</span>`, true);
                } else if (skipped) {
                    updateStatus(`--- Channel ${channel.name} was skipped (e.g. no access or stopped). ---`, true);
                } else if (channelMessages.length > 0) {
                    allExportedMessages.push(...channelMessages);
                    updateStatus(`--- Finished export for channel: ${channel.name}. Fetched ${channelMessages.length} messages. Total: ${allExportedMessages.length} ---`, true);
                } else if (isExporting) {
                    updateStatus(`--- Finished export for channel: ${channel.name}. No new messages found or fetched for this channel. ---`, true);
                }
            }

            if (isExporting && allExportedMessages.length > 0) {
                updateStatus(`Total messages fetched from all selected channels: ${allExportedMessages.length}. Preparing download...`, true);
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, -5);
                triggerDownload(allExportedMessages, exportFormat, `discord_messages_export_${timestamp}`);
            } else if (isExporting) {
                updateStatus('No messages found to export from any of the selected channels after processing all.', true);
            }

        } catch (error) {
            console.error('[Disappear] Critical error during export process:', error);
            updateStatus(`<span style="color: red;">A critical error occurred during export: ${error.message}. Process halted. Check console.</span>`, true);
        } finally {
            if (isExporting) {
                updateStatus('Export process run completed. Check logs for details.', true);
            } else if (!isExporting && channelsToExport.length > 0) {
                updateStatus('Export process was stopped by user or an early error. Check logs.', true);
            }
            isExporting = false;
            updateControlButtonIndicator(false); // Revert indicator
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
        }
    }

    // --- Initialization ---
    function init() {
        console.log('[Disappear]', 'Script loaded.');
        addControlButton();

        // We need a way to get the user's own ID for message filtering.
        // This often involves listening to network requests or inspecting internal Discord objects.
        // For Undiscord, it seems to get it from an API endpoint or a global variable.
        // We'll need to investigate how to reliably get the current user's ID.
        // We will try to fetch this when the user clicks "Refresh Channels" or "Start Deletion" after providing token.

        GM_addStyle(`
            #${SCRIPT_PREFIX}-modal button.danger { background-color: #f04747; color: white; } /* Default, will be more specific below */
            #${SCRIPT_PREFIX}-modal button.danger:hover { background-color: #d84040; } /* Default, will be more specific below */
            #${SCRIPT_PREFIX}-modal input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle;}
            #${SCRIPT_PREFIX}-status-container div { margin-bottom: 4px; font-size: 0.9em;}
            #${SCRIPT_PREFIX}-status-container div:last-child { margin-bottom: 0;}
            
            @keyframes disappearPulse {
                0% { box-shadow: 0 0 0 0 rgba(114, 137, 218, 0.7); }
                70% { box-shadow: 0 0 0 10px rgba(114, 137, 218, 0); }
                100% { box-shadow: 0 0 0 0 rgba(114, 137, 218, 0); }
            }
            .${SCRIPT_PREFIX}-working-indicator {
                animation: disappearPulse 2s infinite;
            }

            /* Base style for main action buttons in the modal */
            #${SCRIPT_PREFIX}-modal .${SCRIPT_PREFIX}-action-button {
                padding: 7px 14px; /* Uniform padding */
                border-radius: 3px;
                border: 1px solid transparent;
                cursor: pointer;
                margin-left: 8px; /* Uniform margin */
                min-width: 110px; /* Minimum width for consistency */
                box-sizing: border-box;
                text-align: center;
                vertical-align: middle;
                font-size: 14px;
            }

            /* Start Deletion Button (uses .danger class) */
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button {
                background-color: #f04747;
                color: white;
                border-color: #f04747;
            }
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #d84040;
                border-color: #d84040;
            }
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button:disabled {
                background-color: #747f8d; /* Discord's disabled button grey */
                color: #dcddde;       /* Discord's disabled button text color */
                border-color: #747f8d;
                cursor: not-allowed;
            }

            /* Stop Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-stop-button.${SCRIPT_PREFIX}-action-button {
                background-color: #faa61a; /* Orange/Warning */
                color: white;
                border-color: #faa61a;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-stop-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #e79817;
                border-color: #e79817;
            }

            /* Export Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button {
                background-color: #43b581; /* Green */
                color: white;
                border-color: #43b581;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #3aa873;
                border-color: #3aa873;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button:disabled {
                background-color: #3a7056; /* Muted green for disabled */
                color: #a0c3b0;
                border-color: #3a7056;
                cursor: not-allowed;
            }

            /* Hide Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-hide-button.${SCRIPT_PREFIX}-action-button {
                background-color: #5865f2; /* Blurple */
                color: white;
                border-color: #5865f2;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-hide-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #4a55cf;
                border-color: #4a55cf;
            }

            /* Close Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-close-button.${SCRIPT_PREFIX}-action-button {
                background-color: #72767d; /* Discord's secondary/grey button */
                color: white;
                border-color: #72767d;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-close-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #686c72;
                border-color: #686c72;
            }
            
            /* Styling for the export format select element */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-format {
                padding: 7px;
                background-color: #202225;
                color: white;
                border: 1px solid #000;
                border-radius: 3px;
                vertical-align: middle;
                margin-left: 8px;
                font-size: 14px;
            }
        `);
    }

    // Wait for Discord to load before initializing
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();