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.

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