YouTube Chat Filter

Filters messages in YouTube stream chat.

目前为 2021-08-01 提交的版本,查看 最新版本

// ==UserScript==
// @name         YouTube Chat Filter
// @namespace   https://greasyfork.org/users/696211-ctl2
// @version      0.1
// @description  Filters messages in YouTube stream chat.
// @author       Callum Latham
// @match        *://www.youtube.com/*
// @match        *://youtube.com/*
// @grant        none
// ==/UserScript==

const CONFIG = {
    /**
     * A higher number means more message space.
     * A value of 1 will make the chat area as tall as the chat iframe's body.
     */
    'CHAT_HEIGHT': 2
};

const FILTER = [
    {
        'streamer': /^/,
        'author': /$./,
        // Filters out non-Japanese messages (allows 'w' (笑))
        'message': /[abcdefghijklmnopqrstuvxyz]/i,
        'requireBadge': true,
        'queueTime': 1000,
        'stopOnHover': true
    }
];

(() => {
    if (window.frameElement.id !== 'chatframe') {
        return;
    }

    (function style() {
        const addStyle = (sheet, selector, rules) => {
            const ruleString = rules.map(
                ([selector, rule]) => `${selector}:${typeof rule === 'function' ? rule() : rule} !important;`
            );

            sheet.insertRule(`${selector}{${ruleString.join('')}}`);
        };

        const styleElement = document.createElement('style');
        const {sheet} = document.head.appendChild(styleElement);

        const styles = [
            ['#item-offset', [
                ['height', `${document.body.clientHeight * CONFIG.CHAT_HEIGHT}px`]
            ]],
            ['#items:not(.cf)', [
                ['display', 'none']
            ]],
            ['#items.cf > :nth-child(even)', [
                ['background-color', '#1f1f1f']
            ]]
        ];

        for (const style of styles) {
            addStyle(sheet, style[0], style[1]);
        }
    })();

    window.onload = async () => {
        const filter = (() => {
            const streamer = parent.document.querySelector('#meta').querySelector('#channel-name').innerText;

            for (const {'streamer': regex, ...filter} of FILTER) {
                if (regex.test(streamer)) {
                    return filter;
                }
            }
        })();

        // Terminate if there's no valid filter
        if (!filter) {
            return;
        }

        const chatElements = {'held': document.body.querySelector('#chat').querySelector('#items')};

        chatElements.accepted = chatElements.held.cloneNode(false);

        chatElements.accepted.classList.add('cf');
        chatElements.held.parentElement.appendChild(chatElements.accepted);

        let queuedPost;
        let doQueue = false;
        let hovered = false;

        function trimPosts() {
            const chat = chatElements.accepted;
            const container = chat.parentElement;
            const {top} = container.getBoundingClientRect();

            for (let topPost = chat.firstChild; topPost.getBoundingClientRect().top - top < 0; topPost = chat.firstChild) {
                topPost.remove();
            }
        }

        function showPost(post) {
            const container = chatElements.accepted;

            container.appendChild(post);

            // Save memory by deleting passed posts
            trimPosts();

            queuedPost = undefined;

            if (filter.queueTime > 0) {
                // Start queueing
                doQueue = true;

                window.setTimeout(() => {
                    doQueue = false;

                    // Unqueue
                    acceptPost();
                }, filter.queueTime);
            }
        }

        function acceptPost(post = queuedPost) {
            if (!post) {
                return;
            }

            if (doQueue || (filter.stopOnHover && hovered)) {
                queuedPost = post;
            } else {
                showPost(post);
            }
        }

        window.document.body.addEventListener('mouseenter', () => {
            hovered = true;
        });
        window.document.body.addEventListener('mouseleave', () => {
            hovered = false;

            /** Unqueue iff:
             * - Nothing was queued at the most recent unqueue
             * - No posts have been shown since the last unqueue
             * - A post is queued
             */
            acceptPost();
        });

        function processPost(post) {
            chatElements.held.parentElement.style.removeProperty('height');

            try {
                if (
                    filter.author.test(post.querySelector('#author-name').textContent) ||
                    filter.message.test(post.querySelector('#message').textContent) ||
                    (filter.requireBadge && !post.querySelector('#chat-badges').hasChildNodes())
                ) {
                    // Save memory by deleting rejected posts
                    post.remove();
                } else {
                    acceptPost(post);
                }
            } catch (e) {
                // console.group('STRANGE POST');
                // console.warn(post);
                // console.warn(e);
                // console.groupEnd();
            }
        }

        new MutationObserver((mutations) => {
            for (const {addedNodes} of mutations) {
                addedNodes.forEach(processPost);
            }
        }).observe(
            chatElements.held,
            {childList: true}
        );
    };
})();