YT Comment Filter (Improved)

Automatically hide YouTube comments based on username and spam text patterns.

// ==UserScript==
// @name            YT Comment Filter (Improved)
// @name:id         Penyaring Komentar YT (Ditingkatkan)
// @namespace       https://www.youtube.com/@MochAdiCH
// @version         3.9.5
// @description     Automatically hide YouTube comments based on username and spam text patterns.
// @description:id  Sembunyikan komentar YouTube secara otomatis berdasarkan nama pengguna dan pola teks spam.
// @author          Mochamad Adi MR (adimuham.mad)
// @match           *://www.youtube.com/watch?v=*
// @match           *://www.youtube.com/shorts/*
// @icon            https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant           GM_registerMenuCommand
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_addStyle
// @license         MIT
// @run-at          document-end
// ==/UserScript==

(function () {
    "use strict";

    let filterToggleButton = null;
    let isShowingHidden = false;
    const STORAGE_KEY = "yt_filter_custom_words";
    const builtInForbiddenUsernames = ["vip"];

    function loadCustomWords() {
        const savedWords = GM_getValue(STORAGE_KEY, "");

        if (savedWords) {
            return savedWords.split(',').map(word => word.trim()).filter(Boolean);
        }

        return [];
    }

    function showSettingsPrompt() {
        const currentWords = GM_getValue(STORAGE_KEY, "");
        const newWords = prompt("Enter custom username keywords (separate with commas)):\nno need to enter a username where the vowels\nare replaced with numbers", currentWords);
        if (newWords === null) return;
        GM_setValue(STORAGE_KEY, newWords);
        alert("Usernames keywords has saved!\nRefresh the page to apply the changes and see the results.");
    }

    function registerMenu() {
        GM_registerMenuCommand("List Blocked Username", showSettingsPrompt);
    }

    const customForbiddenUsernames = loadCustomWords();
    const forbiddenUsernames = Array.from(new Set([...builtInForbiddenUsernames, ...customForbiddenUsernames]));

    const forbiddenRegex = /[^\u0000-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u08A0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u125F\u1280-\u12BF\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1A20-\u1A5F\u1A80-\u1AFF\u1B00-\u1B7F\u1B80-\u1BBF\u1BC0-\u1BFF\u1C00-\u1C4F\u1C50-\u1C7F\u1C90-\u1CBF\u1CC0-\u1CCF\u1CD0-\u1CFF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u1F00-\u1FFF\u2000-\u206F\u2070-\u20CF\u20D0-\u20FF\u2150-\u218F\u2190-\u21FF\u2200-\u22FF\u2300-\u23FF\u2400-\u243F\u2440-\u245F\u2500-\u257F\u2580-\u259F\u25A0-\u25FF\u2600-\u26FF\u2700-\u27BF\u27C0-\u27EF\u27F0-\u27FF\u2800-\u28FF\u2900-\u297F\u2980-\u29FF\u2A00-\u2AFF\u2B00-\u2BFF\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2DE0-\u2DFF\u2E00-\u2E7F\u2E80-\u2EFF\u2F00-\u2FDF\u2FF0-\u2FFF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31A0-\u31BF\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA4D0-\uA4FF\uA500-\uA63F\uA640-\uA69F\uA6A0-\uA6FF\uA700-\uA71F\uA720-\uA7FF\uA800-\uA82F\uA830-\uA83F\uA840-\uA87F\uA880-\uA8DF\uA8E0-\uA8FF\uA900-\uA92F\uA930-\uA95F\uA960-\uA97F\uA980-\uA9DF\uA9E0-\uA9FF\uAA00-\uAA3F\uAA40-\uAA6F\uAA70-\uAAAB\uAAAC-\uAAAF\uAAB0-\uAABF\uAAC0-\uAADF\uAAE0-\uAAEF\uAAF0-\uAAFF\uAB00-\uAB2F\uAB30-\uAB6F\uAB70-\uABBF\uABC0-\uABFF\uAC00-\uD7AF\uD7B0-\uD7FF\uF900-\uFAFF\uFB00-\uFB4F\uFB50-\uFDFF\uFE00-\uFE0F\uFE10-\uFE1F\uFE20-\uFE2F\uFE30-\uFE4F\uFE50-\uFE6F\uFE70-\uFEFF\uFF00-\uFFEF]|\uD800[\uDC00-\uDFFF]|\uD801[\uDC00-\uDFFF]|\uD802[\uDC00-\uDFFF]|\uD803[\uDC00-\uDFFF]|\uD804[\uDC00-\uDFFF]|\uD805[\uDC00-\uDFFF]|\uD806[\uDC00-\uDFFF]|\uD807[\uDC00-\uDFFF]|\uD808[\uDC00-\uDFFF]|\uD809[\uDC00-\uDFFF]|\uD80A[\uDC00-\uDFFF]|\uD80B[\uDC00-\uDFFF]|\uD80C[\uDC00-\uDFFF]|\uD80D[\uDC00-\uDFFF]|\uD80E[\uDC00-\uDFFF]|\uD80F[\uDC00-\uDFFF]|\uD810[\uDC00-\uDFFF]|\uD811[\uDC00-\uDFFF]|\uD812[\uDC00-\uDFFF]|\uD813[\uDC00-\uDFFF]|\uD814[\uDC00-\uDFFF]|\uD815[\uDC00-\uDFFF]|\uD816[\uDC00-\uDFFF]|\uD817[\uDC00-\uDFFF]|\uD818[\uDC00-\uDFFF]|\uD819[\uDC00-\uDFFF]|\uD81A[\uDC00-\uDFFF]|\uD81B[\uDC00-\uDFFF]|\uD81C[\uDC00-\uDFFF]|\uD81D[\uDC00-\uDFFF]|\uD81E[\uDC00-\uDFFF]|\uD81F[\uDC00-\uDFFF]|\uD820[\uDC00-\uDFFF]|\uD821[\uDC00-\uDFFF]|\uD822[\uDC00-\uDFFF]|\uD823[\uDC00-\uDFFF]|\uD824[\uDC00-\uDFFF]|\uD825[\uDC00-\uDFFF]|\uD826[\uDC00-\uDFFF]|\uD827[\uDC00-\uDFFF]|\uD828[\uDC00-\uDFFF]|\uD829[\uDC00-\uDFFF]|\uD82A[\uDC00-\uDFFF]|\uD82B[\uDC00-\uDFFF]|\uD82C[\uDC00-\uDFFF]|\uD82D[\uDC00-\uDFFF]|\uD82E[\uDC00-\uDFFF]|\uD82F[\uDC00-\uDFFF]|\uD830[\uDC00-\uDFFF]|\uD831[\uDC00-\uDFFF]|\uD832[\uDC00-\uDFFF]|\uD833[\uDC00-\uDFFF]|\uD834[\uDC00-\uDFFF]|\uD835[\uDC00-\uDFFF]|\uD836[\uDC00-\uDFFF]|\uD837[\uDC00-\uDFFF]|\uD838[\uDC00-\uDFFF]|\uD839[\uDC00-\uDFFF]|\uD83A[\uDC00-\uDFFF]|\uD83B[\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|\uD83E[\uDC00-\uDFFF]|\uD83F[\uDC00-\uDFFF]/;

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    function createLeetPattern(word) {
        let escapedWord = escapeRegExp(word);
        return escapedWord.replace(/a/gi, "[a4]").replace(/i/gi, "[i1]").replace(/e/gi, "[e3]").replace(/o/gi, "[o0]").replace(/s/gi, "[s5]").replace(/t/gi, "[t7]");
    }

    const forbiddenPatterns = forbiddenUsernames.map(createLeetPattern);
    const forbiddenUserRegex = new RegExp(forbiddenPatterns.join("|"), "i");

    function addGlobalStyles() {
        GM_addStyle(`
            .yt-comment-filter-hidden { display: none; }
            body.yt-filter-showing-hidden .yt-comment-filter-hidden {
                display: block !important;
                opacity: 0.6;
                border: 1px dashed rgba(255, 0, 0, 0.5);
                border-radius: 8px;
                margin-bottom: 8px !important;
            }

            #yt-filter-toggle-container {
                display: inline-flex;
                align-items: center;
                vertical-align: middle;
                cursor: pointer;
                font-family: "Roboto","Arial",sans-serif;
                font-size: 14px;
                font-weight: 500;
                color: var(--yt-spec-text-secondary);
                margin-left: 32px;
                position: relative;
                bottom: 3px;
            }

            #yt-filter-toggle-container:hover {
                 color: var(--yt-spec-text-primary);
            }
            
            #yt-filter-toggle-container:hover svg {
                 fill: var(--yt-spec-text-primary);
            }

            #yt-filter-toggle-container > div {
                display: inline-flex;
                align-items: center;
            }

            #yt-filter-toggle-container svg {
                width: 24px;
                height: 24px;
                fill: var(--yt-spec-text-secondary);
                margin-right: 4px;
            }

            #yt-filter-toggle-container .yt-filter-label,
            #yt-filter-toggle-container .yt-filter-status {
                text-transform: uppercase;
                margin-right: 4px;
            }

            #yt-filter-toggle-container .icon-visibility-off { display: none; }
            #yt-filter-toggle-container .icon-visibility-on { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-off { display: inline-block; }
            body.yt-filter-showing-hidden #yt-filter-toggle-container .icon-visibility-on { display: none; }
        `);
    }

     function updateHiddenCount() {
        if (!filterToggleButton) return;
        const count = document.querySelectorAll(".yt-comment-filter-hidden").length;
        const label = filterToggleButton.querySelector(".yt-filter-label");
        const status = filterToggleButton.querySelector(".yt-filter-status");
        const info = filterToggleButton.querySelector(".yt-filter-info");
        if (!label || !status || !info) return;

        label.textContent = "Hidden";

        if (isShowingHidden) {
            status.textContent = "OFF";
            info.style.display = "none";
        } else {
            status.textContent = "ON";
            info.style.display = "";
            info.textContent = `(${count})`;
        }
    }

    function injectFilterButton() {
        if (document.getElementById("yt-filter-toggle-container")) return;
        const targetElement = document.querySelector("ytd-comments-header-renderer #additional-section");
        if (!targetElement) return;
        filterToggleButton = document.createElement("span");
        filterToggleButton.id = "yt-filter-toggle-container";
        filterToggleButton.className = "style-scope ytd-comments-header-renderer";
        const visibilityIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5C21.27 7.61 17 4.5 12 4.5zm0 13c-3.04 0-5.5-2.46-5.5-5.5S8.96 6.5 12 6.5s5.5 2.46 5.5 5.5-2.46 5.5-5.5 5.5zm0-9c-1.93 0-3.5 1.57-3.5 3.5s1.57 3.5 3.5 3.5 3.5-1.57 3.5-3.5-1.57-3.5-3.5-3.5z"></path></svg>`;
        const visibilityOffIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L21.73 23 23 21.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.93 1.57 3.5 3.5 3.5.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-3.04 0-5.5-2.46-5.5-5.5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.93-1.57-3.5-3.5-3.5l-.16.02z"></path></svg>`;
        filterToggleButton.innerHTML = `<div><svg class="icon-visibility-on" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityIconSVG}</svg><svg class="icon-visibility-off" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false">${visibilityOffIconSVG}</svg><span class="yt-filter-label">Hidden</span><span class="yt-filter-status">OFF</span><span class="yt-filter-info">(0)</span></div>`;
        filterToggleButton.addEventListener("click", () => {
            isShowingHidden = !isShowingHidden;
            document.body.classList.toggle("yt-filter-showing-hidden", isShowingHidden);
            updateHiddenCount();
        });
        targetElement.after(filterToggleButton);
        return true;
    }

    let throttleCooldown = false;
    const throttleDelay = 1000;

    function removeBadComments() {
        const commentContainers = document.querySelectorAll("ytd-comment-view-model:not(.yt-comment-filter-hidden)");

        commentContainers.forEach((commentContainer) => {
            const commentTextElement = commentContainer.querySelector("#content-text");
            const usernameElement = commentContainer.querySelector("#author-text span");
            if (!commentTextElement) return;
            let isSpam = false;
            if (forbiddenRegex.test(commentTextElement.innerText)) isSpam = true;
            if (!isSpam && usernameElement && forbiddenUserRegex.test(usernameElement.innerText)) isSpam = true;

            if (isSpam) {
                commentContainer.style.display = "none";
                commentContainer.classList.add("yt-comment-filter-hidden");

                if (commentContainer.id === "comment") {
                    const thread = commentContainer.closest("ytd-comment-thread-renderer");
                    if (thread) {
                        const repliesSection = thread.querySelector("div#replies");
                        if (repliesSection) {
                            repliesSection.style.display = "none";
                        }
                    }
                }
            }
        });
        
        updateHiddenCount();
    }

    function throttledRemoveBadComments() {
        if (throttleCooldown) return;
        removeBadComments();
        throttleCooldown = true;
        setTimeout(() => {
            throttleCooldown = false;
        }, throttleDelay);
    }

    registerMenu();
    addGlobalStyles();

    setTimeout(removeBadComments, 3000);

    const observer = new MutationObserver(() => {
        throttledRemoveBadComments();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    const buttonInjectInterval = setInterval(() => {
        if (injectFilterButton()) {
            clearInterval(buttonInjectInterval);
            updateHiddenCount();
        }
    }, 1000);
})();