您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); })(); })(); })();