Stumblechat Stalker

Intercept WebSocket messages on StumbleChat and display them in tabs, sorted by sender's handle, with minimize and resize functionality

目前为 2024-11-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         Stumblechat Stalker
// @namespace    http://tampermonkey.net/
// @version      1.04
// @description  Intercept WebSocket messages on StumbleChat and display them in tabs, sorted by sender's handle, with minimize and resize functionality
// @author       MeKLiN
// @match        https://stumblechat.com/room/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=stumblechat.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let userMap = {}; // Store user data with handle as key and { username, nick } as value
    let overlayVisible = true; // Flag to track overlay visibility

    // Create a function to update the userMap based on join/quit events
    function updateUserMap(message) {
        if (message.stumble === "joined" || message.stumble === "join") {
            message.userlist.forEach(user => {
                userMap[user.handle] = {
                    username: user.username || "Unknown",
                    nick: user.nick || "Unknown"
                };
            });
        } else if (message.stumble === "quit") {
            // Remove the user from the map when they quit
            delete userMap[message.handle];
        }
    }

    // Function to display WebSocket message under the appropriate tab
    function displayWebSocketMessage(message, tab) {
        const tabDiv = document.getElementById(tab);
        if (tabDiv) {
            // Group by sender's handle
            let senderDiv = document.getElementById(message.handle);
            if (!senderDiv) {
                senderDiv = document.createElement("div");
                senderDiv.id = message.handle;

                // Retrieve the username from the user map
                const user = userMap[message.handle] || { username: "Unknown", nick: "Unknown" };

                // Create the user's tab button
                const userTabButton = document.createElement("button");
                userTabButton.innerText = `${user.username} (${message.handle} / ${user.nick})`; // Username, handle, and nick
                userTabButton.onclick = () => {
                    // Toggle visibility of the user's message div
                    if (senderDiv.style.display === "none") {
                        senderDiv.style.display = "block";
                    } else {
                        senderDiv.style.display = "none";
                    }
                };

                // Add the button to the tab
                tabDiv.appendChild(userTabButton);

                // Create the user's message container
                const senderHeader = document.createElement("h4");
                senderHeader.innerText = `${user.username} (${message.handle} / ${user.nick})`;
                senderDiv.appendChild(senderHeader);
                senderDiv.style.display = "none"; // Initially hidden

                // Add the message container to the tab
                tabDiv.appendChild(senderDiv);
            }

            const messageDiv = document.createElement("div");
            messageDiv.innerHTML = message.text; // Show the message content
            senderDiv.appendChild(messageDiv);
            tabDiv.scrollTop = tabDiv.scrollHeight; // Auto-scroll
        }
    }

    // Override WebSocket constructor to intercept WebSocket creation
    const originalWebSocket = window.WebSocket;
    window.WebSocket = function(url, protocols) {
        console.log('WebSocket URL:', url);

        // Call original WebSocket constructor
        const ws = new originalWebSocket(url, protocols);

        // Event listener for receiving messages
        ws.addEventListener('message', event => {
            try {
                const message = JSON.parse(event.data);

                // Update userMap when users join or quit
                if (message.stumble === "join" || message.stumble === "joined" || message.stumble === "quit") {
                    updateUserMap(message);
                }

                // Filter messages by the "stumble" type
                switch (message.stumble) {
                    case "join":
                    case "joined":
                        displayWebSocketMessage(message, "joinTab");
                        break;
                    case "msg":
                        displayWebSocketMessage(message, "msgTab");
                        break;
                    default:
                        console.log("Unhandled message type:", message.stumble);
                        break;
                }
            } catch (error) {
                console.error("Error processing WebSocket message:", error);
            }
        });

        return ws;
    };

    // Method to create the overlay container with tabs
    function createWebSocketTabs() {
        const overlayDiv = document.createElement("div");
        overlayDiv.id = "overlayContainer";
        overlayDiv.style.position = "fixed";
        overlayDiv.style.top = "0";
        overlayDiv.style.left = "0";
        overlayDiv.style.zIndex = "9999";
        overlayDiv.style.backgroundColor = "#222";
        overlayDiv.style.color = "#fff";
        overlayDiv.style.padding = "10px";
        overlayDiv.style.borderRadius = "8px";
        overlayDiv.style.maxWidth = "400px";
        overlayDiv.style.maxHeight = "80vh";
        overlayDiv.style.overflowY = "auto";
        overlayDiv.style.display = "flex";
        overlayDiv.style.flexDirection = "column";
        overlayDiv.style.justifyContent = "flex-start";
        overlayDiv.style.resize = "both";
        overlayDiv.style.overflow = "hidden";
        overlayDiv.style.visibility = "visible"; // Ensures the overlay is visible initially

        // Create a collapse/restore button
        const toggleButton = document.createElement("button");
        toggleButton.innerHTML = "Collapse/Expand";
        toggleButton.style.marginBottom = "10px";
        toggleButton.style.padding = "8px";
        toggleButton.style.cursor = "pointer";
        toggleButton.style.zIndex = "10000"; // Set z-index to 10000
        toggleButton.onclick = () => {
            // Toggle the overlay visibility based on the flag
            if (overlayVisible) {
                overlayDiv.style.display = "none"; // Hide overlay
                overlayVisible = false;
            } else {
                overlayDiv.style.display = "flex"; // Show overlay
                overlayVisible = true;
            }
        };
        overlayDiv.appendChild(toggleButton);

        // Create Tabs
        const tabNames = ["join", "msg"];
        tabNames.forEach(tab => {
            const tabButton = document.createElement("button");
            tabButton.innerHTML = tab.charAt(0).toUpperCase() + tab.slice(1);
            tabButton.style.backgroundColor = "#444";
            tabButton.style.border = "none";
            tabButton.style.padding = "10px";
            tabButton.style.margin = "2px";
            tabButton.style.cursor = "pointer";
            tabButton.onclick = () => {
                showTab(tab); // Show the clicked tab
            };
            overlayDiv.appendChild(tabButton);
        });

        // Create divs for each tab
        const tabContentDivs = ["join", "msg"];
        tabContentDivs.forEach(tab => {
            const tabDiv = document.createElement("div");
            tabDiv.id = tab + "Tab";
            tabDiv.style.display = "none";
            tabDiv.style.padding = "10px";
            tabDiv.style.border = "1px solid #555";
            tabDiv.style.borderRadius = "5px";
            tabDiv.style.marginTop = "5px";
            tabDiv.style.maxHeight = "60vh";
            tabDiv.style.overflowY = "auto";
            overlayDiv.appendChild(tabDiv);
        });

        // Append the overlay to the body
        document.body.appendChild(overlayDiv);

        // Function to show the selected tab
        function showTab(tab) {
            tabContentDivs.forEach(tabName => {
                const tabDiv = document.getElementById(tabName + "Tab");
                tabDiv.style.display = tabName === tab ? "block" : "none";
            });
        }

        // Default to showing the "join" tab
        showTab("join");
    }

    // Call the functions to create the WebSocket tabs
    createWebSocketTabs();
})();