Drawaria Chat Tools (Downloader & Message All Friends)

La herramienta definitiva para descargar conversaciones de chat y enviar mensajes masivos en Drawaria.online. Ambos menús coexisten independientemente.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Drawaria Chat Tools (Downloader & Message All Friends)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  La herramienta definitiva para descargar conversaciones de chat y enviar mensajes masivos en Drawaria.online. Ambos menús coexisten independientemente.
// @author       YouTubeDrawaria
// @match        *://*.drawaria.online/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// ==/UserScript==

(function() {
    'use strict';

    // --- GLOBAL STYLES FOR BOTH MENUS ---
    // Combined and scoped CSS to prevent conflicts
    GM_addStyle(`
        /* Base styles for both containers */
        #chat-downloader-container, #mass-msg-container {
            position: fixed;
            background-color: #fff;
            border: 1px solid #d3d3d3;
            border-radius: 8px;
            z-index: 9999;
            box-shadow: 0 8px 16px rgba(0,0,0,0.2);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            font-size: 14px;
            color: #333;
        }

        /* Header styles for both menus */
        #chat-downloader-header, #mass-msg-header {
            padding: 10px;
            cursor: move;
            z-index: 10000;
            background-color: #007bff;
            color: #fff;
            border-top-left-radius: 7px;
            border-top-right-radius: 7px;
            text-align: center;
            font-weight: bold;
        }

        /* Toggle button styles for both menus */
        #chat-downloader-toggle, #mass-msg-toggle {
            padding: 4px 0;
            background-color: #f8f9fa;
            text-align: center;
            cursor: pointer;
            border-bottom: 1px solid #d3d3d3;
            user-select: none;
        }
        #chat-downloader-toggle:hover, #mass-msg-toggle:hover {
            background-color: #e2e6ea;
        }

        /* Body styles for both menus */
        #chat-downloader-body, #mass-msg-body {
            padding: 15px;
            overflow: hidden;
            transition: all 0.3s ease;
        }

        /* Collapsed state for both menus */
        #chat-downloader-container.collapsed #chat-downloader-body,
        #mass-msg-container.collapsed #mass-msg-body {
            display: none;
        }
        #chat-downloader-container.collapsed #chat-downloader-toggle,
        #mass-msg-container.collapsed #mass-msg-toggle {
            border-bottom-left-radius: 7px;
            border-bottom-right-radius: 7m;
            border-bottom: none;
        }

        /* Dragging cursor styles for both */
        body.chat-downloader-dragging, body.chat-downloader-dragging *,
        body.mass-msg-dragging, body.mass-msg-dragging * {
            cursor: move !important;
            user-select: none !important;
        }

        /* Common input/select styles scoped to containers */
        #chat-downloader-container label, #mass-msg-container label {
            display: block;
            margin: 12px 0 5px 0;
            font-weight: 600;
            color: #333;
        }
        #chat-downloader-container select,
        #chat-downloader-container input[type="date"],
        #mass-msg-container input,
        #mass-msg-container select,
        #mass-msg-container textarea {
            width: 100%;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
            margin-bottom: 5px; /* Added for date inputs spacing */
        }
        #mass-msg-container textarea { resize: vertical; min-height: 80px; }

        /* Generic section headers for both */
        #chat-downloader-container .section-header, #mass-msg-container .section-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-top: 10px;
        }
        #chat-downloader-container .section-toggle, #mass-msg-container .section-toggle {
            background: none;
            border: none;
            color: #007bff;
            cursor: pointer;
            padding: 8px 0;
            font-weight: bold;
            text-align: left;
            flex-grow: 1;
        }
        #chat-downloader-container .collapsible-section, #mass-msg-container .collapsible-section {
            border-top: 1px solid #ddd;
            padding-top: 10px;
            margin-top: 5px;
        }
        /* Default for chat downloader section is open, mass message advanced options default closed */
        #chat-downloader-container #chat-download-section { display: block; }
        #mass-msg-container #advanced-options, #mass-msg-container #profile-section { display: none; }


        /* Button groups */
        #chat-downloader-container .button-group, #mass-msg-container .button-container {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }
        #chat-downloader-container .button-group button, #mass-msg-container .button-container button {
            flex-grow: 1;
            padding: 10px;
            color: black;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        #chat-downloader-container #download-chat-btn, #mass-msg-container #start-mass-msg, #mass-msg-container #progress-bar-inner { background-color: #28a745; }
        #chat-downloader-container #download-chat-btn:hover, #mass-msg-container #start-mass-msg:hover { background-color: #218838; }
        #chat-downloader-container #copy-chat-btn { background-color: #007bff; }
        #chat-downloader-container #copy-chat-btn:hover { background-color: #0056b3; }
        #mass-msg-container #stop-mass-msg { background-color: #dc3545; display: none; } /* Default hidden for mass msg */
        #mass-msg-container #stop-mass-msg:hover { background-color: #c82333; }

        #chat-downloader-container #download-chat-btn:disabled, #chat-downloader-container #copy-chat-btn:disabled,
        #mass-msg-container #start-mass-msg:disabled {
            background-color: #aaa;
            cursor: not-allowed;
        }

        /* Progress indicator for chat downloader */
        #chat-downloader-container #progress-indicator {
            text-align: center;
            margin-top: 10px;
            font-weight: bold;
            color: #007bff;
            display: none;
        }

        /* Log panel styles for both menus */
        #chat-downloader-container #chat-downloader-log, #mass-msg-container #mass-msg-log {
            margin-top: 10px;
            padding: 8px;
            background-color: #fff;
            border: 1px solid #ddd;
            height: 100px; /* Adjusted to 100px for consistency, mass-msg was 120px */
            overflow-y: auto;
            font-size: 12px;
            border-radius: 4px;
            line-height: 1.5;
            position: relative;
        }
        #chat-downloader-container #chat-downloader-log-clear {
            position: absolute;
            top: 5px;
            right: 5px;
            background: #f0f0f0;
            border: 1px solid #ccc;
            border-radius: 3px;
            padding: 2px 5px;
            font-size: 10px;
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.2s;
        }
        #chat-downloader-container #chat-downloader-log-clear:hover {
            opacity: 1;
            background: #e0e0e0;
        }

        /* Log message types for both */
        .log-info { color: #555; }
        .log-success { color: #28a745; font-weight: bold; }
        .log-error { color: #dc3545; font-weight: bold; }
        .log-pause { color: #ff8c00; font-style: italic; } /* Specific to mass message */

        /* Styles specific to Mass Message menu */
        #mass-msg-container { width: 360px; } /* Specific width */
        #mass-msg-container .info-button { font-size: 14px; font-weight: bold; color: #007bff; cursor: pointer; border: 1px solid #007bff; border-radius: 50%; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; transition: background-color 0.2s, color 0.2s; }
        #mass-msg-container .info-button:hover { background-color: #007bff; color: #fff; }
        #mass-msg-container .input-grid { display: flex; gap: 10px; margin-top: 12px; }
        #mass-msg-container .grid-item { flex: 1; min-width: 0; }
        #mass-msg-container .grid-item label { margin-top: 0; }
        #mass-msg-container #profile-manager { display: flex; gap: 5px; align-items: center; }
        #mass-msg-container #profile-manager select { flex-grow: 1; }
        #mass-msg-container #profile-manager button { padding: 8px; white-space: nowrap; }
        #mass-msg-container #exclusion-controls { display: flex; gap: 5px; margin: 8px 0; }
        #mass-msg-container #exclusion-controls button { flex: 1; padding: 3px; font-size: 10px; }
        #mass-msg-container #progress-container { margin-top: 15px; display: none; }
        #mass-msg-container #progress-bar { width: 100%; height: 20px; background-color: #e9ecef; border-radius: 4px; overflow: hidden; }
        #mass-msg-container #progress-bar-inner { width: 0%; height: 100%; transition: width 0.3s ease; }
        #mass-msg-container #progress-text { text-align: center; font-size: 12px; margin-top: 4px; }
        #mass-msg-container .exclusion-container { border: 1px solid #e0e0e0; border-radius: 4px; background: #fff; margin-bottom: 8px; }
        #mass-msg-container #exclusion-list { max-height: 60px; overflow-y: auto; }
        #mass-msg-exclusion-list { display: none; }

    `);

    // --- Module 1: Drawaria Friend Chat Downloader ---
    (function() {
        // --- 0. CONSTANTS AND CONFIGURATION ---
        const CHAT_SELECTORS = {
            CHAT_CONTAINER: 'div#friends-tabmessages-list',
            CHAT_HEADER: 'div#friends-tabmessages-header',
            MESSAGE_ELEMENT: '.message',
            SENDER_NAME: '.sender-name, .username',
            MESSAGE_CONTENT: '.message-content, .message-text, .text-content, .msg-text',
            MESSAGE_TIMESTAMP: '.message-timestamp, .timestamp, .msg-time, small'
        };

        const SCROLL_LOAD_MAX_ATTEMPTS = 30;
        const SCROLL_LOAD_PAUSE_MS = 250;

        // --- 1. HTML FOR THE MENU ---
        const chatDownloaderMenuHTML = `


            <div id="chat-downloader-container">
                <div id="chat-downloader-header">💬 Drawaria Friend Chat Downloader</div>
                <div id="chat-downloader-toggle">▼</div>
                <div id="chat-downloader-body">
                    <div id="chat-download-section">
                        <p>Abre el chat con la persona deseada antes de usar esta función.</p>

                        <label for="chat-downloader-export-format">Formato de Exportación:</label>
                        <select id="chat-downloader-export-format">
                            <option value="txt">Texto Plano (.txt)</option>
                            <option value="json">JSON (.json)</option>
                            <option value="csv">CSV (.csv)</option>
                        </select>

                        <label for="chat-downloader-timestamp-format">Formato de Fecha/Hora:</label>
                        <select id="chat-downloader-timestamp-format">
                            <option value="full">Fecha y Hora Completa (ej. 7/12/25, 2:55:30 PM)</option>
                            <option value="time">Solo Hora (ej. 2:55:30 PM)</option>
                            <option value="date">Solo Fecha (ej. 7/12/25)</option>
                            <option value="iso">ISO 8601 (ej. 2025-07-12T14:55:30.000Z)</option>
                        </select>

                        <label for="chat-downloader-message-detail-format">Detalle del Mensaje:</label>
                        <select id="chat-downloader-message-detail-format">
                            <option value="full_detail">Fecha, Remitente y Mensaje (ej. [Fecha] Nombre: Mensaje)</option>
                            <option value="no_timestamp">Remitente y Mensaje (ej. Nombre: Mensaje)</option>
                            <option value="content_only">Solo Mensaje (ej. Mensaje)</option>
                        </select>

                        <label>Filtrar por Fecha:</label>
                        <div class="input-group">
                            <div>
                                <label for="chat-downloader-start-date">Desde:</label>
                                <input type="date" id="chat-downloader-start-date">
                            </div>
                            <div>
                                <label for="chat-downloader-end-date">Hasta:</label>
                                <input type="date" id="chat-downloader-end-date">
                            </div>
                        </div>

                        <div class="button-group">
                            <button id="chat-downloader-download-chat-btn">Descargar</button>
                            <button id="chat-downloader-copy-chat-btn">Copiar al Portapapeles</button>
                        </div>

                        <div id="chat-downloader-progress-indicator">Cargando mensajes...</div>
                    </div>
                    <div id="chat-downloader-log">
                        <button id="chat-downloader-log-clear">Limpiar</button>
                        Esperando instrucciones...
                    </div>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', chatDownloaderMenuHTML);



        // --- 2. DEFINITION OF VARIABLES AND UI ELEMENTS ---
        const chatDownloader_ui = {
            container: document.getElementById('chat-downloader-container'),
            header: document.getElementById('chat-downloader-header'),
            toggleButton: document.getElementById('chat-downloader-toggle'),
            body: document.getElementById('chat-downloader-body'),
            logPanel: document.getElementById('chat-downloader-log'),
            logClearButton: document.getElementById('chat-downloader-log-clear'),
            downloadChatButton: document.getElementById('chat-downloader-download-chat-btn'),
            copyChatButton: document.getElementById('chat-downloader-copy-chat-btn'),
            exportFormatSelect: document.getElementById('chat-downloader-export-format'),
            timestampFormatSelect: document.getElementById('chat-downloader-timestamp-format'),
            messageDetailFormatSelect: document.getElementById('chat-downloader-message-detail-format'),
            startDateInput: document.getElementById('chat-downloader-start-date'),
            endDateInput: document.getElementById('chat-downloader-end-date'),
            progressIndicator: document.getElementById('chat-downloader-progress-indicator'),
        };

        // --- 3. HELPER FUNCTIONS ---

        /**
         * Writes a message to the script's log panel.
         * @param {string} message The message to log.
         * @param {string} type The message type (e.g., 'info', 'success', 'error').
         */
        function chatDownloader_logToPanel(message, type = 'info') {
            const timestamp = new Date().toLocaleTimeString();
            chatDownloader_ui.logPanel.insertAdjacentHTML('beforeend', `<div class="log-${type}">[${timestamp}] ${message}</div>`);
            chatDownloader_ui.logPanel.scrollTop = chatDownloader_ui.logPanel.scrollHeight;
        }

        /**
         * Collapses or expands the script menu and saves the state.
         * @param {boolean} collapsed Whether the menu should be collapsed.
         */
        function chatDownloader_setMenuCollapsed(collapsed) {
            if (collapsed) {
                chatDownloader_ui.container.classList.add('collapsed');
                chatDownloader_ui.toggleButton.textContent = '▲';
            } else {
                chatDownloader_ui.container.classList.remove('collapsed');
                chatDownloader_ui.toggleButton.textContent = '▼';
            }
            GM_setValue('chatDownloader_menuCollapsed', collapsed);
        }

        /**
         * Enables/disables action buttons and shows/hides the progress indicator.
         * @param {boolean} disabled Whether buttons should be disabled.
         */
        function chatDownloader_toggleButtonsAndProgress(disabled) {
            chatDownloader_ui.downloadChatButton.disabled = disabled;
            chatDownloader_ui.copyChatButton.disabled = disabled;
            chatDownloader_ui.progressIndicator.style.display = disabled ? 'block' : 'none';
        }

        /**
         * Allows an element to be dragged by a handle. Saves its position.
         * @param {HTMLElement} elmnt The element that can be dragged.
         * @param {HTMLElement} dragHandle The element that acts as the drag handle.
         */
        function chatDownloader_dragElement(elmnt, dragHandle) {
            let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

            // Load saved position
            const savedTop = GM_getValue('chatDownloader_menuTop', '10px');
            const savedLeft = GM_getValue('chatDownloader_menuLeft', '10px');
            elmnt.style.top = savedTop;
            elmnt.style.left = savedLeft;

            dragHandle.onmousedown = dragMouseDown;

            function dragMouseDown(e) {
                e.preventDefault();
                pos3 = e.clientX;
                pos4 = e.clientY;
                document.body.classList.add('chat-downloader-dragging');
                document.onmouseup = closeDragElement;
                document.onmousemove = elementDrag;
            }

            function elementDrag(e) {
                e.preventDefault();
                pos1 = pos3 - e.clientX;
                pos2 = pos4 - e.clientY;
                pos3 = e.clientX;
                pos4 = e.clientY;
                elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
                elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
            }

            function closeDragElement() {
                document.body.classList.remove('chat-downloader-dragging');
                document.onmouseup = null;
                document.onmousemove = null;
                // Save current position
                GM_setValue('chatDownloader_menuTop', elmnt.style.top);
                GM_setValue('chatDownloader_menuLeft', elmnt.style.left);
            }
        }

        /**
         * Attempts to extract the clean date/time part from a string that may contain other text.
         * This is crucial if the `MESSAGE_TIMESTAMP` selector sometimes captures more than just the date.
         * @param {string} fullText The full text string of the timestamp element.
         * @returns {string} The part of the string that is likely the date/time or the original string.
         */
        function chatDownloader_extractCleanTimestampPart(fullText) {
            const match = fullText.match(/(\d{1,2}\/\d{1,2}\/\d{2,4}, \d{1,2}:\d{2}(?::\d{2})? (?:AM|PM))/i);
            if (match && match[1]) {
                return match[1];
            }
            return fullText;
        }

        /**
         * Parses a timestamp string or number (epoch) into a Date object.
         * @param {string|number} rawTimestamp The timestamp string or epoch number.
         * @returns {Date|null} A Date object or null if it cannot be parsed.
         */
        function chatDownloader_parseTimestampToDate(rawTimestamp) {
            if (rawTimestamp instanceof Date) {
                return rawTimestamp;
            }
            if (typeof rawTimestamp === 'number') {
                const date = new Date(rawTimestamp);
                return date;
            }
            if (typeof rawTimestamp === 'string') {
                const cleanedTimestamp = chatDownloader_extractCleanTimestampPart(rawTimestamp);

                let dateObj = new Date(cleanedTimestamp);
                if (!isNaN(dateObj.getTime())) {
                    return dateObj;
                }

                const parts = cleanedTimestamp.match(/(\d{1,2})\/(\d{1,2})\/(\d{2,4}), (\d{1,2}):(\d{2})(?::(\d{2}))? (AM|PM)/i);
                if (parts) {
                    let [_, month, day, year, hour, minute, second, ampm] = parts;
                    let fullYear = parseInt(year, 10);
                    if (fullYear < 100) {
                        fullYear += (fullYear > (new Date().getFullYear() % 100) + 1 ? 1900 : 2000);
                    }
                    let h = parseInt(hour, 10);
                    if (ampm.toUpperCase() === 'PM' && h < 12) h += 12;
                    if (ampm.toUpperCase() === 'AM' && h === 12) h = 0;
                    const s = second ? parseInt(second, 10) : 0;

                    dateObj = new Date(fullYear, parseInt(month, 10) - 1, parseInt(day, 10), h, parseInt(minute, 10), s, 0);
                    if (!isNaN(dateObj.getTime())) {
                        return dateObj;
                    }
                }

                dateObj = new Date(Date.parse(cleanedTimestamp));
                if (!isNaN(dateObj.getTime())) {
                    return dateObj;
                }
            }
            return null;
        }

        /**
         * Formats a Date object according to the user's selected format.
         * If dateObj is invalid, it uses the current system time.
         * @param {Date} dateObj The Date object to format.
         * @param {string} format 'full', 'time', 'date', 'iso'.
         * @returns {string} The formatted timestamp.
         */
        function chatDownloader_formatTimestamp(dateObj, format) {
            let dateToFormat = dateObj;

            if (!dateObj || isNaN(dateObj.getTime())) {
                dateToFormat = new Date();
                console.warn(`[chatDownloader_formatTimestamp] Invalid date object received. Using current time: ${dateToFormat.toISOString()}`);
            }

            if (format === 'iso') {
                return dateToFormat.toISOString();
            }

            let options = {};
            const locale = 'es-ES';

            switch (format) {
                case 'full':
                    options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
                    break;
                case 'time':
                    options = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
                    break;
                case 'date':
                    options = { year: 'numeric', month: '2-digit', day: '2-digit' };
                    break;
                default:
                    options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
                    console.warn(`[chatDownloader_formatTimestamp] Unknown format "${format}". Using default full format.`);
                    break;
            }

            try {
                return dateToFormat.toLocaleString(locale, options);
            } catch (e) {
                console.error("Error formatting date with toLocaleString:", e);
                return dateToFormat.toISOString();
            }
        }

        /**
         * Robustly extracts the friend's name from the chat header.
         * @returns {string} The friend's name or 'UnknownFriend' if not found.
         */
        function chatDownloader_getFriendName() {
            const headerElement = document.querySelector(CHAT_SELECTORS.CHAT_HEADER);
            if (!headerElement) {
                return 'UnknownFriend';
            }

            const nameEl = headerElement.querySelector('.username, .playername');
            if (nameEl && nameEl.textContent.trim()) {
                return nameEl.textContent.trim();
            }

            const messagesTitle = headerElement.textContent.trim();
            if (messagesTitle.includes('Messages')) {
                const parts = messagesTitle.split(' ').filter(part => part.toLowerCase() !== 'messages' && part.trim() !== '');
                if (parts.length > 0) {
                    return parts.join(' ');
                }
            }

            const headerTextNodes = Array.from(headerElement.childNodes)
                                     .filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0)
                                     .map(node => node.textContent.trim());
            const filteredText = headerTextNodes.filter(text => text.toLowerCase() !== 'messages').join(' ');
            if (filteredText) {
                return filteredText;
            }

            return 'UnknownFriend';
        }

        /**
         * Scrolls the chat container up to load all history.
         * @param {HTMLElement} chatContainer The scrollable chat element.
         */
        async function chatDownloader_scrollToLoadAllMessages(chatContainer) {
            chatDownloader_logToPanel('Intentando cargar todo el historial de chat...', 'info');
            chatDownloader_toggleButtonsAndProgress(true);

            let previousScrollHeight = 0;
            let attempts = 0;

            while (attempts < SCROLL_LOAD_MAX_ATTEMPTS) {
                chatContainer.scrollTop = 0;
                await new Promise(resolve => setTimeout(resolve, SCROLL_LOAD_PAUSE_MS));

                const currentScrollHeight = chatContainer.scrollHeight;

                if (currentScrollHeight === previousScrollHeight) {
                    chatDownloader_logToPanel(`Historial cargado. ${attempts + 1} intentos de scroll.`, 'info');
                    break;
                } else {
                    previousScrollHeight = currentScrollHeight;
                    attempts++;
                    chatDownloader_logToPanel(`Cargando... Altura de scroll: ${currentScrollHeight}px`, 'info');
                }
            }

            if (attempts >= SCROLL_LOAD_MAX_ATTEMPTS) {
                chatDownloader_logToPanel('Advertencia: El historial de chat podría no estar completamente cargado (límite de intentos alcanzado).', 'error');
            }
        }

        /**
         * Collects and processes all chat messages, applying date filters.
         * @returns {Array<Object>} An array of message objects.
         */
        async function chatDownloader_getFilteredChatMessages() {
            const chatContainer = document.querySelector(CHAT_SELECTORS.CHAT_CONTAINER);
            if (!chatContainer) {
                chatDownloader_logToPanel('Error: No se encontró la ventana de chat activa. Asegúrate de tener una conversación abierta.', 'error');
                return [];
            }

            await chatDownloader_scrollToLoadAllMessages(chatContainer);

            const messagesElements = chatContainer.querySelectorAll(CHAT_SELECTORS.MESSAGE_ELEMENT);
            if (messagesElements.length === 0) {
                chatDownloader_logToPanel('No se encontraron mensajes en la conversación. Asegúrate de tener un historial de chat visible.', 'error');
                return [];
            }

            const friendName = chatDownloader_getFriendName();

            const startDateStr = chatDownloader_ui.startDateInput.value;
            const endDateStr = chatDownloader_ui.endDateInput.value;

            let filterStartDate = null;
            let filterEndDate = null;

            if (startDateStr) {
                filterStartDate = new Date(startDateStr);
                filterStartDate.setHours(0, 0, 0, 0);
                if (isNaN(filterStartDate.getTime())) {
                    chatDownloader_logToPanel('Advertencia: Fecha de inicio inválida. Ignorando filtro de inicio.', 'error');
                    filterStartDate = null;
                }
            }
            if (endDateStr) {
                filterEndDate = new Date(endDateStr);
                filterEndDate.setHours(23, 59, 59, 999);
                if (isNaN(filterEndDate.getTime())) {
                    chatDownloader_logToPanel('Advertencia: Fecha de fin inválida. Ignorando filtro de fin.', 'error');
                    filterEndDate = null;
                }
            }

            const collectedMessages = [];

            messagesElements.forEach(msgEl => {
                let sender = 'Desconocido';
                let content = '';
                let rawTimestamp = '';

                const timestampEl = msgEl.querySelector(CHAT_SELECTORS.MESSAGE_TIMESTAMP);
                if (timestampEl) {
                    rawTimestamp = timestampEl.textContent.trim();
                } else {
                    const dateMeta = msgEl.querySelector('[data-timestamp], [title]');
                    if (dateMeta && dateMeta.dataset.timestamp) {
                        rawTimestamp = parseInt(dateMeta.dataset.timestamp, 10);
                    } else if (dateMeta && dateMeta.title) {
                        const titleMatch = dateMeta.title.match(/(\d{1,2}\/\d{1,2}\/\d{2,4}, \d{1,2}:\d{2}(?::\d{2})? (?:AM|PM))/i);
                        if (titleMatch && titleMatch[1]) {
                            rawTimestamp = titleMatch[1];
                        } else {
                            rawTimestamp = dateMeta.title;
                        }
                    }
                }
                const messageDate = chatDownloader_parseTimestampToDate(rawTimestamp);

                if (messageDate) {
                    if (filterStartDate && messageDate < filterStartDate) {
                        return;
                    }
                    if (filterEndDate && messageDate > filterEndDate) {
                        return;
                    }
                } else {
                    if (filterStartDate || filterEndDate) {
                        chatDownloader_logToPanel(`Advertencia: Mensaje omitido porque no se pudo parsear la fecha/hora para el filtro: "${rawTimestamp}".`, 'info');
                        return;
                    }
                }

                const senderEl = msgEl.querySelector(CHAT_SELECTORS.SENDER_NAME);
                if (senderEl && senderEl.textContent.trim()) {
                    sender = senderEl.textContent.trim();
                } else if (msgEl.classList.contains('fromself')) {
                    sender = 'Yo';
                } else {
                    sender = friendName;
                }

                const contentEl = msgEl.querySelector(CHAT_SELECTORS.MESSAGE_CONTENT);
                if (contentEl && contentEl.textContent.trim()) {
                    content = contentEl.textContent.trim();
                } else {
                    content = Array.from(msgEl.childNodes)
                                .filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0)
                                .map(node => node.textContent.trim())
                                .join(' ');
                    if (!content && msgEl.children.length > 0) {
                        const relevantChild = Array.from(msgEl.children)
                                                    .find(child => !child.matches(CHAT_SELECTORS.MESSAGE_TIMESTAMP) && child.textContent.trim().length > 0);
                        if (relevantChild) {
                            content = relevantChild.textContent.trim();
                        }
                    }
                }

                collectedMessages.push({
                    date: messageDate,
                    sender: sender,
                    content: content
                });
            });

            chatDownloader_logToPanel(`Se recolectaron ${collectedMessages.length} mensajes después de aplicar filtros.`, 'success');
            return collectedMessages;
        }

        /**
         * Generates chat content in plain text format.
         * @param {Array<Object>} messages The messages to export.
         * @param {string} friendName Friend's name.
         * @param {string} timestampFormat Date/time format.
         * @param {string} messageDetailFormat Message detail format.
         * @returns {string} The text file content.
         */
        function chatDownloader_exportChatAsText(messages, friendName, timestampFormat, messageDetailFormat) {
            let chatText = `--- Conversación con ${friendName} ---\n\n`;
            messages.forEach(msg => {
                let line = '';
                const formattedTimestamp = chatDownloader_formatTimestamp(msg.date, timestampFormat);

                if (messageDetailFormat === 'content_only') {
                    line = `${msg.content}\n`;
                } else if (messageDetailFormat === 'no_timestamp') {
                    line = `${msg.sender}: ${msg.content}\n`;
                } else {
                    line = `[${formattedTimestamp}] ${msg.sender}: ${msg.content}\n`;
                }
                chatText += line;
            });
            return chatText;
        }

        /**
         * Generates chat content in JSON format.
         * @param {Array<Object>} messages The messages to export.
         * @param {string} friendName Friend's name.
         * @param {string} timestampFormat Date/time format.
         * @param {string} messageDetailFormat Message detail format.
         * @returns {string} The JSON content.
         */
        function chatDownloader_exportChatAsJson(messages, friendName, timestampFormat, messageDetailFormat) {
            const data = {
                friend: friendName,
                exportedAt: new Date().toISOString(),
                messages: messages.map(msg => {
                    const messageObject = {};
                    if (messageDetailFormat === 'full_detail') {
                        messageObject.timestamp = chatDownloader_formatTimestamp(msg.date, timestampFormat === 'iso' ? 'iso' : 'full');
                        messageObject.sender = msg.sender;
                    } else if (messageDetailFormat === 'no_timestamp') {
                        messageObject.sender = msg.sender;
                    }
                    messageObject.content = msg.content;
                    return messageObject;
                })
            };
            return JSON.stringify(data, null, 2);
        }

        /**
         * Generates chat content in CSV format.
         * @param {Array<Object>} messages The messages to export.
         * @param {string} friendName Friend's name.
         * @param {string} timestampFormat Date/time format.
         * @param {string} messageDetailFormat Message detail format.
         * @returns {string} The CSV content.
         */
        function chatDownloader_exportChatAsCsv(messages, friendName, timestampFormat, messageDetailFormat) {
            const headers = [];
            if (messageDetailFormat === 'full_detail') {
                headers.push("Timestamp", "Sender", "Content");
            } else if (messageDetailFormat === 'no_timestamp') {
                headers.push("Sender", "Content");
            } else {
                headers.push("Content");
            }

            let csv = headers.join(",") + "\n";

            messages.forEach(msg => {
                const row = [];
                const escapeCsv = (str) => `"${String(str).replace(/"/g, '""')}"`;

                if (messageDetailFormat === 'full_detail') {
                    const formattedTimestamp = chatDownloader_formatTimestamp(msg.date, timestampFormat === 'iso' ? 'iso' : 'full');
                    row.push(escapeCsv(formattedTimestamp));
                    row.push(escapeCsv(msg.sender));
                } else if (messageDetailFormat === 'no_timestamp') {
                    row.push(escapeCsv(msg.sender));
                }
                row.push(escapeCsv(msg.content));
                csv += row.join(",") + "\n";
            });
            return csv;
        }

        /**
         * Creates a download link and "clicks" it to initiate file download.
         * @param {string} filename File name.
         * @param {string} content File content.
         * @param {string} mimeType File MIME type.
         * @returns {boolean} True if download started successfully, false otherwise.
         */
        function chatDownloader_createDownloadFile(filename, content, mimeType) {
            const blob = new Blob([content], { type: mimeType });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = filename;

            try {
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(link.href);
                return true;
            } catch (error) {
                chatDownloader_logToPanel(`Error al iniciar la descarga: ${error.message}. Verifique la consola para más detalles.`, 'error');
                console.error('Error during download link creation/click:', error);
                return false;
            }
        }

        /**
         * Main function that orchestrates chat collection, processing, and export/copy.
         * @param {string} action 'download' to download, 'copy' to copy to clipboard.
         */
        async function chatDownloader_handleChatExport(action) {
            chatDownloader_logToPanel('Iniciando exportación de chat...', 'info');
            chatDownloader_toggleButtonsAndProgress(true);

            try {
                const messages = await chatDownloader_getFilteredChatMessages();
                if (messages.length === 0) {
                    chatDownloader_logToPanel('No hay mensajes para exportar después de aplicar filtros.', 'error');
                    return;
                }

                const friendName = chatDownloader_getFriendName();
                const exportFormat = chatDownloader_ui.exportFormatSelect.value;
                const timestampFormat = chatDownloader_ui.timestampFormatSelect.value;
                const messageDetailFormat = chatDownloader_ui.messageDetailFormatSelect.value;

                let fileContent = '';
                let fileExtension = '';
                let mimeType = '';

                switch (exportFormat) {
                    case 'txt':
                        fileContent = chatDownloader_exportChatAsText(messages, friendName, timestampFormat, messageDetailFormat);
                        fileExtension = 'txt';
                        mimeType = 'text/plain;charset=utf-8';
                        break;
                    case 'json':
                        fileContent = chatDownloader_exportChatAsJson(messages, friendName, timestampFormat, messageDetailFormat);
                        fileExtension = 'json';
                        mimeType = 'application/json;charset=utf-8';
                        break;
                    case 'csv':
                        fileContent = chatDownloader_exportChatAsCsv(messages, friendName, timestampFormat, messageDetailFormat);
                        fileExtension = 'csv';
                        mimeType = 'text/csv;charset=utf-8';
                        break;
                    default:
                        chatDownloader_logToPanel('Error: Formato de exportación no reconocido.', 'error');
                        return;
                }

                if (action === 'download') {
                    const filename = `Drawaria_Chat_${friendName.replace(/[^a-zA-Z0-9_.-]/g, '')}_${new Date().toISOString().slice(0, 10)}.${fileExtension}`;
                    if (chatDownloader_createDownloadFile(filename, fileContent, mimeType)) {
                        chatDownloader_logToPanel(`Conversación con ${friendName} descargada como "${filename}".`, 'success');
                    }
                } else if (action === 'copy') {
                    try {
                        GM_setClipboard(fileContent, mimeType);
                        chatDownloader_logToPanel(`Contenido del chat (${exportFormat.toUpperCase()}) copiado al portapapeles.`, 'success');
                    } catch (clipboardError) {
                        chatDownloader_logToPanel(`Error al copiar al portapapeles: ${clipboardError.message}. Asegúrate de que Tampermonkey tenga permiso para acceder al portapapeles (grant GM_setClipboard).`, 'error');
                        console.error('Error copying to clipboard:', clipboardError);
                    }
                }

            } catch (error) {
                chatDownloader_logToPanel(`Error general al exportar chat: ${error.message}.`, 'error');
                console.error('Error exporting chat:', error);
            } finally {
                chatDownloader_toggleButtonsAndProgress(false);
            }
        }

        // --- 4. SCRIPT INITIALIZATION ---
        (function chatDownloader_init() {
            // Load saved state of the menu (collapsed/expanded)
            const isCollapsed = GM_getValue('chatDownloader_menuCollapsed', false);
            chatDownloader_setMenuCollapsed(isCollapsed);

            // Load saved preferences
            chatDownloader_ui.exportFormatSelect.value = GM_getValue('chatDownloader_exportFormat', 'txt');
            chatDownloader_ui.timestampFormatSelect.value = GM_getValue('chatDownloader_timestampFormat', 'full');
            chatDownloader_ui.messageDetailFormatSelect.value = GM_getValue('chatDownloader_messageDetailFormat', 'full_detail');
            chatDownloader_ui.startDateInput.value = GM_getValue('chatDownloader_startDate', '');
            chatDownloader_ui.endDateInput.value = GM_getValue('chatDownloader_endDate', '');

            // Assign events to UI elements
            chatDownloader_ui.toggleButton.addEventListener('click', () => {
                chatDownloader_setMenuCollapsed(chatDownloader_ui.container.classList.toggle('collapsed'));
            });

            chatDownloader_ui.downloadChatButton.addEventListener('click', () => chatDownloader_handleChatExport('download'));
            chatDownloader_ui.copyChatButton.addEventListener('click', () => chatDownloader_handleChatExport('copy'));

            chatDownloader_ui.logClearButton.addEventListener('click', () => {
                Array.from(chatDownloader_ui.logPanel.children).forEach(child => {
                    if (child.tagName === 'DIV') {
                        chatDownloader_ui.logPanel.removeChild(child);
                    }
                });
                chatDownloader_logToPanel('Log limpiado.');
            });

            // Save preferences on change
            chatDownloader_ui.exportFormatSelect.addEventListener('change', (e) => {
                GM_setValue('chatDownloader_exportFormat', e.target.value);
                chatDownloader_logToPanel(`Formato de exportación cambiado a: ${e.target.options[e.target.selectedIndex].text}`, 'info');
            });

            chatDownloader_ui.timestampFormatSelect.addEventListener('change', (e) => {
                GM_setValue('chatDownloader_timestampFormat', e.target.value);
                chatDownloader_logToPanel(`Formato de fecha/hora cambiado a: ${e.target.options[e.target.selectedIndex].text}`, 'info');
            });

            chatDownloader_ui.messageDetailFormatSelect.addEventListener('change', (e) => {
                GM_setValue('chatDownloader_messageDetailFormat', e.target.value);
                chatDownloader_logToPanel(`Detalle de mensaje cambiado a: ${e.target.options[e.target.selectedIndex].text}`, 'info');
            });

            chatDownloader_ui.startDateInput.addEventListener('change', (e) => GM_setValue('chatDownloader_startDate', e.target.value));
            chatDownloader_ui.endDateInput.addEventListener('change', (e) => GM_setValue('chatDownloader_endDate', e.target.value));

            // Initialize menu dragging functionality
            chatDownloader_dragElement(chatDownloader_ui.container, chatDownloader_ui.header);
        })();
    })();

    // --- Module 2: Drawaria Message All Friends ---
    (function() {
        let massMsg_isSending = false;
        const MASS_MSG_BATCH_PAUSE_SECONDS = 60;
        const massMsg_profileHelpText = `Saves and loads all your configurations (message, filters, delays, etc.) for easy reuse.\n\n--- HOW TO USE ---\n\n1. SAVE:\n   - Configure everything to your liking.\n   - Enter a name for the profile.\n   - Click on 'Save Current Profile'.\n\n2. LOAD:\n   - Select a profile from the dropdown menu.\n\n3. DELETE:\n   - Load a profile and click the trash can icon (🗑️).`;

        // --- 1. HTML FOR THE MENU ---
        const massMsgMenuHTML = `
            <div id="mass-msg-container">
                <div id="mass-msg-header">✉️ Drawaria Message All Friends</div>

                <div id="mass-msg-toggle">▼</div>
                <div id="mass-msg-body">
                    <label for="mass-msg-text">Message (use {name} to personalize):</label>
                    <textarea id="mass-msg-text" placeholder="Hello, {name}! How are you?"></textarea>
                    <div class="section-header"> <button class="section-toggle" data-target="mass-msg-profile-section">Profile Manager ▼</button> <span id="mass-msg-profile-info-btn" class="info-button">ⓘ</span> </div>
                    <div id="mass-msg-profile-section" class="collapsible-section">
                        <div id="mass-msg-profile-manager"> <select id="mass-msg-profile-select"><option value="">--- Load Profile ---</option></select> <button id="mass-msg-delete-profile-btn" title="Delete Selected Profile">🗑️</button> </div>
                        <input type="text" id="mass-msg-profile-name" placeholder="New profile name..." style="margin-top: 5px;"> <button id="mass-msg-save-profile-btn" style="width:100%; margin-top:5px;">Save Current Profile</button>
                    </div>
<button id="mass-msg-exclusion-toggle">Mostrar/Ocultar lista de exclusión</button>
<div id="mass-msg-exclusion-list" style="display:none;">
          #mass-msg-container .exclusion-item { display: flex; align-items: center; padding: 1px 3px; border-bottom: 1px solid #f8f9fa; font-size: 10px; line-height: 1.1; }
        #mass-msg-container .exclusion-item label { margin: 0 0 0 4px; font-weight: normal; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        #mass-msg-container .exclusion-item input[type="checkbox"] { width: 10px; height: 10px; margin: 0; }
        #mass-msg-container #exclusion-controls { display: flex; gap: 4px; margin: 6px 0; }
</div>
                    <div class="input-grid">
                         <div class="grid-item"><label for="mass-msg-delay">Delay (ms):</label><input type="number" id="mass-msg-delay" value="1500" min="500" step="100"></div>
                         <div class="grid-item"><label for="mass-msg-lang-filter">Send to:</label><select id="mass-msg-lang-filter"><option value="all">All / Todos</option><option value="es">Spanish</option><option value="en">English</option><option value="ru">Russian</option><option value="ar">Arabic</option></select></div>
                         <div class="grid-item"><label for="mass-msg-count-limit">Limit:</label><input type="number" id="mass-msg-count-limit" placeholder="All"></div>
                    </div>
                    <label for="mass-msg-exclusion-search">Skip these people:</label>
                    <div id="mass-msg-exclusion-controls"> <button id="mass-msg-select-all-exclude">All</button> <button id="mass-msg-deselect-all-exclude">None</button> <button id="mass-msg-invert-exclude">Invert</button> </div>
                    <div class="exclusion-container"> <input type="text" id="mass-msg-exclusion-search" placeholder="Search friend to skip..."> <div id="mass-msg-exclusion-list">Open friends list to populate.</div> </div>
                    <div class="section-header"> <button class="section-toggle" data-target="mass-msg-advanced-options">Advanced Options (Batches) ▼</button> </div>
                    <div id="mass-msg-advanced-options" class="collapsible-section">
                        <label for="mass-msg-batch-size">Batch Size (e.g. 50):</label> <input type="number" id="mass-msg-batch-size" placeholder="Leave empty to not use batches"> <small>There will be a 60-second pause between each batch.</small>
                    </div>
                    <div class="button-container"> <button id="mass-msg-start-mass-msg">Send</button> <button id="mass-msg-stop-mass-msg">Stop Sending</button> </div>
                    <div id="mass-msg-progress-container"> <div id="mass-msg-progress-bar"><div id="mass-msg-progress-bar-inner"></div></div> <div id="mass-msg-progress-text"></div> </div>
                    <div id="mass-msg-log">Waiting for instructions...</div>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', massMsgMenuHTML);

        // --- 2. VARIABLE AND UI ELEMENT DEFINITIONS ---
        const massMsg_ui = {
            container: document.getElementById('mass-msg-container'),
            header: document.getElementById('mass-msg-header'),
            toggleButton: document.getElementById('mass-msg-toggle'),
            body: document.getElementById('mass-msg-body'),
            startButton: document.getElementById('mass-msg-start-mass-msg'),
            stopButton: document.getElementById('mass-msg-stop-mass-msg'),
            messageInput: document.getElementById('mass-msg-text'),
            delayInput: document.getElementById('mass-msg-delay'),
            logPanel: document.getElementById('mass-msg-log'),
            langFilter: document.getElementById('mass-msg-lang-filter'),
            countLimit: document.getElementById('mass-msg-count-limit'),
            exclusionList: document.getElementById('mass-msg-exclusion-list'),
            exclusionSearch: document.getElementById('mass-msg-exclusion-search'),
            batchSizeInput: document.getElementById('mass-msg-batch-size'),
            profileSelect: document.getElementById('mass-msg-profile-select'),
            profileNameInput: document.getElementById('mass-msg-profile-name'),
            saveProfileButton: document.getElementById('mass-msg-save-profile-btn'),
            deleteProfileButton: document.getElementById('mass-msg-delete-profile-btn'),
            selectAllButton: document.getElementById('mass-msg-select-all-exclude'),
            deselectAllButton: document.getElementById('mass-msg-deselect-all-exclude'),
            invertButton: document.getElementById('mass-msg-invert-exclude'),
            progressContainer: document.getElementById('mass-msg-progress-container'),
            progressBarInner: document.getElementById('mass-msg-progress-bar-inner'),
            progressText: document.getElementById('mass-msg-progress-text'),
            profileInfoButton: document.getElementById('mass-msg-profile-info-btn'),
        };

document.getElementById('mass-msg-exclusion-toggle').addEventListener('click', function() {
    var list = document.getElementById('mass-msg-exclusion-list');
    if (list.style.display === 'none' || list.style.display === '') {
        list.style.display = 'block';
        this.textContent = 'Ocultar lista de exclusión';
    } else {
        list.style.display = 'none';
        this.textContent = 'Mostrar lista de exclusión';
    }
});


        // --- 3. FUNCTION DEFINITIONS ---
        function massMsg_logToPanel(message, type = 'info') {
            const timestamp = new Date().toLocaleTimeString();
            massMsg_ui.logPanel.innerHTML += `<div class="log-${type}">[${timestamp}] ${message}</div>`;
            massMsg_ui.logPanel.scrollTop = massMsg_ui.logPanel.scrollHeight;
        }

        function massMsg_setMenuCollapsed(collapsed) {
            if (collapsed) {
                massMsg_ui.container.classList.add('collapsed');
                massMsg_ui.toggleButton.textContent = '▲';
            } else {
                massMsg_ui.container.classList.remove('collapsed');
                massMsg_ui.toggleButton.textContent = '▼';
            }
            GM_setValue('massMsg_menuCollapsed', collapsed);
        }

        function massMsg_saveCurrentSettings() {
            return {
                message: massMsg_ui.messageInput.value,
                delay: massMsg_ui.delayInput.value,
                lang: massMsg_ui.langFilter.value,
                limit: massMsg_ui.countLimit.value,
                batchSize: massMsg_ui.batchSizeInput.value,
            };
        }

        function massMsg_loadSettings(settings) {
            massMsg_ui.messageInput.value = settings.message || '';
            massMsg_ui.delayInput.value = settings.delay || 1500;
            massMsg_ui.langFilter.value = settings.lang || 'all';
            massMsg_ui.countLimit.value = settings.limit || '';
            massMsg_ui.batchSizeInput.value = settings.batchSize || '';
        }

        function massMsg_populateProfileDropdown() {
            const profiles = GM_getValue('massMsg_savedProfiles', {});
            massMsg_ui.profileSelect.innerHTML = '<option value="">--- Load Profile ---</option>';
            for (const name in profiles) {
                const option = document.createElement('option');
                option.value = name;
                option.textContent = name;
                massMsg_ui.profileSelect.appendChild(option);
            }
        }

        function massMsg_setAllCheckboxes(checked) {
            massMsg_ui.exclusionList.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = checked);
        }

        function massMsg_stopSending(finished = false) {
            if (finished) {
                massMsg_logToPanel('--- Process finished! ---', 'success');
            }
            massMsg_isSending = false;
            massMsg_ui.startButton.style.display = 'block';
            massMsg_ui.stopButton.style.display = 'none';
            massMsg_ui.startButton.disabled = false;
            massMsg_ui.progressContainer.style.display = 'none';
        }

        function massMsg_filterByLanguage(elements, lang) {
            if (lang === 'all') return elements;
            const patterns = {
                es: /[ñáéíóúü¡¿]/i,
                ru: /[а-яА-Я]/,
                ar: /[\u0600-\u06FF]/,
                en: /^[a-zA-Z0-9\s!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]*$/
            };
            const otherLangsPattern = /([ñáéíóúü¡¿]|[а-яА-Я]|[\u0600-\u06FF])/i;

            return elements.filter(el => {
                const username = el.querySelector('.playername')?.textContent || '';
                return lang === 'en' ? !otherLangsPattern.test(username) : (patterns[lang] && patterns[lang].test(username));
            });
        }

        function massMsg_populateExclusionList() {
            const friendElements = document.querySelectorAll('#friends-tabfriendlist .content .tabrow');
            if (friendElements.length === 0) return;
            massMsg_ui.exclusionList.innerHTML = '';
            friendElements.forEach((el, index) => {
                const uid = el.dataset.playeruid;
                const name = el.querySelector('.playername')?.textContent || uid;
                massMsg_ui.exclusionList.insertAdjacentHTML('beforeend', `<div class="exclusion-item" data-name="${name.toLowerCase()}"><input type="checkbox" id="mass-msg-exclude-${index}" data-uid="${uid}"><label for="mass-msg-exclude-${index}">${name}</label></div>`);
            });
        }

        function massMsg_updateProgress(current, total) {
            const percentage = total > 0 ? (current / total) * 100 : 0;
            massMsg_ui.progressBarInner.style.width = `${percentage}%`;
            const etaText = massMsg_calculateETA(current, total);
            massMsg_ui.progressText.textContent = `Sent ${current} / ${total} ${etaText}`;
        }

        function massMsg_formatTime(seconds) {
            if (seconds < 60) return `${Math.round(seconds)}s`;
            const minutes = Math.floor(seconds / 60);
            const remainingSeconds = Math.round(seconds % 60);
            return `${minutes}m ${remainingSeconds}s`;
        }

        function massMsg_calculateETA(current, total) {
            if (current >= total) return '';
            const remaining = total - current;
            const delay = (parseInt(massMsg_ui.delayInput.value, 10) || 1500) / 1000;
            const batchSize = parseInt(massMsg_ui.batchSizeInput.value, 10) || 0;
            let estimatedSeconds = remaining * delay;
            if (batchSize > 0) {
                const batchesLeft = Math.floor((total - 1) / batchSize) - Math.floor((current - 1) / batchSize);
                estimatedSeconds += batchesLeft * MASS_MSG_BATCH_PAUSE_SECONDS;
            }
            return `| ETA: ~${massMsg_formatTime(estimatedSeconds)}`;
        }

        function massMsg_applyFilters(elements) {
            massMsg_logToPanel("Applying filters...");
            const lang = massMsg_ui.langFilter.value;
            const friendsByLang = massMsg_filterByLanguage(elements, lang);
            massMsg_logToPanel(`Filtered by language '${lang}': ${friendsByLang.length} friends.`);
            const excludedUIDs = Array.from(massMsg_ui.exclusionList.querySelectorAll('input:checked')).map(cb => cb.dataset.uid);
            const friendsAfterExclusion = friendsByLang.filter(el => !excludedUIDs.includes(el.dataset.playeruid));
            massMsg_logToPanel(`After exclusion: ${friendsAfterExclusion.length} friends to send.`);
            const limit = parseInt(massMsg_ui.countLimit.value, 10);
            const finalFriendList = (limit > 0) ? friendsAfterExclusion.slice(0, limit) : friendsAfterExclusion;
            if (limit > 0) massMsg_logToPanel(`Limit applied: Sending to the first ${finalFriendList.length}.`);
            return finalFriendList;
        }

        function massMsg_dragElement(elmnt, dragHandle) {
            let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

            // Load saved position
            const savedTop = GM_getValue('massMsg_menuTop', '10px');
            const savedLeft = GM_getValue('massMsg_menuLeft', '380px'); // Adjusted initial position
            elmnt.style.top = savedTop;
            elmnt.style.left = savedLeft;

            dragHandle.onmousedown = dragMouseDown;

            function dragMouseDown(e) {
                e.preventDefault();
                pos3 = e.clientX;
                pos4 = e.clientY;
                document.body.classList.add('mass-msg-dragging');
                document.onmouseup = closeDragElement;
                document.onmousemove = elementDrag;
            }

            function elementDrag(e) {
                e.preventDefault();
                pos1 = pos3 - e.clientX;
                pos2 = pos4 - e.clientY;
                pos3 = e.clientX;
                pos4 = e.clientY;
                elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
                elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
            }

            function closeDragElement() {
                document.body.classList.remove('mass-msg-dragging');
                document.onmouseup = null;
                document.onmousemove = null;
                // Save current position
                GM_setValue('massMsg_menuTop', elmnt.style.top);
                GM_setValue('massMsg_menuLeft', elmnt.style.left);
            }
        }

        async function massMsg_startSending() {
            if (!massMsg_ui.messageInput.value.trim()) {
                alert('Message cannot be empty.');
                return;
            }
            massMsg_isSending = true;
            massMsg_ui.logPanel.innerHTML = '';
            massMsg_logToPanel('--- Starting script ---');
            massMsg_ui.startButton.style.display = 'none';
            massMsg_ui.stopButton.style.display = 'block';
            massMsg_ui.startButton.disabled = true;
            massMsg_ui.progressContainer.style.display = 'block';

            const allFriends = Array.from(document.querySelectorAll('#friends-tabfriendlist .content .tabrow'));
            if (allFriends.length === 0) {
                massMsg_logToPanel('Error: Friends list not found or empty. Please open your friends list in Drawaria.', 'error');
                massMsg_stopSending(false);
                return;
            }

            const finalFriendList = massMsg_applyFilters(allFriends);
            const totalToSend = finalFriendList.length;
            massMsg_updateProgress(0, totalToSend);

            const batchSize = parseInt(massMsg_ui.batchSizeInput.value, 10) || 0;

            for (let i = 0; i < totalToSend; i++) {
                if (!massMsg_isSending) {
                    massMsg_logToPanel('Sending stopped by user.', 'error');
                    break;
                }
                if (batchSize > 0 && i > 0 && i % batchSize === 0) {
                    massMsg_logToPanel(`Batch completed. Pausing for ${MASS_MSG_BATCH_PAUSE_SECONDS} seconds...`, 'pause');
                    for (let s = 0; s < MASS_MSG_BATCH_PAUSE_SECONDS; s++) {
                        if (!massMsg_isSending) break;
                        await new Promise(resolve => setTimeout(resolve, 1000));
                    }
                    if (!massMsg_isSending) {
                        massMsg_logToPanel('Sending stopped during pause.', 'error');
                        break;
                    }
                    massMsg_logToPanel(`Resuming sending...`, 'pause');
                }

                const friendElement = finalFriendList[i];
                const uid = friendElement.dataset.playeruid;
                const name = friendElement.querySelector('.playername')?.textContent || uid;
                const delay = parseInt(massMsg_ui.delayInput.value, 10) || 1500;
                const personalizedMessage = massMsg_ui.messageInput.value.replace(/{name}/g, name);

                massMsg_logToPanel(`(${i + 1}/${totalToSend}) Sending to: ${name}`, 'info');
                try {
                    await $.post("/friendsapi/sendmessage", { uid, message: personalizedMessage });
                    massMsg_logToPanel(`✔ Message sent to ${name}`, 'success');
                } catch (error) {
                    massMsg_logToPanel(`✖ Failed to send to ${name}.`, 'error');
                    console.error(`Error sending to ${name} (UID: ${uid})`, error);
                }
                massMsg_updateProgress(i + 1, totalToSend);

                if (massMsg_isSending && i < totalToSend - 1 && !(batchSize > 0 && (i + 1) % batchSize === 0)) {
                    await new Promise(resolve => setTimeout(resolve, delay));
                }
            }
            massMsg_stopSending(true);
        }

        // --- 4. INITIALIZATION ---
        (function massMsg_init() {
            // Load saved state
            const isCollapsed = GM_getValue('massMsg_menuCollapsed', false);
            massMsg_setMenuCollapsed(isCollapsed);
            massMsg_ui.messageInput.value = GM_getValue('massMsg_savedMessage', 'Hello, {name}!');
            massMsg_ui.delayInput.value = GM_getValue('massMsg_savedDelay', 1500);
            massMsg_ui.langFilter.value = GM_getValue('massMsg_savedLang', 'all');
            massMsg_ui.countLimit.value = GM_getValue('massMsg_savedCount', '');
            massMsg_ui.batchSizeInput.value = GM_getValue('massMsg_savedBatchSize', '');
            massMsg_populateProfileDropdown();

            // Assign events
            massMsg_ui.toggleButton.addEventListener('click', () => {
                const currentlyCollapsed = massMsg_ui.container.classList.toggle('collapsed');
                massMsg_setMenuCollapsed(currentlyCollapsed);
            });

            document.querySelectorAll('#mass-msg-container .section-toggle').forEach(button => {
                button.addEventListener('click', () => {
                    const target = document.getElementById(button.dataset.target);
                    const isVisible = target.style.display === 'block';
                    target.style.display = isVisible ? 'none' : 'block';
                    button.textContent = button.textContent.includes('▼') ? button.textContent.replace('▼', '▲') : button.textContent.replace('▲', '▼');
                });
            });

            massMsg_ui.messageInput.addEventListener('input', () => GM_setValue('massMsg_savedMessage', massMsg_ui.messageInput.value));
            massMsg_ui.delayInput.addEventListener('input', () => GM_setValue('massMsg_savedDelay', massMsg_ui.delayInput.value));
            massMsg_ui.langFilter.addEventListener('change', () => GM_setValue('massMsg_savedLang', massMsg_ui.langFilter.value));
            massMsg_ui.countLimit.addEventListener('input', () => GM_setValue('massMsg_savedCount', massMsg_ui.countLimit.value));
            massMsg_ui.batchSizeInput.addEventListener('input', () => GM_setValue('massMsg_savedBatchSize', massMsg_ui.batchSizeInput.value));
            massMsg_ui.profileInfoButton.addEventListener('click', () => alert(massMsg_profileHelpText));

            massMsg_ui.saveProfileButton.addEventListener('click', () => {
                const name = massMsg_ui.profileNameInput.value.trim();
                if (!name) {
                    alert('Please enter a profile name.');
                    return;
                }
                const profiles = GM_getValue('massMsg_savedProfiles', {});
                profiles[name] = massMsg_saveCurrentSettings();
                GM_setValue('massMsg_savedProfiles', profiles);
                massMsg_ui.profileNameInput.value = '';
                massMsg_populateProfileDropdown();
                alert(`Profile '${name}' saved.`);
            });

            massMsg_ui.profileSelect.addEventListener('change', () => {
                const name = massMsg_ui.profileSelect.value;
                if (!name) return;
                const profiles = GM_getValue('massMsg_savedProfiles', {});
                if (profiles[name]) {
                    massMsg_loadSettings(profiles[name]);
                    massMsg_logToPanel(`Profile '${name}' loaded.`);
                }
            });

            massMsg_ui.deleteProfileButton.addEventListener('click', () => {
                const name = massMsg_ui.profileSelect.value;
                if (!name) {
                    alert('Select a profile to delete.');
                    return;
                }
                if (confirm(`Are you sure you want to delete profile '${name}'?`)) {
                    const profiles = GM_getValue('massMsg_savedProfiles', {});
                    delete profiles[name];
                    GM_setValue('massMsg_savedProfiles', profiles);
                    massMsg_populateProfileDropdown();
                    alert(`Profile '${name}' deleted.`);
                }
            });

            massMsg_ui.selectAllButton.addEventListener('click', () => massMsg_setAllCheckboxes(true));
            massMsg_ui.deselectAllButton.addEventListener('click', () => massMsg_setAllCheckboxes(false));
            massMsg_ui.invertButton.addEventListener('click', () => {
                massMsg_ui.exclusionList.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = !cb.checked);
            });

            massMsg_ui.exclusionSearch.addEventListener('input', e => {
                const searchTerm = e.target.value.toLowerCase();
                massMsg_ui.exclusionList.querySelectorAll('.exclusion-item').forEach(item => {
                    item.style.display = item.dataset.name.includes(searchTerm) ? 'flex' : 'none';
                });
            });

            massMsg_ui.startButton.addEventListener('click', massMsg_startSending);
            massMsg_ui.stopButton.addEventListener('click', () => { massMsg_isSending = false; });

            // DOM observer for the friends list
            const observer = new MutationObserver(() => {
                if (document.querySelector('#friends-tabfriendlist .content .tabrow')) {
                    massMsg_populateExclusionList();
                }
            });
            const friendsWg = document.getElementById('friends-wg');
            if (friendsWg) {
                observer.observe(friendsWg, { childList: true, subtree: true });
            }

            // Initialize menu dragging
            massMsg_dragElement(massMsg_ui.container, massMsg_ui.header);
        })();
    })();
})();