Cleanup YouTube live chat for spam

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

当前为 2021-05-31 提交的版本,查看 最新版本

// ==UserScript==
// @name         Cleanup YouTube live chat for spam
// @namespace    http://tampermonkey.net/
// @version      0.1
// @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: 120000,

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

            // 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.stripSpaceChars = /\s/g;
    hyperCleanup.getMessageSymbol = (messageText) => {
        // Ignore case & remove spaces
        // We could PROBABLY also just strip out punctuation
        return Symbol.for( messageText.toLowerCase().replace(hyperCleanup.stripSpaceChars, '') );
    };
    hyperCleanup.messageCountSince = (messageText, time, deleteIfOlderThan) => {
        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 (deleteIfOlderThan && cache[i] < deleteIfOlderThan) {
                    // 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 = "";
        let isAllEmoji = true;

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

            if (run.text) {
                isAllEmoji = false;
                messageText += 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, spamStartCheck);

        if (hyperCleanup.options.removeEmojiDuplicates && isAllEmoji && spamCount > 0) {
            isSpam = true;
        }
        else if (spamCount > hyperCleanup.options.spamAllowedAmount) {
            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
}
}
    ]
}
        */