Torn Copy Chat Button [Chat2.0]

Export chat messages from Torn

// ==UserScript==
// @name         Torn Copy Chat Button [Chat2.0]
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Export chat messages from Torn
// @author       Latinobull14[2881384]
// @match        https://www.torn.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // Disable debugging
  const DEBUG = false;

  function debugLog(...args) {
    if (DEBUG) {
      console.log('[Chat Copy Button]', ...args);
    }
  }

  // Helper function to find elements by partial class name
  function getElementByPartialClassName(parent, partialClassName) {
    if (!parent) {
      debugLog(
        'Parent element is null or undefined in getElementByPartialClassName'
      );
      return null;
    }

    try {
      const elements = parent.querySelectorAll('*');
      for (let i = 0; i < elements.length; i++) {
        // Check if element has classList (safer than className)
        if (elements[i].classList && elements[i].classList.length > 0) {
          // Convert classList to array and check each class
          const classArray = Array.from(elements[i].classList);
          for (let j = 0; j < classArray.length; j++) {
            if (classArray[j].includes(partialClassName)) {
              return elements[i];
            }
          }
        } else if (typeof elements[i].className === 'string') {
          // Fallback to className if it's a string
          const classes = elements[i].className.split(' ');
          for (let j = 0; j < classes.length; j++) {
            if (classes[j].includes(partialClassName)) {
              return elements[i];
            }
          }
        }
      }
    } catch (error) {
      debugLog('Error in getElementByPartialClassName:', error);
    }
    return null;
  }

  // Function to find all elements by partial class name
  function getElementsByPartialClassName(parent, partialClassName) {
    if (!parent) {
      debugLog(
        'Parent element is null or undefined in getElementsByPartialClassName'
      );
      return [];
    }

    const result = [];
    try {
      const elements = parent.querySelectorAll('*');
      for (let i = 0; i < elements.length; i++) {
        // Check if element has classList (safer than className)
        if (elements[i].classList && elements[i].classList.length > 0) {
          // Convert classList to array and check each class
          const classArray = Array.from(elements[i].classList);
          for (let j = 0; j < classArray.length; j++) {
            if (classArray[j].includes(partialClassName)) {
              result.push(elements[i]);
              break;
            }
          }
        } else if (typeof elements[i].className === 'string') {
          // Fallback to className if it's a string
          const classes = elements[i].className.split(' ');
          for (let j = 0; j < classes.length; j++) {
            if (classes[j].includes(partialClassName)) {
              result.push(elements[i]);
              break;
            }
          }
        }
      }
    } catch (error) {
      debugLog('Error in getElementsByPartialClassName:', error);
    }
    return result;
  }

  // Function to safely get class name from an element
  function getSafeClassName(element) {
    if (!element) return '';

    try {
      if (element.classList && element.classList.length > 0) {
        return Array.from(element.classList).join(' ');
      } else if (typeof element.className === 'string') {
        return element.className;
      }
    } catch (error) {
      debugLog('Error in getSafeClassName:', error);
    }
    return '';
  }

  // Function to find the chat box body directly
  function findChatBoxBody(chatBoxHeader) {
    try {
      // First, find the parent chat box container
      let chatBox = null;
      let currentElement = chatBoxHeader;

      // Go up the DOM tree to find the chat box container
      while (currentElement && !chatBox) {
        if (getSafeClassName(currentElement).includes('chat-box___')) {
          chatBox = currentElement;
          break;
        }
        currentElement = currentElement.parentElement;
      }

      if (!chatBox) {
        return null;
      }

      // Look for a div with style="height: X" which typically contains the chat body
      const heightDivs = chatBox.querySelectorAll('div[style*="height"]');
      if (heightDivs.length > 0) {
        // The first child of this div is usually the chat-box-body
        for (let i = 0; i < heightDivs.length; i++) {
          const children = heightDivs[i].children;
          for (let j = 0; j < children.length; j++) {
            if (getSafeClassName(children[j]).includes('chat-box-body')) {
              return children[j];
            }
          }

          // If we didn't find it in the children, check if the height div itself contains messages
          const messages = getElementsByPartialClassName(
            heightDivs[i],
            'chat-box-message'
          );
          if (messages.length > 0) {
            return heightDivs[i];
          }
        }
      }

      // Direct search for chat-box-body
      const chatBoxBody = getElementByPartialClassName(
        chatBox,
        'chat-box-body'
      );
      if (chatBoxBody) {
        return chatBoxBody;
      }

      return null;
    } catch (error) {
      debugLog('Error in findChatBoxBody:', error);
      return null;
    }
  }

  // Function to extract and format chat messages based on the actual HTML structure
  function extractChatMessages(chatBoxBody) {
    if (!chatBoxBody) {
      return '';
    }

    let formattedChat = '';
    // Keep track of processed messages to avoid duplicates
    const processedMessages = new Set();
    // Keep track of the current timestamp
    let currentTimestamp = '';
    // Keep track of the last sender
    let lastSender = '';
    // Keep track of the last timestamp-sender combination
    let lastTimestampSender = '';

    try {
      // Find all direct children of the chat box body - these are message groups
      const messageGroups = chatBoxBody.children;

      // Process each message group
      for (let i = 0; i < messageGroups.length; i++) {
        const group = messageGroups[i];

        // Skip the last message timestamp if present
        if (getSafeClassName(group).includes('lastmessage-timestamp')) {
          continue;
        }

        // Check if this group has a timestamp
        const timestampDiv = getElementByPartialClassName(
          group,
          'chat-box-message__timestamp'
        );
        if (timestampDiv) {
          const timestampP = timestampDiv.querySelector('p');
          if (timestampP) {
            currentTimestamp = timestampP.textContent.trim();
          }
        }

        // Find all message divs in this group
        const messageDivs = getElementsByPartialClassName(
          group,
          'chat-box-message'
        );

        // Process each message
        messageDivs.forEach(messageDiv => {
          // Find the sender element
          const senderElement = getElementByPartialClassName(
            messageDiv,
            'chat-box-message__sender'
          );

          // Find the message text element
          const messageElement = getElementByPartialClassName(
            messageDiv,
            'text-message'
          );

          if (senderElement && messageElement) {
            // Extract sender name (remove the colon)
            const senderName = senderElement.textContent
              .trim()
              .replace(':', '');

            // Extract message text and clean it up
            let messageText = messageElement.textContent;

            // Clean up the message text - remove extra whitespace and normalize line breaks
            messageText = messageText.replace(/\s+/g, ' ').trim();

            // Create a unique key for this message to detect duplicates
            const messageKey = `${senderName}-${messageText}`;

            // Skip if we've already processed this message
            if (processedMessages.has(messageKey)) {
              return;
            }

            // Mark this message as processed
            processedMessages.add(messageKey);

            // Create the current timestamp-sender combination
            const currentTimestampSender = `${currentTimestamp} ${senderName}`;

            // Add to formatted chat with optimized formatting
            if (currentTimestamp) {
              if (currentTimestampSender === lastTimestampSender) {
                // Same sender and timestamp - just add the message without header
                formattedChat += `${messageText}\n`;
              } else {
                // New sender or timestamp - add full header with blank line if not the first message
                if (formattedChat !== '') {
                  formattedChat += '\n';
                }
                formattedChat += `${currentTimestamp} ${senderName}:\n${messageText}\n`;
                // Update the last timestamp-sender combination
                lastTimestampSender = currentTimestampSender;
              }
            } else {
              // No timestamp case
              if (senderName === lastSender) {
                // Same sender - just add the message without sender name
                formattedChat += `${messageText}\n`;
              } else {
                // New sender - add sender name with blank line if not the first message
                if (formattedChat !== '') {
                  formattedChat += '\n';
                }
                formattedChat += `${senderName}:\n${messageText}\n`;
                // Update the last sender
                lastSender = senderName;
              }
            }
          }
        });
      }
    } catch (error) {
      debugLog('Error in extractChatMessages:', error);
    }

    return formattedChat;
  }

  // Function to show notification
  function showNotification(message, isError = false, button = null) {
    try {
      // Remove any existing notifications
      const existingNotifications = document.querySelectorAll(
        '.chat-copy-notification'
      );
      existingNotifications.forEach(notification => {
        if (notification.parentNode) {
          document.body.removeChild(notification);
        }
      });

      // Create notification container
      const notification = document.createElement('div');
      notification.className = 'chat-copy-notification';
      notification.style.position = 'fixed';
      notification.style.top = '20px'; // Changed from bottom to top
      notification.style.right = '20px';
      notification.style.backgroundColor = isError
        ? 'rgba(200, 0, 0, 0.9)'
        : 'rgba(0, 128, 0, 0.9)';
      notification.style.color = 'white';
      notification.style.padding = '12px 20px';
      notification.style.borderRadius = '4px';
      notification.style.zIndex = '9999';
      notification.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)';
      notification.style.display = 'flex';
      notification.style.alignItems = 'center';
      notification.style.fontFamily = 'Arial, sans-serif';
      notification.style.fontSize = '14px';

      // Add icon based on success/error
      const icon = document.createElement('div');
      icon.style.marginRight = '10px';
      icon.style.fontSize = '18px';
      icon.textContent = isError ? '❌' : '✅';
      notification.appendChild(icon);

      // Add message
      const messageElement = document.createElement('div');
      messageElement.textContent = message;
      notification.appendChild(messageElement);

      // If a button was provided, highlight it briefly
      if (button) {
        const originalColor = button.style.backgroundColor;
        button.style.backgroundColor = isError
          ? 'rgba(255, 0, 0, 0.3)'
          : 'rgba(0, 255, 0, 0.3)';
        setTimeout(() => {
          button.style.backgroundColor = originalColor;
        }, 500);
      }

      document.body.appendChild(notification);

      // Add close button
      const closeButton = document.createElement('div');
      closeButton.textContent = '×';
      closeButton.style.marginLeft = '15px';
      closeButton.style.cursor = 'pointer';
      closeButton.style.fontSize = '18px';
      closeButton.style.fontWeight = 'bold';
      closeButton.addEventListener('click', () => {
        if (notification.parentNode) {
          document.body.removeChild(notification);
        }
      });
      notification.appendChild(closeButton);

      // Remove the notification after 3 seconds
      setTimeout(() => {
        if (notification.parentNode) {
          // Fade out animation
          notification.style.transition = 'opacity 0.5s';
          notification.style.opacity = '0';
          setTimeout(() => {
            if (notification.parentNode) {
              document.body.removeChild(notification);
            }
          }, 500);
        }
      }, 3000);
    } catch (error) {
      debugLog('Error showing notification:', error);
    }
  }

  // Function to copy text to clipboard
  function copyToClipboard(text, button) {
    if (!text) {
      showNotification('No chat messages found to copy', true, button);
      return;
    }

    try {
      // Use the newer Clipboard API if available
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard
          .writeText(text)
          .then(() => {
            showNotification('Chat copied to clipboard!', false, button);
          })
          .catch(err => {
            // Fall back to the older method
            copyToClipboardFallback(text, button);
          });
      } else {
        // Use the older method for browsers that don't support the Clipboard API
        copyToClipboardFallback(text, button);
      }
    } catch (error) {
      debugLog('Error in copyToClipboard:', error);
      showNotification('Failed to copy: ' + error.message, true, button);
    }
  }

  // Fallback method for copying to clipboard
  function copyToClipboardFallback(text, button) {
    try {
      // Create a temporary textarea element
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', '');
      textarea.style.position = 'absolute';
      textarea.style.left = '-9999px';
      document.body.appendChild(textarea);

      // Select and copy the text
      textarea.select();
      const success = document.execCommand('copy');

      // Remove the temporary element
      document.body.removeChild(textarea);

      if (success) {
        showNotification('Chat copied to clipboard!', false, button);
      } else {
        showNotification('Failed to copy chat. Try again.', true, button);
      }
    } catch (error) {
      debugLog('Error in copyToClipboardFallback:', error);
      showNotification('Failed to copy: ' + error.message, true, button);
    }
  }

  // Function to add the custom button to a chat box header
  function addCustomButton(chatBoxHeader) {
    try {
      // Find the actions container in the header using partial class name
      const actionsContainer = getElementByPartialClassName(
        chatBoxHeader,
        'chat-box-header__actions'
      );
      if (!actionsContainer) {
        return;
      }

      // Check if our button already exists to avoid duplicates
      if (actionsContainer.querySelector('.custom-chat-button')) {
        return;
      }

      // Get the class name of the existing action wrapper buttons for consistent styling
      const existingButtons = actionsContainer.children;
      let actionWrapperClass = '';
      if (existingButtons.length > 0) {
        // Extract the full class name from an existing button
        actionWrapperClass = getSafeClassName(existingButtons[0]);
      } else {
        // Fallback class if no existing buttons are found
        actionWrapperClass =
          'chat-box-header__action-wrapper custom-chat-button';
      }

      // Create the new button with the same styling as the existing buttons
      const customButton = document.createElement('button');
      customButton.type = 'button';
      customButton.className = actionWrapperClass + ' custom-chat-button';
      customButton.title = 'Copy Chat';

      // Create the SVG icon for the button (clipboard icon)
      customButton.innerHTML = `
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="custom-chat-icon">
                  <defs>
                      <linearGradient id="custom_button_gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
                          <stop offset="0" stop-color="#868686"></stop>
                          <stop offset="1" stop-color="#656565"></stop>
                      </linearGradient>
                  </defs>
                  <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" 
                        fill="url(#custom_button_gradient)"></path>
              </svg>
          `;

      // Add click event listener
      customButton.addEventListener('click', function (event) {
        event.stopPropagation(); // Prevent the chat header click event from firing

        try {
          // Find the chat box body directly from the header
          const chatBoxBody = findChatBoxBody(chatBoxHeader);

          if (!chatBoxBody) {
            showNotification(
              'Error: Could not find chat messages',
              true,
              customButton
            );
            return;
          }

          // Extract and format chat messages
          const formattedChat = extractChatMessages(chatBoxBody);

          if (!formattedChat) {
            showNotification(
              'No chat messages found to copy',
              true,
              customButton
            );
            return;
          }

          // Copy to clipboard
          copyToClipboard(formattedChat, customButton);
        } catch (error) {
          debugLog('Error in button click handler:', error);
          showNotification('Error: ' + error.message, true, customButton);
        }
      });

      // Insert the button at the beginning of the actions container (left side)
      actionsContainer.insertBefore(customButton, actionsContainer.firstChild);
    } catch (error) {
      debugLog('Error in addCustomButton:', error);
    }
  }

  // Function to process all chat boxes in the document
  function processChatBoxes() {
    try {
      // Find all chat box headers
      const chatBoxHeaders = getElementsByPartialClassName(
        document,
        'chat-box-header'
      );

      // Add buttons to each header
      chatBoxHeaders.forEach(addCustomButton);

      return chatBoxHeaders.length > 0;
    } catch (error) {
      debugLog('Error in processChatBoxes:', error);
      return false;
    }
  }

  // Function to check for chat containers and monitor for changes
  function monitorChatContainers() {
    try {
      // Find all potential chat container elements
      const chatContainers = [
        ...document.querySelectorAll('.group-chat-box___HzH0r'),
        ...document.querySelectorAll('[class*="chat-app"]'),
        ...document.querySelectorAll('[class*="chat-list"]'),
        ...document.querySelectorAll('[class*="chat-box-wrapper"]'),
      ];

      // Set up mutation observers for each container
      chatContainers.forEach(container => {
        // Skip if we've already set up an observer for this container
        if (container.dataset.chatObserverSet === 'true') {
          return;
        }

        // Mark this container as having an observer
        container.dataset.chatObserverSet = 'true';

        // Create a mutation observer for this container
        const observer = new MutationObserver(function (mutations) {
          // Process any new chat boxes that might have appeared
          processChatBoxes();
        });

        // Start observing the container
        observer.observe(container, {
          childList: true,
          subtree: true,
        });
      });

      // Also observe the body for new chat containers that might appear
      if (!document.body.dataset.chatContainerObserverSet) {
        document.body.dataset.chatContainerObserverSet = 'true';

        const bodyObserver = new MutationObserver(function (mutations) {
          // Check if any new chat containers have been added
          const foundNewContainers = mutations.some(mutation => {
            return Array.from(mutation.addedNodes).some(node => {
              if (node.nodeType !== Node.ELEMENT_NODE) return false;

              // Check if this is a chat container or contains one
              const isChatContainer =
                node.className &&
                (node.className.includes('group-chat-box') ||
                  node.className.includes('chat-app') ||
                  node.className.includes('chat-list') ||
                  node.className.includes('chat-box-wrapper'));

              // If it's a container, set up an observer for it
              if (isChatContainer && node.dataset.chatObserverSet !== 'true') {
                node.dataset.chatObserverSet = 'true';

                const containerObserver = new MutationObserver(() => {
                  processChatBoxes();
                });

                containerObserver.observe(node, {
                  childList: true,
                  subtree: true,
                });

                return true;
              }

              // Check if it contains chat containers
              const containsContainers =
                node.querySelector('.group-chat-box___HzH0r') ||
                node.querySelector('[class*="chat-app"]') ||
                node.querySelector('[class*="chat-list"]') ||
                node.querySelector('[class*="chat-box-wrapper"]');

              if (containsContainers) {
                // Run the monitor function again to set up observers for these new containers
                setTimeout(monitorChatContainers, 100);
                return true;
              }

              return false;
            });
          });

          // If we found new containers, process chat boxes
          if (foundNewContainers) {
            processChatBoxes();
          }
        });

        bodyObserver.observe(document.body, {
          childList: true,
          subtree: true,
        });
      }
    } catch (error) {
      debugLog('Error in monitorChatContainers:', error);
    }
  }

  // Function to initialize the script
  function initialize() {
    try {
      // First, process any existing chat boxes
      const foundExistingChats = processChatBoxes();

      // Set up monitoring for chat containers
      monitorChatContainers();

      // Set up a periodic check for new chat boxes
      setInterval(() => {
        processChatBoxes();
      }, 2000); // Check every 2 seconds

      return foundExistingChats;
    } catch (error) {
      debugLog('Error in initialize:', error);
      return false;
    }
  }

  // Wait for the page to load completely
  window.addEventListener('load', function () {
    // Start the script after a short delay to ensure all elements are loaded
    setTimeout(initialize, 1000);
  });

  // Also run on document ready for faster initialization
  if (
    document.readyState === 'complete' ||
    document.readyState === 'interactive'
  ) {
    setTimeout(initialize, 1000);
  }
})();