Twitch chat cleaner

Add spam controls and filters to twitch chat.

目前為 2021-08-10 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Twitch chat cleaner
  3. // @namespace https://filipesabella.com
  4. // @version 0.20
  5. // @description Add spam controls and filters to twitch chat.
  6. // @author Filipe Sabella
  7. // @license MIT
  8. // @match https://www.twitch.tv/*
  9. // ==/UserScript==
  10.  
  11. const defaults = {
  12. disableAll: false,
  13. spammy: true,
  14. emojiOnly: true,
  15. allCaps: true,
  16. freeFilters: [
  17. 'lulw',
  18. 'you can use quotes',
  19. '/.*and regexes.*/i',
  20. ],
  21. maxWords: 40,
  22. minWords: 1,
  23. tooManyDuplicatesThreshold: 1.7,
  24. tooManyEmojiThreshold: 3,
  25. allowWords: [],
  26. };
  27.  
  28. let options = defaults;
  29. let counter = 0;
  30.  
  31. function isGarbage(options, s) {
  32. const trimmed = s.trim();
  33. const upperCased = trimmed.toUpperCase();
  34. const words = trimmed.split(' ').filter(s => s !== '');
  35.  
  36. const isUpperCase = s => s === upperCased;
  37.  
  38. const filteredOut = _ => options
  39. .freeFilters
  40. .map(filter => {
  41. if (filter[0] === '/') {
  42. const [_, regex, flags] = filter.match(/\/(.*)\/(.*)/);
  43. return new RegExp(regex, flags);
  44. } else {
  45. return new RegExp(`.*${filter}.*`, 'i');
  46. }
  47. })
  48. .filter(filter => filter.test(trimmed))
  49. .length > 0;
  50.  
  51. const isMessageTooLong = _ => words.length > options.maxWords;
  52. const isMessageTooShort = _ => words.length < options.minWords;
  53. const isDuplicatedPhrase = words =>
  54. words.length / new Set(words).size >= options.tooManyDuplicatesThreshold;
  55.  
  56. return (options.emojiOnly && trimmed === '') ||
  57. (options.allCaps && isUpperCase(trimmed)) ||
  58. isMessageTooLong(trimmed) ||
  59. isMessageTooShort(trimmed) ||
  60. filteredOut(trimmed) ||
  61. (options.spammy && isDuplicatedPhrase(words));
  62. }
  63.  
  64. function handler(event) {
  65. if (options.disableAll) return false;
  66.  
  67. const messageContainer = event.target;
  68. if (messageContainer.className !== 'chat-line__message') return;
  69.  
  70. const text = Array.from(messageContainer.querySelectorAll('.text-fragment'))
  71. .map(e => e.innerHTML).join(' ').trim();
  72.  
  73. const allowRegex =
  74. new RegExp('.*(' + options.allowWords.join('|') + ').*', 'i');
  75. if (allowRegex.test(text)) return false;
  76.  
  77. const tooManyEmoji = () => options.spammy && messageContainer
  78. .querySelectorAll('.chat-line__message--emote-button')
  79. .length >= options.tooManyEmojiThreshold;
  80.  
  81. if (tooManyEmoji() || isGarbage(options, text)) {
  82. remove(messageContainer);
  83.  
  84. if (text !== '') {
  85. console.log('Filtered message: ' + text);
  86. }
  87. }
  88. }
  89.  
  90. function remove(messageContainer) {
  91. // removing the node causes issues with other twitch features
  92. // messageContainer.remove();
  93. messageContainer.style.display = 'none';
  94. document.getElementById('counter-container').innerHTML = '🚯' + ++counter;
  95. }
  96.  
  97. function listenToMessages() {
  98. readOptions();
  99.  
  100. document.onkeyup = e => {
  101. if (e.which === 27) { // escape
  102. hideOptions();
  103. }
  104. }
  105.  
  106. const c = document.querySelector('.chat-scrollable-area__message-container');
  107. if (!c) {
  108. window.setTimeout(listenToMessages, 500);
  109. return;
  110. }
  111.  
  112. c.removeEventListener('DOMNodeInserted', handler);
  113. c.addEventListener('DOMNodeInserted', handler, false);
  114.  
  115. if (!document.getElementById('counter-container')) {
  116. const container = document.querySelector('.chat-input__buttons-container');
  117. if (!container) return;
  118.  
  119. const counterContainer = document.createElement('div');
  120. counterContainer.id = 'counter-container';
  121. counterContainer.style.cursor = 'pointer';
  122. counterContainer.style.userSelect = 'none';
  123. counterContainer.innerHTML = '🚯';
  124. counterContainer.onclick = () => {
  125. showOptions();
  126. };
  127. container.childNodes[1].prepend(counterContainer);
  128. }
  129.  
  130. }
  131.  
  132. function showOptions() {
  133. readOptions();
  134.  
  135. let optionsContainer = document.getElementById('options-container');
  136. if (!optionsContainer) {
  137. addStyle(`
  138. #options-container {
  139. display: block;
  140. position: absolute;
  141. top: 0;
  142. left: 0;
  143. width: 30em;
  144. z-index: 99999;
  145. padding: 1.5em ;
  146. background-color: rgb(24, 24, 27);
  147. color: rgb(239, 239, 241);
  148. border: 1px solid rgb(239, 239, 241);
  149. }
  150.  
  151. #options-container .close-button {
  152. position: absolute;
  153. top: .5em;
  154. right: 1em;
  155. cursor: pointer;
  156. }
  157.  
  158. #options-container label {
  159. display: inline-block;
  160. width: 15em;
  161. }
  162.  
  163. #options-container label span,
  164. #options-container p span {
  165. cursor: help;
  166. text-decoration: underline;
  167. }
  168.  
  169. #options-container > div:not(:last-child) {
  170. margin-bottom: 1em;
  171. }
  172.  
  173. #options-container input, #options-container textarea {
  174. background-color: rgb(239, 239, 241);
  175. }
  176.  
  177. #options-container input[type=number] {
  178. width: 6em;
  179. }
  180.  
  181. #options-container textarea {
  182. width: 100%;
  183. height: 15em;
  184. font-family: inherit;
  185. line-height: 1.5em;
  186. padding: .5em;
  187. }
  188.  
  189. #options-container .button {
  190. text-align: right;
  191. }
  192.  
  193. #options-container .button input {
  194. padding: 3px;
  195. }
  196. `);
  197.  
  198. const freeFilters = options.freeFilters.join(' ');
  199.  
  200. document.body.insertAdjacentHTML('beforeend',
  201. `<div id="options-container">
  202. <div class="close-button"
  203. id="twitchCleaner__closeButton">X</div>
  204. <div>
  205. <label>Block
  206. <span title="Overall spam with duplicated text or too many emoji">
  207. spammy messages
  208. </span>
  209. </label>
  210. <input class="input" type="checkbox" name="spammy"
  211. ${options.spammy && 'checked'}></input>
  212. </div>
  213. <div>
  214. <label>Block emoji only</label>
  215. <input class="input" type="checkbox" name="emojiOnly"
  216. ${options.emojiOnly && 'checked'}></input>
  217. </div>
  218. <div>
  219. <label>Block all caps</label>
  220. <input class="input" type="checkbox" name="allCaps"
  221. ${options.allCaps && 'checked'}></input>
  222. </div>
  223. <div>
  224. <label>Max words per message</label>
  225. <input class="input" type="number" name="maxWords"
  226. value="${options.maxWords}"></input>
  227. </div>
  228. <div>
  229. <label>Min words per message</label>
  230. <input class="input" type="number" name="minWords"
  231. value="${options.minWords}"></input>
  232. </div>
  233. <div>
  234. <p>
  235. Block messages that match the
  236. <span class="help" title="You can:
  237. - Add words, it simply blocks messages that contain them, case insensitive
  238. - Add phrases by enclosing them in double-quotes, same as above
  239. - Add regular expressions by surrounding your expression with '/'">
  240. following
  241. </span>
  242. </p>
  243. <textarea
  244. class="input"
  245. name="freeFilters">${freeFilters}</textarea>
  246. </div>
  247. <div>
  248. <label>
  249. ALLOW messages
  250. <span class="help" title="Useful for adding your name, for example.
  251. Precedes *all* other filters, case insensitive.">
  252. containing
  253. </span>
  254. </label>
  255. <input class="input" type="text" name="allowWords"
  256. value="${options.allowWords.join(' ')}"></input>
  257. </div>
  258. <div>
  259. <label>Disable all filters</label>
  260. <input class="input" type="checkbox" name="disableAll"
  261. ${options.disableAll && 'checked'}></input>
  262. </div>
  263. </div>`);
  264.  
  265. optionsContainer = document.getElementById('options-container');
  266.  
  267. const {
  268. top,
  269. left
  270. } = document.getElementById('counter-container').getBoundingClientRect();
  271.  
  272. const {
  273. width,
  274. height,
  275. } = optionsContainer.getBoundingClientRect();
  276.  
  277. optionsContainer.style.left = left - width + 'px';
  278. optionsContainer.style.top = top - height + 'px';
  279.  
  280. document.querySelectorAll('#options-container .input').forEach(e => {
  281. e.onkeyup = e.onkeypress = e.onchange = () => {
  282. const opts = Array.from(
  283. document.querySelectorAll('#options-container .input')
  284. ).reduce((acc, e) => {
  285. const name = e.name;
  286. acc[name] = e.type === 'checkbox' ?
  287. e.checked :
  288. e.value;
  289. return acc;
  290. }, {});
  291.  
  292. storeOptions(opts);
  293. readOptions();
  294. };
  295. });
  296.  
  297. document.getElementById('twitchCleaner__closeButton').onclick = hideOptions;
  298. } else {
  299. optionsContainer.style.display = optionsContainer
  300. .style.display === 'block' ?
  301. 'none' :
  302. 'block';
  303. }
  304. }
  305.  
  306. function hideOptions() {
  307. document.getElementById('options-container').style.display = 'none';
  308. }
  309.  
  310. window.setInterval(() => {
  311. if (!document.getElementById('counter-container')) {
  312. listenToMessages();
  313. }
  314. }, 1000);
  315.  
  316. function addStyle(css) {
  317. var style = document.createElement('style');
  318. style.type = 'text/css';
  319. style.textContent = css;
  320. document.head.appendChild(style);
  321. }
  322.  
  323. function readOptions() {
  324. try {
  325. const s = localStorage.getItem('twitch-cleaner-options');
  326. const opt = s ? JSON.parse(s) : defaults;
  327. const merged = {
  328. ...defaults,
  329. ...opt,
  330. };
  331.  
  332. // puts quotes back into multi-word items. e.g., the array:
  333. // ['aaa', 'hello there', 'bbb', '/a regex/']
  334. // returns the string:
  335. // aaa "hello there" bbb /a regex/
  336. merged.freeFilters = merged.freeFilters
  337. .map(w => w.includes(' ') && w[0] !== '/' ? `"${w}"` : w);
  338.  
  339. options = merged;
  340. } catch (e) {
  341. console.error(e);
  342. options = defaults;
  343. }
  344. }
  345.  
  346. function storeOptions(options) {
  347. // split into array and remove possible double-quotes. e.g., the string:
  348. // aaa "hello there" bbb /a regex/
  349. // returns the array:
  350. // ['aaa', 'hello there', 'bbb', '/a regex/']
  351. options.freeFilters = options.freeFilters
  352. .match(/\w+|"[^"]+"|\/[^\/]+\/[a-z]*/g)
  353. .map(s => s.replace(/"/g, ''));
  354.  
  355. options.allowWords = options.allowWords.split(' ');
  356.  
  357. localStorage.setItem(
  358. 'twitch-cleaner-options',
  359. JSON.stringify(options));
  360. }

QingJ © 2025

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