Stumblechat Stalker

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

// ==UserScript==
// @name         Stumblechat Stalker
// @namespace    http://tampermonkey.net/
// @version      1.072
// @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 overlayVisible = true; // Flag to track overlay visibility
    let userMap = {}; // Maps handles to user info (username, nick)
    let selfHandle = null; // Store the handle of the current user

    // Function to update userMap based on "joined", "join", or "quit" events
    function updateUserMap(message) {
        if (message.stumble === "join" || message.stumble === "joined") {
            const { handle, username, nick, userlist } = message;

            if (userlist) {
                userlist.forEach(user => {
                    if (user.handle && user.username) {
                        userMap[user.handle] = { username: user.username, nick: user.nick };
                    }
                });
            }

            if (handle && username) {
                userMap[handle] = { username, nick };
            }

            if (handle && !selfHandle) {
                selfHandle = handle; // Store the user's handle if it's the first "joined"
            }
        } else if (message.stumble === "quit") {
            const { handle } = message;
            if (handle) {
                delete userMap[handle];
            }
        }
    }

    // Function to display the user list in the joinTab
    function displayUserList() {
        const joinTab = document.getElementById('joinTab');
        joinTab.innerHTML = ''; // Clear previous content

        Object.keys(userMap).forEach(handle => {
            const user = userMap[handle];
            const userButton = document.createElement('button');
            userButton.innerText = `${user.username} (${handle} / ${user.nick})`;
            joinTab.appendChild(userButton);
        });
    }

    const originalWebSocket = window.WebSocket;
    window.WebSocket = function(url, protocols) {
        const ws = new originalWebSocket(url, protocols);
        ws.addEventListener('message', event => {
            try {
                const message = JSON.parse(event.data);

                if (["join", "joined", "quit"].includes(message.stumble)) {
                    updateUserMap(message);
                    displayUserList();
                } else if (message.stumble === "msg") {
                    displayWebSocketMessage(message); // Handle message event
                }
            } catch (error) {
                console.error("Error processing WebSocket message:", error);
            }
        });
        return ws;
    };

    // Create the overlay container
    const overlayDiv = document.createElement("div");
    overlayDiv.id = "overlayContainer";
    Object.assign(overlayDiv.style, {
        position: "fixed",
        top: "0",
        left: "0",
        zIndex: "9999",
        backgroundColor: "#222",
        color: "#fff",
        padding: "10px",
        borderRadius: "8px",
        maxWidth: "400px",
        maxHeight: "80vh",
        overflowY: "auto",
        display: "flex",
        flexDirection: "column",
        resize: "both",
        visibility: "visible"
    });

    // Create the toggle button (separate from the overlay)
    const toggleButton = document.createElement("button");
    toggleButton.innerHTML = "+";
    Object.assign(toggleButton.style, {
        position: "fixed",
        top: "0px",
        left: "0px",
        zIndex: "10001", // Ensure it's on top of everything
        marginBottom: "10px",
        padding: "8px",
        cursor: "pointer",
    });
    toggleButton.onclick = () => {
        overlayDiv.style.display = overlayVisible ? "none" : "flex";
        overlayVisible = !overlayVisible;
        toggleButton.innerHTML = overlayVisible ? "-" : "+";
    };
    document.body.appendChild(toggleButton); // Add button to the body

    // Add the overlay content (tabs)
    const tabNames = ["msg", "join"];  // Reordered 'msg' to be first
    tabNames.forEach(tab => {
        const tabButton = document.createElement("button");
        tabButton.innerHTML = tab.charAt(0).toUpperCase() + tab.slice(1);
        Object.assign(tabButton.style, { backgroundColor: "#444", border: "none", padding: "10px", margin: "2px", cursor: "pointer" });
        tabButton.onclick = () => showTab(tab);
        overlayDiv.appendChild(tabButton);
    });

    // Add the tab content
    const tabContentDivs = ["msg", "join"];  // Reordered 'msg' to be first
    tabContentDivs.forEach(tab => {
        const tabDiv = document.createElement("div");
        tabDiv.id = `${tab}Tab`;
        Object.assign(tabDiv.style, { display: "none", padding: "10px", border: "1px solid #555", borderRadius: "5px", marginTop: "5px", maxHeight: "60vh", overflowY: "auto" });
        overlayDiv.appendChild(tabDiv);
    });

    document.body.appendChild(overlayDiv);

    // Function to display WebSocket messages in the "msg" tab
    function displayWebSocketMessage(message) {
        const { handle, text } = message;
        const username = userMap[handle] ? userMap[handle].username : handle;  // Use handle if user not in map

        // Display the message in the "msg" tab
        const msgTab = document.getElementById("msgTab");
        const messageDiv = document.createElement("div");
        messageDiv.textContent = `${username}: ${text}`;
        msgTab.appendChild(messageDiv);

        // Scroll to the bottom of the messages div
        msgTab.scrollTop = msgTab.scrollHeight;
    }

    // Function to show the selected tab (join or msg)
    function showTab(tab) {
        tabContentDivs.forEach(tabName => {
            document.getElementById(`${tabName}Tab`).style.display = tabName === tab ? "block" : "none";
        });
    }

    showTab("join");  // Initially show the "join" tab
})();