Cleans up youtube live chat for simple spam - Repeated messages, overused emojis
当前为
// ==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
}
}
]
}
*/