text: // ==UserScript==
  // @name         Twitch chat cleaner
  // @namespace    https://filipesabella.com
  // @version      0.20
  // @description  Add spam controls and filters to twitch chat.
  // @author       Filipe Sabella
  // @license      MIT
  // @match        https://www.twitch.tv/*
  // ==/UserScript==
  
  const defaults = {
    disableAll: false,
    spammy: true,
    emojiOnly: true,
    allCaps: true,
    freeFilters: [
      'lulw',
      'you can use quotes',
      '/.*and regexes.*/i',
    ],
    maxWords: 40,
    minWords: 1,
    tooManyDuplicatesThreshold: 1.7,
    tooManyEmojiThreshold: 3,
    allowWords: [],
  };
  
  let options = defaults;
  let counter = 0;
  
  function isGarbage(options, s) {
    const trimmed = s.trim();
    const upperCased = trimmed.toUpperCase();
    const words = trimmed.split(' ').filter(s => s !== '');
  
    const isUpperCase = s => s === upperCased;
  
    const filteredOut = _ => options
      .freeFilters
      .map(filter => {
        if (filter[0] === '/') {
          const [_, regex, flags] = filter.match(/\/(.*)\/(.*)/);
          return new RegExp(regex, flags);
        } else {
          return new RegExp(`.*${filter}.*`, 'i');
        }
      })
      .filter(filter => filter.test(trimmed))
      .length > 0;
  
    const isMessageTooLong = _ => words.length > options.maxWords;
    const isMessageTooShort = _ => words.length < options.minWords;
    const isDuplicatedPhrase = words =>
      words.length / new Set(words).size >= options.tooManyDuplicatesThreshold;
  
    return (options.emojiOnly && trimmed === '') ||
      (options.allCaps && isUpperCase(trimmed)) ||
      isMessageTooLong(trimmed) ||
      isMessageTooShort(trimmed) ||
      filteredOut(trimmed) ||
      (options.spammy && isDuplicatedPhrase(words));
  }
  
  function handler(event) {
    if (options.disableAll) return false;
  
    const messageContainer = event.target;
    if (messageContainer.className !== 'chat-line__message') return;
  
    const text = Array.from(messageContainer.querySelectorAll('.text-fragment'))
      .map(e => e.innerHTML).join(' ').trim();
  
    const allowRegex =
      new RegExp('.*(' + options.allowWords.join('|') + ').*', 'i');
    if (allowRegex.test(text)) return false;
  
    const tooManyEmoji = () => options.spammy && messageContainer
      .querySelectorAll('.chat-line__message--emote-button')
      .length >= options.tooManyEmojiThreshold;
  
    if (tooManyEmoji() || isGarbage(options, text)) {
      remove(messageContainer);
  
      if (text !== '') {
        console.log('Filtered message: ' + text);
      }
    }
  }
  
  function remove(messageContainer) {
    // removing the node causes issues with other twitch features
    // messageContainer.remove();
    messageContainer.style.display = 'none';
    document.getElementById('counter-container').innerHTML = '🚯' + ++counter;
  }
  
  function listenToMessages() {
    readOptions();
  
    document.onkeyup = e => {
      if (e.which === 27) { // escape
        hideOptions();
      }
    }
  
    const c = document.querySelector('.chat-scrollable-area__message-container');
    if (!c) {
      window.setTimeout(listenToMessages, 500);
      return;
    }
  
    c.removeEventListener('DOMNodeInserted', handler);
    c.addEventListener('DOMNodeInserted', handler, false);
  
    if (!document.getElementById('counter-container')) {
      const container = document.querySelector('.chat-input__buttons-container');
      if (!container) return;
  
      const counterContainer = document.createElement('div');
      counterContainer.id = 'counter-container';
      counterContainer.style.cursor = 'pointer';
      counterContainer.style.userSelect = 'none';
      counterContainer.innerHTML = '🚯';
      counterContainer.onclick = () => {
        showOptions();
      };
      container.childNodes[1].prepend(counterContainer);
    }
  
  }
  
  function showOptions() {
    readOptions();
  
    let optionsContainer = document.getElementById('options-container');
    if (!optionsContainer) {
      addStyle(`
        #options-container {
          display: block;
          position: absolute;
          top: 0;
          left: 0;
          width: 30em;
          z-index: 99999;
          padding: 1.5em ;
          background-color: rgb(24, 24, 27);
          color: rgb(239, 239, 241);
          border: 1px solid rgb(239, 239, 241);
        }
  
        #options-container .close-button {
          position: absolute;
          top: .5em;
          right: 1em;
          cursor: pointer;
        }
  
        #options-container label {
          display: inline-block;
          width: 15em;
        }
  
        #options-container label span,
        #options-container p span {
          cursor: help;
          text-decoration: underline;
        }
  
        #options-container > div:not(:last-child) {
          margin-bottom: 1em;
        }
  
        #options-container input, #options-container textarea {
          background-color: rgb(239, 239, 241);
        }
  
        #options-container input[type=number] {
          width: 6em;
        }
  
        #options-container textarea {
          width: 100%;
          height: 15em;
          font-family: inherit;
          line-height: 1.5em;
          padding: .5em;
        }
  
        #options-container .button {
          text-align: right;
        }
  
        #options-container .button input {
          padding: 3px;
        }
      `);
  
      const freeFilters = options.freeFilters.join(' ');
  
      document.body.insertAdjacentHTML('beforeend',
        `<div id="options-container">
          <div class="close-button"
            id="twitchCleaner__closeButton">X</div>
          <div>
            <label>Block
              <span title="Overall spam with duplicated text or too many emoji">
                spammy messages
              </span>
            </label>
            <input class="input" type="checkbox" name="spammy"
              ${options.spammy && 'checked'}></input>
          </div>
          <div>
            <label>Block emoji only</label>
            <input class="input" type="checkbox" name="emojiOnly"
              ${options.emojiOnly && 'checked'}></input>
          </div>
          <div>
            <label>Block all caps</label>
            <input class="input" type="checkbox" name="allCaps"
              ${options.allCaps && 'checked'}></input>
          </div>
          <div>
            <label>Max words per message</label>
            <input class="input" type="number" name="maxWords"
              value="${options.maxWords}"></input>
          </div>
          <div>
            <label>Min words per message</label>
            <input class="input" type="number" name="minWords"
              value="${options.minWords}"></input>
          </div>
          <div>
            <p>
              Block messages that match the
              <span class="help" title="You can:
    - Add words, it simply blocks messages that contain them, case insensitive
    - Add phrases by enclosing them in double-quotes, same as above
    - Add regular expressions by surrounding your expression with '/'">
                following
              </span>
            </p>
            <textarea
              class="input"
              name="freeFilters">${freeFilters}</textarea>
          </div>
          <div>
            <label>
              ALLOW messages
              <span class="help" title="Useful for adding your name, for example.
  Precedes *all* other filters, case insensitive.">
                containing
              </span>
            </label>
            <input class="input" type="text" name="allowWords"
              value="${options.allowWords.join(' ')}"></input>
          </div>
          <div>
            <label>Disable all filters</label>
            <input class="input" type="checkbox" name="disableAll"
              ${options.disableAll && 'checked'}></input>
          </div>
        </div>`);
  
      optionsContainer = document.getElementById('options-container');
  
      const {
        top,
        left
      } = document.getElementById('counter-container').getBoundingClientRect();
  
      const {
        width,
        height,
      } = optionsContainer.getBoundingClientRect();
  
      optionsContainer.style.left = left - width + 'px';
      optionsContainer.style.top = top - height + 'px';
  
      document.querySelectorAll('#options-container .input').forEach(e => {
        e.onkeyup = e.onkeypress = e.onchange = () => {
          const opts = Array.from(
            document.querySelectorAll('#options-container .input')
          ).reduce((acc, e) => {
            const name = e.name;
            acc[name] = e.type === 'checkbox' ?
              e.checked :
              e.value;
            return acc;
          }, {});
  
          storeOptions(opts);
          readOptions();
        };
      });
  
      document.getElementById('twitchCleaner__closeButton').onclick = hideOptions;
    } else {
      optionsContainer.style.display = optionsContainer
        .style.display === 'block' ?
        'none' :
        'block';
    }
  }
  
  function hideOptions() {
    document.getElementById('options-container').style.display = 'none';
  }
  
  window.setInterval(() => {
    if (!document.getElementById('counter-container')) {
      listenToMessages();
    }
  }, 1000);
  
  function addStyle(css) {
    var style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = css;
    document.head.appendChild(style);
  }
  
  function readOptions() {
    try {
      const s = localStorage.getItem('twitch-cleaner-options');
      const opt = s ? JSON.parse(s) : defaults;
      const merged = {
        ...defaults,
        ...opt,
      };
  
      // puts quotes back into multi-word items. e.g., the array:
      // ['aaa', 'hello there', 'bbb', '/a regex/']
      // returns the string:
      // aaa "hello there" bbb /a regex/
      merged.freeFilters = merged.freeFilters
        .map(w => w.includes(' ') && w[0] !== '/' ? `"${w}"` : w);
  
      options = merged;
    } catch (e) {
      console.error(e);
      options = defaults;
    }
  }
  
  function storeOptions(options) {
    // split into array and remove possible double-quotes. e.g., the string:
    // aaa "hello there" bbb /a regex/
    // returns the array:
    // ['aaa', 'hello there', 'bbb', '/a regex/']
    options.freeFilters = options.freeFilters
      .match(/\w+|"[^"]+"|\/[^\/]+\/[a-z]*/g)
      .map(s => s.replace(/"/g, ''));
  
    options.allowWords = options.allowWords.split(' ');
  
    localStorage.setItem(
      'twitch-cleaner-options',
      JSON.stringify(options));
  }
  