FishTank chat filter

Removes duplicate messages, messages that have too many repetitious strings, and item usages.

// ==UserScript==
// @name         FishTank chat filter
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Removes duplicate messages, messages that have too many repetitious strings, and item usages.
// @author       Stan
// @match        https://www.fishtank.live/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fishtank.live
// @grant        none
// @license MIT
// ==/UserScript==

function waitForElement(selector, callback) {
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                const element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    callback(element);
                    break
                }
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
}

function cleanupMessage(message) {
    return message.toLowerCase().trim().replace(/[^a-zA-Z0-9\s]/g, '');
}

// Returns true if any word is repeated more than `maxRepeats` times.
function containsRepetitiousWords(message, maxRepeats = 4) {
    const words = message.split(' ');
    const wordCounts = {};
    // Check for repeated words
    for (const word of words) {
        wordCounts[word] = (wordCounts[word] || 0) + 1;
        if (wordCounts[word] > maxRepeats)
            return true;
    }
    return false;
}

// Returns true if any sub-string is repeated more than `maxRepeats` times.
function containsRepetitiousStrings(message, maxRepeats = 10) {
    for (let i = 0; i < message.length - 1; i++) {
        for (let j = i + 1; j < message.length; j++) {
            const substring = message.substring(i, j + 1);
            if (message.split(substring).length - 1 > maxRepeats)
                return true;
        }
    }
    return false;
}

const previousMessageByUserMap = {}; // Stores last message per user

// Returns true if the previous message and current message's unique words overlap more than `maxSimilarity`.
function isTooSimilarToPreviousMessage(message, user, maxSimilarity = 80) {
    const previousMessage = previousMessageByUserMap[user];
    if (!previousMessage)
        return false;
    const currentWords = new Set(message.split());
    const previousWords = new Set(previousMessage.split());
    const intersection = new Set([...currentWords].filter(word => previousWords.has(word)));
    const similarity = (intersection.size / Math.max(currentWords.size, previousWords.size)) * 100;
    return similarity > maxSimilarity;
}

// Hides the message
function remove(messageDiv) {
    messageDiv.style.display = "none";
}

function messageFilter(mutations) {
    const myUserId = document
        .querySelector(".top-bar-user_display-name__bzlpw")
        .getAttribute('data-user-id')

    mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE) {
                const messageDiv = node.querySelector('.chat-message-default_message__milmT');
                if (messageDiv) {
                    const userId = node.getAttribute('data-user-id');
                    if (userId !== myUserId) {
                        const messageText = cleanupMessage(messageDiv.textContent);
                        const wordSpam = containsRepetitiousWords(messageText)
                        const gibberish = containsRepetitiousStrings(messageText)
                        const repeat = isTooSimilarToPreviousMessage(messageText, userId);
                        if (wordSpam || gibberish || repeat) {
                            remove(node);
                            console.log(`Filtered message(spam="${wordSpam}", gibberish="${gibberish}", repeat="${repeat}"): "${messageText}" from ${userId || 'Unknown User'}`);
                        } else {
                            console.log('Setting previous message for "${userId}" to "${messageText}');
                            previousMessageByUserMap[userId] = messageText; // Update last message for user
                        }
                    }
                } else {
                    if (node.classList.contains("chat-message-happening_chat-message-happening__tYeDU")) {
                        remove(node)
                        console.log("Filtered out item usage spam")
                    }
                }
            }
        });
    });
}

(function () {
    'use strict';
    waitForElement("#chat-messages", chatContainer => {
        console.log("Detected chat-container, registering message filter.")
        let observer = new MutationObserver(messageFilter);
        observer.observe(chatContainer, { childList: true});
    })
})();