Cleanup YouTube live chat for spam

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

            // 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 = /[^a-zA-Z0-9\[\]:]/g;
    hyperCleanup.getMessageSymbol = (messageText) => {
        // Ignore case & remove spaces
        // We could PROBABLY also just strip out punctuation
        return Symbol.for( messageText.toLowerCase().replace(hyperCleanup.stripChars, '') );
    };
    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
}
}
    ]
}
        */