YouTube Chat Filter

Filters messages in YouTube stream chat.

当前为 2021-07-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Chat Filter
  3. // @namespace https://greasyfork.org/users/696211-ctl2
  4. // @version 0.1
  5. // @description Filters messages in YouTube stream chat.
  6. // @author Callum Latham
  7. // @match *://www.youtube.com/*
  8. // @match *://youtube.com/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. const CONFIG = {
  13. // A higher number means less messages are shown
  14. 'SPACE_DIVIDER': 20,
  15. // A higher number means more message space
  16. 'HEIGHT_MULTIPLIER': 0.91
  17. };
  18.  
  19. const FILTER = [
  20. {
  21. 'streamer': /^/,
  22. 'author': /$./,
  23. // Filters out non-Japanese messages (allows 'w' (笑))
  24. 'message': /[abcdefghijklmnopqrstuvxyz]/i,
  25. 'requireBadge': true,
  26. 'limit': 1000,
  27. 'stopOnHover': true
  28. }
  29. ];
  30.  
  31. (() => {
  32. if (window.frameElement.id !== 'chatframe') {
  33. return;
  34. }
  35.  
  36. const {clientHeight} = window.document.body;
  37. const spaces = Math.floor(clientHeight / CONFIG.SPACE_DIVIDER);
  38.  
  39. (function style() {
  40. const addStyle = (sheet, selector, rules) => {
  41. const ruleString = rules.map(
  42. ([selector, rule]) => `${selector}:${typeof rule === 'function' ? rule() : rule};`
  43. );
  44.  
  45. sheet.insertRule(`${selector}{${ruleString.join('')}}`);
  46. };
  47.  
  48. const styleElement = document.createElement('style');
  49. const {sheet} = document.head.appendChild(styleElement);
  50.  
  51. const styles = [
  52. ['#item-offset', [
  53. ['height', `${clientHeight * CONFIG.HEIGHT_MULTIPLIER}px`]
  54. ]],
  55. ['#items:not(.cf)', [
  56. ['display', 'none']
  57. ]],
  58. ['#items.cf > :nth-child(even)', [
  59. ['background-color', '#1f1f1f']
  60. ]]
  61. ];
  62.  
  63. for (const style of styles) {
  64. addStyle(sheet, style[0], style[1]);
  65. }
  66. })();
  67.  
  68. window.onload = async () => {
  69. const filter = (() => {
  70. const streamer = parent.document.querySelector('#meta').querySelector('#channel-name').innerText;
  71.  
  72. for (const {'streamer': regex, ...filter} of FILTER) {
  73. if (regex.test(streamer)) {
  74. return filter;
  75. }
  76. }
  77. })();
  78.  
  79. // Terminate if there's no valid filter
  80. if (!filter) {
  81. return;
  82. }
  83.  
  84. const chatElements = {'held': document.body.querySelector('#chat').querySelector('#items')};
  85.  
  86. chatElements.accepted = chatElements.held.cloneNode(false);
  87.  
  88. chatElements.accepted.classList.add('cf');
  89. chatElements.held.parentElement.appendChild(chatElements.accepted);
  90.  
  91. let queuedPost;
  92. let doQueue = false;
  93. let hovered = false;
  94.  
  95. function showPost(post) {
  96. const container = chatElements.accepted;
  97.  
  98. container.appendChild(post);
  99.  
  100. // Save memory by deleting passed posts
  101. while (container.children.length > spaces) {
  102. container.firstChild.remove();
  103. }
  104.  
  105. doQueue = true;
  106. queuedPost = undefined;
  107. }
  108.  
  109. function acceptPost(post = queuedPost) {
  110. if (!post) {
  111. return;
  112. }
  113.  
  114. if (doQueue || (filter.stopOnHover && hovered)) {
  115. queuedPost = post;
  116. } else {
  117. showPost(post);
  118. }
  119. }
  120.  
  121. // Unqueue at regular intervals
  122. window.setInterval(() => {
  123. doQueue = false;
  124.  
  125. acceptPost();
  126. }, filter.limit);
  127.  
  128. window.document.body.addEventListener('mouseenter', () => {
  129. hovered = true;
  130. });
  131. window.document.body.addEventListener('mouseleave', () => {
  132. hovered = false;
  133.  
  134. /** Unqueue iff:
  135. * - Nothing was queued at the most recent unqueue
  136. * - No posts have been shown since the last unqueue
  137. * - A post is queued
  138. */
  139. acceptPost();
  140. });
  141.  
  142. function processPost(post) {
  143. chatElements.held.parentElement.style.removeProperty('height');
  144.  
  145. try {
  146. if (
  147. filter.author.test(post.querySelector('#author-name').textContent) ||
  148. filter.message.test(post.querySelector('#message').textContent) ||
  149. (filter.requireBadge && !post.querySelector('#chat-badges').hasChildNodes())
  150. ) {
  151. // Save memory by deleting rejected posts
  152. post.remove();
  153. } else {
  154. acceptPost(post);
  155. }
  156. } catch (e) {
  157. console.group('STRANGE POST');
  158. console.warn(post);
  159. console.warn(e);
  160. console.groupEnd();
  161. }
  162. }
  163.  
  164. new MutationObserver((mutations) => {
  165. for (const {addedNodes} of mutations) {
  166. addedNodes.forEach(processPost);
  167. }
  168. }).observe(
  169. chatElements.held,
  170. {childList: true}
  171. );
  172. };
  173. })();