YouTube Chat Filter

Filters messages in YouTube stream chat.

目前為 2023-07-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name        YouTube Chat Filter
// @version     1.6
// @description Filters messages in YouTube stream chat.
// @author      Callum Latham
// @namespace   https://gf.qytechs.cn/users/696211-ctl2
// @license     MIT
// @match       *://www.youtube.com/*
// @match       *://youtube.com/*
// @require     https://gf.qytechs.cn/scripts/446506-config/code/$Config.js?version=1081062
// @require     https://gf.qytechs.cn/scripts/449472-boolean/code/$Boolean.js?version=1081058
// @grant       GM.setValue
// @grant       GM.getValue
// @grant       GM.deleteValue
// ==/UserScript==

// Don't run outside the chat frame
if (!window.frameElement || window.frameElement.id !== 'chatframe') {
    // noinspection JSAnnotator
    return;
}
//misidentifying
window.addEventListener('load', async () => {
    // STATIC CONSTS

    const LONG_PRESS_TIME = 400;
    const ACTIVE_COLOUR = 'var(--yt-spec-call-to-action)';
    const CHAT_LIST_SELECTOR = '#items.yt-live-chat-item-list-renderer';
    const FILTER_CLASS = 'cf';
    const TAGS_FILTERABLE = [
        'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER',
        'YT-LIVE-CHAT-PAID-MESSAGE-RENDERER',
        'YT-LIVE-CHAT-MEMBERSHIP-ITEM-RENDERER',
        'YTD-SPONSORSHIPS-LIVE-CHAT-GIFT-PURCHASE-ANNOUNCEMENT-RENDERER',
        'YTD-SPONSORSHIPS-LIVE-CHAT-GIFT-REDEMPTION-ANNOUNCEMENT-RENDERER'
    ];
    const PRIORITIES = {
        'VERIFIED': 'Verification Badge',
        'MODERATOR': 'Moderator Badge',
        'MEMBER': 'Membership Badge',
        'LONG': 'Long',
        'RECENT': 'Recent',
        'SUPERCHAT': 'Superchat',
        'MEMBERSHIP_RENEWAL': 'Membership Purchase',
        'MEMBERSHIP_GIFT_OUT': 'Membership Gift (Given)',
        'MEMBERSHIP_GIFT_IN': 'Membership Gift (Received)',
        'EMOJI': 'Emojis'
    };

    // ELEMENT CONSTS

    const STREAMER = window.parent.document.querySelector('#upload-info > #channel-name').innerText;
    const ROOT_ELEMENT = document.body.querySelector('#chat');
    const [BUTTON, SVG, COUNTER] = await (async () => {
        const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';

        const [button, svgContainer, svg] = await new Promise((resolve) => {
            const template = document.body.querySelector('#live-chat-header-context-menu');
            const button = template.querySelector('button').cloneNode(true);
            const svgContainer = button.querySelector('yt-icon');

            button.style.visibility = 'hidden';

            button.querySelector('yt-touch-feedback-shape').remove();

            template.parentElement.insertBefore(button, template);

            window.setTimeout(() => {
                const path = document.createElementNS(SVG_NAMESPACE, 'path');

                path.setAttribute('d', 'M128.25,175.6c1.7,1.8,2.7,4.1,2.7,6.6v139.7l60-51.3v-88.4c0-2.5,1-4.8,2.7-6.6L295.15,65H26.75L128.25,175.6z');

                const rectangle = document.createElementNS(SVG_NAMESPACE, 'rect');

                rectangle.setAttribute('x', '13.95');
                rectangle.setAttribute('y', '0');
                rectangle.setAttribute('width', '294');
                rectangle.setAttribute('height', '45');

                const svg = document.createElementNS(SVG_NAMESPACE, 'svg');

                svg.setAttribute('viewBox', '-50 -50 400 400');
                svg.setAttribute('x', '0');
                svg.setAttribute('y', '0');
                svg.setAttribute('focusable', 'false');

                svg.append(path, rectangle);

                svgContainer.innerHTML = '';
                svgContainer.append(svg);

                button.style.removeProperty('visibility');

                button.style.setProperty('display', 'contents')

                resolve([button, svgContainer, svg]);
            }, 0);
        });

        const counter = (() => {
            const container = document.createElement('div');

            container.style.position = 'absolute';
            container.style.left = '9px';
            container.style.bottom = '9px';
            container.style.fontSize = '1.1em';
            container.style.lineHeight = 'normal';
            container.style.width = '1.6em';
            container.style.display = 'flex';
            container.style.alignItems = 'center';

            const svg = (() => {
                const circle = document.createElementNS(SVG_NAMESPACE, 'circle');

                circle.setAttribute('r', '50');
                circle.style.color = 'var(--yt-live-chat-header-background-color)';
                circle.style.opacity = '0.65';

                const svg = document.createElementNS(SVG_NAMESPACE, 'svg');

                svg.setAttribute('viewBox', '-70 -70 140 140');

                svg.append(circle);

                return svg;
            })();

            const text = document.createElement('span');

            text.style.position = 'absolute';
            text.style.width = '100%';
            text.innerText = '?';

            container.append(text, svg);

            svgContainer.append(container);

            return text;
        })();

        return [button, svg, counter];
    })();

    // STATE INTERFACES

    const $active = new $Boolean('YTCF_IS_ACTIVE');

    const $config = new $Config(
        'YTCF_TREE',
        (() => {
            const regexPredicate = (value) => {
                try {
                    RegExp(value);
                } catch (_) {
                    return 'Value must be a valid regular expression.';
                }

                return true;
            };

            return {
                'children': [
                    {
                        'label': 'Filters',
                        'children': [],
                        'seed': {
                            'label': 'Description',
                            'value': '',
                            'children': [
                                {
                                    'label': 'Streamer Regex',
                                    'children': [],
                                    'seed': {
                                        'value': '^',
                                        'predicate': regexPredicate
                                    }
                                },
                                {
                                    'label': 'Author Regex',
                                    'children': [],
                                    'seed': {
                                        'value': '^',
                                        'predicate': regexPredicate
                                    }
                                },
                                {
                                    'label': 'Message Regex',
                                    'children': [],
                                    'seed': {
                                        'value': '^',
                                        'predicate': regexPredicate
                                    }
                                }
                            ]
                        }
                    },
                    {
                        'label': 'Options',
                        'children': [
                            {
                                'label': 'Case-Sensitive Regex?',
                                'value': false
                            },
                            {
                                'label': 'Pause on Mouse Over?',
                                'value': false
                            },
                            {
                                'label': 'Queue Time (ms)',
                                'value': 0,
                                'predicate': (value) => value >= 0 ? true : 'Queue time must be positive'
                            }
                        ]
                    },
                    {
                        'label': 'Preferences',
                        'children': [
                            {
                                'label': 'Requirements',
                                'children': [
                                    {
                                        'label': 'OR',
                                        'children': [],
                                        'poolId': 0
                                    },
                                    {
                                        'label': 'AND',
                                        'children': [],
                                        'poolId': 0
                                    }
                                ]
                            },
                            {
                                'label': 'Priorities (High to Low)',
                                'poolId': 0,
                                'children': Object.values(PRIORITIES).map(label => ({
                                    label,
                                    'value': label !== PRIORITIES.EMOJI && label !== PRIORITIES.MEMBERSHIP_GIFT_IN
                                }))
                            }
                        ]
                    }
                ]
            };
        })(),
        (() => {
            const EVALUATORS = (() => {
                const getEvaluator = (evaluator, isDesired) => isDesired ? evaluator : (_) => 1 - evaluator(_);

                return {
                    // Special tests
                    [PRIORITIES.RECENT]: getEvaluator.bind(null, () => 1),
                    [PRIORITIES.LONG]: getEvaluator.bind(null, _ => _.querySelector('#message').textContent.length),
                    // Tests for message type
                    [PRIORITIES.SUPERCHAT]: getEvaluator.bind(null, _ => _.matches('yt-live-chat-paid-message-renderer')),
                    [PRIORITIES.MEMBERSHIP_RENEWAL]: getEvaluator.bind(null, _ => _.matches('yt-live-chat-membership-item-renderer')),
                    [PRIORITIES.MEMBERSHIP_GIFT_OUT]: getEvaluator.bind(null, _ => _.matches('ytd-sponsorships-live-chat-gift-purchase-announcement-renderer')),
                    [PRIORITIES.MEMBERSHIP_GIFT_IN]: getEvaluator.bind(null, _ => _.matches('ytd-sponsorships-live-chat-gift-redemption-announcement-renderer')),
                    // Tests for descendant element presence
                    [PRIORITIES.EMOJI]: getEvaluator.bind(null, _ => Boolean(_.querySelector('.emoji'))),
                    [PRIORITIES.MEMBER]: getEvaluator.bind(null, _ => Boolean(_.querySelector('#chat-badges > [type=member]'))),
                    [PRIORITIES.MODERATOR]: getEvaluator.bind(null, _ => Boolean(_.querySelector('#chip-badges > [type=verified]'))),
                    [PRIORITIES.VERIFIED]: getEvaluator.bind(null, _ => Boolean(_.querySelector('#chat-badges > [type=moderator]')))
                };
            })();

            return ([rawFilters, options, {'children': [{'children': [softRequirements, hardRequirements]}, priorities]}]) => ({
                'filters': (() => {
                    const filters = [];

                    const getRegex = options.children[0].value ?
                        ({value}) => new RegExp(value) :
                        ({value}) => new RegExp(value, 'i');
                    const matchesStreamer = (node) => getRegex(node).test(STREAMER);

                    for (const filter of rawFilters.children) {
                        const [{'children': streamers}, {'children': authors}, {'children': messages}] = filter.children;

                        if (streamers.length === 0 || streamers.some(matchesStreamer)) {
                            filters.push({
                                'authors': authors.map(getRegex),
                                'messages': messages.map(getRegex)
                            });
                        }
                    }

                    return filters;
                })(),
                'pauseOnHover': options.children[1].value,
                'queueTime': options.children[2].value,
                'requirements': {
                    'soft': softRequirements.children.map(({
                        label, 'value': isDesired
                    }) => EVALUATORS[label](isDesired)),
                    'hard': hardRequirements.children.map(({
                        label, 'value': isDesired
                    }) => EVALUATORS[label](isDesired))
                },
                'comparitors': (() => {
                    const getComparitor = (getValue, low, high) => {
                        low = getValue(low);
                        high = getValue(high);

                        return low < high ? -1 : low === high ? 0 : 1;
                    };

                    return priorities.children.map(({
                        label, 'value': isDesired
                    }) => getComparitor.bind(null, EVALUATORS[label](isDesired)));
                })()
            });
        })(),
        'YouTube Chat Filter',
        {
            'headBase': '#ff0000',
            'headButtonExit': '#000000',
            'borderHead': '#ffffff',
            'nodeBase': ['#222222', '#111111'],
            'borderTooltip': '#570000'
        },
        {'zIndex': 10000}
    );

    // CSS

    (function style() {
        function 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 = [
            [`${CHAT_LIST_SELECTOR}`, [
                ['bottom', 'inherit']
            ]],
            [`${CHAT_LIST_SELECTOR} > :not(.${FILTER_CLASS})`, [
                ['display', 'none']
            ]]
        ];

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

    // STATE

    let queuedPost;

    // FILTERING

    function doFilter(isInitial = true) {
        const chatListElement = ROOT_ELEMENT.querySelector(CHAT_LIST_SELECTOR);

        let doQueue = false;
        let paused = false;

        function showPost(post, queueNext) {
            const config = $config.get();

            post.classList.add(FILTER_CLASS);

            queuedPost = undefined;

            if (queueNext && config && config.queueTime > 0) {
                // Start queueing
                doQueue = true;

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

                    // Unqueue
                    if (!paused) {
                        acceptPost();
                    }
                }, config.queueTime);
            }
        }

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

            if (allowQueue && (doQueue || paused)) {
                queuedPost = post;
            } else {
                showPost(post, allowQueue);
            }
        }

        window.document.body.addEventListener('mouseenter', () => {
            const config = $config.get();

            if (config && config.pauseOnHover) {
                paused = true;
            }
        });

        window.document.body.addEventListener('mouseleave', () => {
            const config = $config.get();

            paused = false;

            if (config && config.pauseOnHover) {
                acceptPost();
            }
        });

        function processPost(post, allowQueue = true) {
            const config = $config.get();
            const isFilterable = config && $active.get() && TAGS_FILTERABLE.includes(post.tagName);

            if (isFilterable) {
                if (
                    config.filters.some(filter =>
                        // Test author filter
                        (filter.authors.length > 0 && filter.authors.some(_ => _.test(post.querySelector('#author-name')?.textContent))) ||
                        // Test message filter
                        (filter.messages.length > 0 && filter.messages.some(_ => _.test(post.querySelector('#message')?.textContent)))
                    ) ||
                    // Test requirements
                    (config.requirements.soft.length > 0 && !config.requirements.soft.some(passes => passes(post))) ||
                    config.requirements.hard.some(passes => !passes(post))
                ) {
                    return;
                }

                // Test inferior to queued post
                if (queuedPost) {
                    for (const comparitor of config.comparitors) {
                        const rating = comparitor(post, queuedPost);

                        if (rating < 0) {
                            return;
                        }

                        if (rating > 0) {
                            break;
                        }
                    }
                }
            }

            acceptPost(post, isFilterable && allowQueue);
        }

        if (isInitial) {
            // Process initial messages
            for (const post of chatListElement.children) {
                processPost(post, false);
            }

            // Re-sizes the chat after removing initial messages
            chatListElement.parentElement.style.height = `${chatListElement.clientHeight}px`;

            // Restart if the chat element gets replaced
            // This happens when switching between 'Top Chat Replay' and 'Live Chat Replay'
            new MutationObserver((mutations) => {
                for (const {addedNodes} of mutations) {
                    for (const node of addedNodes) {
                        if (node.matches('yt-live-chat-item-list-renderer')) {
                            doFilter(false);
                        }
                    }
                }
            }).observe(
                ROOT_ELEMENT.querySelector('#item-list'),
                {childList: true}
            );
        }

        // Handle new posts
        new MutationObserver((mutations) => {
            for (const {addedNodes} of mutations) {
                for (const addedNode of addedNodes) {
                    processPost(addedNode);
                }
            }
        }).observe(
            chatListElement,
            {childList: true}
        );
    }

    // MAIN

    (() => {
        let timeout;

        const updateSvg = () => {
            SVG.style[`${$active.get() ? 'set' : 'remove'}Property`]('color', ACTIVE_COLOUR);
        };

        const updateCounter = () => {
            const config = $config.get();
            const count = config ? config.filters.length : 0;

            queuedPost = undefined;

            COUNTER.style[`${count > 0 ? 'set' : 'remove'}Property`]('color', ACTIVE_COLOUR);

            COUNTER.innerText = `${count}`;
        };

        const onShortClick = (event) => {
            if (timeout && event.button === 0) {
                timeout = window.clearTimeout(timeout);

                $active.toggle();

                updateSvg();
            }
        };

        const onLongClick = () => {
            timeout = undefined;

            $config.edit()
                .then(updateCounter)
                .catch(({message}) => {
                    if (window.confirm(`${message}\n\nWould you like to erase your data?`)) {
                        $config.reset();

                        updateCounter();
                    }
                });
        };

        Promise.allSettled([
            $active.init()
                .then(updateSvg),
            $config.init()
                .then(updateCounter)
        ])
            .then((responses) => {
                // Start filtering
                doFilter();

                // Inform users of issues
                for (const response of responses) {
                    if ('reason' in response) {
                        window.alert(response.reason.message);
                    }
                }

                // Add short click listener
                BUTTON.addEventListener('mouseup', onShortClick);

                // Add long click listener
                BUTTON.addEventListener('mousedown', (event) => {
                    if (event.button === 0) {
                        timeout = window.setTimeout(onLongClick, LONG_PRESS_TIME);
                    }
                });
            });
    })();
});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址