Cleanup YouTube live chat for spam

Cleans up youtube live chat for simple spam - Repeated messages, overused emojis

目前為 2021-05-31 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Cleanup YouTube live chat for spam
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Cleans up youtube live chat for simple spam - Repeated messages, overused emojis
// @author       Zallist
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    var originalFetch = window.fetch;

    let hyperCleanup = {
        options: {
            // How many milliseconds back to check for spam
            spamTime: 360000,

            // How many times can the message be repeated before it looks like spam
            spamAllowedAmount: 2,

            // Strip spam messages that are the same but just have extra emojis
            spamAllowedAmountStrippingEmojis: 3,

            // Just strip out all-emoji messages?
            removeEmojiDuplicates: true
        },
        cache: {},
        messageCountSince: (messageText, time) => 0,
        logMessage: (messageText, message) => { },
        isSpamMessage: (message) => false,
        cleanJson: (json) => json
    };

    // Prepare ahead of time so it compiles
    hyperCleanup.stripChars = /[^\w\u007F-\uFFFF]/g;
    hyperCleanup.getMessageSymbol = (messageText) => {
        // Ignore case & remove spaces
        // We could PROBABLY also just strip out punctuation
        messageText = messageText.toLowerCase();
        messageText = messageText.replace(hyperCleanup.stripChars, '');

        return Symbol.for( messageText );
    };
    hyperCleanup.messageCountSince = (messageText, time) => {
        let count = 0;
        let symbol = hyperCleanup.getMessageSymbol(messageText);
        let cache = hyperCleanup.cache[symbol];

        if (cache) {
            for (let i = cache.length - 1; i >= 0; i--) {
                if (cache[i] >= time) {
                    count++;
                }
                else if (cache[i] < time) {
                    // We can just delete it since we shouldn't care about it again
                    cache.splice(i, 1);
                }
            }
        }

        return count;
    };
    hyperCleanup.logMessage = (messageText, message) => {
        let symbol = hyperCleanup.getMessageSymbol(messageText);
        let cache = hyperCleanup.cache[symbol];

        if (!cache) {
            hyperCleanup.cache[symbol] = [Date.now()];
        }
        else {
            hyperCleanup.cache[symbol].push(Date.now());
        }
    };

    hyperCleanup.isSpamMessage = (message) => {
        // Algorithm
        //  if (message is all emoji) and (seen in x minutes) spam
        //  else if (seen >y times in x minutes) spam

        let isSpam = false;
        let messageText = "", messageTextNoEmoji = "";

        // Build up message
        for (let iRun = 0; iRun < message.runs.length; iRun++) {
            let run = message.runs[iRun];

            if (run.text) {
                messageText += run.text;
                messageTextNoEmoji += run.text;
            }
            else if (run.emoji) {
                // We COULD look at the url but id works better to be honest
                messageText += "[emoji:" + run.emoji.emojiId + "]";
            }
        }

        // It's not a message we can read? More research required.
        // Might also just be a superchat which we allow - Superchat spam is part of the chat experience.
        if (messageText.length < 1) return false;

        let spamStartCheck = Date.now() - hyperCleanup.options.spamTime;
        let spamCount = hyperCleanup.messageCountSince(messageText, spamStartCheck);

        if (hyperCleanup.options.removeEmojiDuplicates && messageTextNoEmoji.length === 0 && spamCount > 0) {
            isSpam = true;
        }
        else if (spamCount >= hyperCleanup.options.spamAllowedAmount) {
            isSpam = true;
        }
        else if (hyperCleanup.messageCountSince(messageTextNoEmoji, spamStartCheck) >= hyperCleanup.options.spamAllowedAmountStrippingEmojis) {
            isSpam = true;
        }

        if (isSpam) {
            // Performance improvement: don't log when spam detected
            // Left in for now
            console.log('Spam detected: ' + messageText);
        }

        // Make sure we actually log the message AFTER we've checked for it
        hyperCleanup.logMessage(messageText, message);

        return isSpam;
    };

    hyperCleanup.cleanJson = (json) => {
        // Potential paths:
        // .continuationContents.liveChatContinuation.actions[0].replayChatItemAction.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs[0].text
        // .continuationContents.liveChatContinuation.actions[0].replayChatItemAction.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs[0].emoji

        if (!json || !json.continuationContents || !json.continuationContents.liveChatContinuation || !json.continuationContents.liveChatContinuation.actions) return json;

        try {
            // Loop all message actions and remove stuff that looks like spam
            // We go FORWARDS so that we catch the first spam message and then clean from there
            // This is a bit more performance heavy than going backwards (splicing from start, resetting indexes) but the alternative is reversing twice...which is heavier

            for (let iContAction = 0; iContAction < json.continuationContents.liveChatContinuation.actions.length; iContAction++) {
                let contAction = json.continuationContents.liveChatContinuation.actions[iContAction];

                if (contAction.replayChatItemAction) {
                    // If is a chat replay
                    for (let iChatAction = 0; iChatAction < contAction.replayChatItemAction.actions.length; iChatAction++) {
                        let chatAction = contAction.replayChatItemAction.actions[iChatAction];
                        if (chatAction.addChatItemAction && chatAction.addChatItemAction.item && chatAction.addChatItemAction.item.liveChatTextMessageRenderer) {
                            if (hyperCleanup.isSpamMessage(chatAction.addChatItemAction.item.liveChatTextMessageRenderer.message)) {
                                // Remove & reset
                                contAction.replayChatItemAction.actions.splice(iChatAction, 1);
                                iChatAction--;
                            }
                        }
                    }

                    if (contAction.replayChatItemAction.actions.length === 0) {
                        // Remove & reset
                        json.continuationContents.liveChatContinuation.actions.splice(iContAction, 1);
                        iContAction--;
                    }
                }
                else if (contAction.addChatItemAction && contAction.addChatItemAction.item && contAction.addChatItemAction.item.liveChatTextMessageRenderer) {
                    // If is actually a live chat
                    if (hyperCleanup.isSpamMessage(contAction.addChatItemAction.item.liveChatTextMessageRenderer.message)) {
                        // Remove & reset
                        json.continuationContents.liveChatContinuation.actions.splice(iContAction, 1);
                        iContAction--;
                    }
                }
            }
        }
        catch (exception) {
            // If an error happens, let's just ignore it and pretend we did nothing
            console.error('An error occurred while cleaning up chat');
            console.error(exception);
        }

        return json;
    };

    // This is borrowed from HyperChat - YouTube makes fetch requests when it gets messages
    window.fetch = async (...args) => {
        const url = args[0].url;
        const result = await originalFetch(...args);

        // Only do stuff if we actually are a live chat request
        if (url.startsWith('https://www.youtube.com/youtubei/v1/live_chat/get_live_chat')) {
            // Clone it because otherwise weird stuff happens
            let newResult = await result.clone();
            let json = await newResult.json();

            // If you want to do more digging into the json
            //console.log(json);
            json = hyperCleanup.cleanJson(json);

            // New response object to pass back, which is read by the parent fetch()
            // No need to work out the init obj, since we can just use our base result
            let madeResult = new Response(JSON.stringify(json), newResult);
            return madeResult;
        }

        return result;
    };
})();

// message object looks like:
/*
{
    "runs": [
        { "text": "hello" },
        { "emoji":
{
    "emojiId": "UCyl1z3jo3XHR1riLFKG5UAg/LpZtX7zMKcmu_APKm7rAAw",
    "shortcuts": [
        ":_hic1:",
        ":hic1:"
    ],
    "searchTerms": [
        "_hic1",
        "hic1"
    ],
    "image": {
        "thumbnails": [
            {
                "url": "https://yt3.ggpht.com/EAnKSTeFBCc0gzNVIgkZGU5OZiLwQVb3gVjySef6GRhTq2Mp6HJ5r1rQo7QBX2iUppf9Xi_C=w24-h24-c-k-nd",
                "width": 24,
                "height": 24
            },
            {
                "url": "https://yt3.ggpht.com/EAnKSTeFBCc0gzNVIgkZGU5OZiLwQVb3gVjySef6GRhTq2Mp6HJ5r1rQo7QBX2iUppf9Xi_C=w48-h48-c-k-nd",
                "width": 48,
                "height": 48
            }
        ],
        "accessibility": {
            "accessibilityData": {
                "label": "hic1"
            }
        }
    },
    "isCustomEmoji": true
}
}
    ]
}
        */