FishTank chat filter

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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});
    })
})();