您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters messages in YouTube stream chat.
当前为
// ==UserScript== // @name YouTube Chat Filter // @version 1.0 // @description Filters messages in YouTube stream chat. // @author Callum Latham // @namespace https://greasyfork.org/users/696211-ctl2 // @license MIT // @match *://www.youtube.com/* // @match *://youtube.com/* // @require https://greasyfork.org/scripts/446506-config/code/$Config.js?version=1081062 // @require https://greasyfork.org/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.id !== 'chatframe') { // noinspection JSAnnotator return; } 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 TITLE = 'YouTube Chat Filter'; const PRIORITIES = { 'MODE_CHANGE': 'Chat Mode Change', '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('#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('#overflow'); const button = template.cloneNode(true); const svgContainer = button.querySelector('yt-icon'); button.style.visibility = 'hidden'; 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'); 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': 'Show Intro Message?', 'value': false }, { 'label': 'Pause on Mouse Over?', 'value': false }, { 'label': 'Queue Time (ms)', 'value': 1000, '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')), [PRIORITIES.MODE_CHANGE]: getEvaluator.bind(null, _ => _.matches('yt-live-chat-mode-change-message-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; })(), 'showIntroMessage': options.children[1].value, 'pauseOnHover': options.children[2].value, 'queueTime': options.children[3].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))); })() }); })(), TITLE, { '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() { const chatListElement = ROOT_ELEMENT.querySelector(CHAT_LIST_SELECTOR); let doQueue = false; let paused = false; function showPost(post) { const config = $config.get(); post.classList.add(FILTER_CLASS); queuedPost = undefined; if (config && config.queueTime > 0) { // Start queueing doQueue = true; window.setTimeout(() => { doQueue = false; // Unqueue if (!paused) { acceptPost(); } }, config.queueTime); } } function acceptPost(post = queuedPost) { /** Unqueue iff: * - Nothing was queued at the most recent unqueue * - No posts have been shown since the last unqueue * - A post is queued */ if (!post) { return; } if (doQueue || paused) { queuedPost = post; } else { showPost(post); } } 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) { const config = $config.get(); // Test insta-accept if ( (!config || !$active.get()) || (config.showIntroMessage && post.matches('yt-live-chat-viewer-engagement-message-renderer')) || post.matches('yt-live-chat-placeholder-item-renderer') ) { showPost(post); return; } // Test reject 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); } // Handle new posts new MutationObserver((mutations) => { for (const {addedNodes} of mutations) { addedNodes.forEach(processPost); } }).observe( chatListElement, {childList: true} ); } // BUTTON LISTENERS (() => { 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) => { for (const response of responses) { if ('reason' in response) { window.alert(response.reason.message); } } BUTTON.addEventListener('mouseup', onShortClick); BUTTON.addEventListener('mousedown', (event) => { if (event.button === 0) { timeout = window.setTimeout(onLongClick, LONG_PRESS_TIME); } }); }); })(); doFilter(); // 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(); } } } }).observe( ROOT_ELEMENT.querySelector('#item-list'), {childList: true} ); });