Guest User

Twitch plays Pokemon - Updated Chat Filter script (March 2nd

a guest
Mar 2nd, 2014
8,056
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.56 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Twitch Plays Pokemon Chat Filter
  3. // @namespace https://github.com/jpgohlke/twitch-chat-filter
  4. // @description Hide input commands from the chat.
  5.  
  6. // @include http://www.twitch.tv/twitchplayspokemon
  7. // @include http://www.twitch.tv/twitchplayspokemon/
  8. // @include http://www.twitch.tv/chat/embed?channel=twitchplayspokemon&popout_chat=true
  9.  
  10. // @version 1.6
  11. // @updateURL http://jpgohlke.github.io/twitch-chat-filter/chat_filter.user.js
  12. // @grant unsafeWindow
  13. // ==/UserScript==
  14.  
  15. /*
  16. * Permission is hereby granted, free of charge, to any person obtaining a copy
  17. * of this software and associated documentation files (the "Software"), to deal
  18. * in the Software without restriction, including without limitation the rights
  19. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  20. * copies of the Software, and to permit persons to whom the Software is furnished
  21. * to do so, subject to the following conditions:
  22. *
  23. * The above copyright notice and this permission notice shall be included in all
  24. * copies or substantial portions of the Software.
  25. *
  26. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  27. * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  28. * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  29. * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  30. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  31. * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  32. */
  33.  
  34. /*
  35. * chat_filter.user.js
  36. *
  37. * Feel free to review/compress it yourself; good internet security is important!
  38. * Passes http://www.jshint.com on default settings
  39. * Contributors:
  40. * /u/RenaKunisaki
  41. * /u/smog_alado
  42. * /u/SRS-SRSLY
  43. * /u/schrobby
  44. * /u/red_agent
  45. * /u/DeathlyDeep
  46. * /u/jeff_gohlke
  47. * /u/yankjenets
  48. * /u/hugomg
  49. * /u/MKody
  50. * /u/feha
  51. * /u/jakery2
  52. * /u/redopium
  53. * /u/codefusion
  54. * /u/Zephymastyx
  55. */
  56.  
  57. /* global unsafeWindow:false */
  58. /* jshint lastsemic:true */
  59.  
  60.  
  61. (function(){
  62. "use strict";
  63.  
  64. // --- Script configuration ---
  65.  
  66. var TPP_COMMANDS = [
  67. "left", "right", "up", "down",
  68. "start", "select",
  69. "a", "b",
  70. "democracy", "anarchy", "wait"
  71. ];
  72.  
  73. // Score-based filter for "Guys, we need to beat Misty" spam.
  74. var MISTY_SUBSTRINGS = [
  75. "misty",
  76. "guys",
  77. "we have to",
  78. "we need to",
  79. "beat",
  80. ];
  81.  
  82. var URL_WHITELIST = [
  83. //us
  84. "github.com",
  85. //reddit
  86. "reddit.com",
  87. "webchat.freenode.net/?channels=twitchplayspokemon",
  88. "sites.google.com/site/twitchplayspokemonstatus/",
  89. "reddit.com/live/sw7bubeycai6hey4ciytwamw3a",
  90. //miscelaneous
  91. "strawpoll.me",
  92. "imgur.com",
  93. "pokeworld.herokuapp.com",
  94. "strategywiki.org/wiki/Pok", //truncated before special characters
  95. "vgmaps.com"
  96. ];
  97.  
  98. var BANNED_WORDS = [
  99. "anus",
  100. "giveaway", "t-shirt", "hoodie",
  101. "imgur.com/4jlbxid.jpg"
  102. ];
  103.  
  104. var MINIMUM_DISTANCE_ERROR = 2; // Number of insertions / deletions / substitutions away from a blocked word.
  105. var MAXIMUM_NON_ASCII_CHARACTERS = 2; // For donger smilies, etc
  106. var MINIMUM_MESSAGE_WORDS = 2; // For Kappas and other short messages.
  107.  
  108. // The regexp Twitch uses to detect and automatically linkify URLs, with some modifications
  109. // so we can blacklist more messages.
  110. // - Recognizes *** in URLS (due to the Twitch chat censoring)
  111. // - Recognizes .mx and .sh TLDs
  112. var URL_REGEX = /\x02?((?:https?:\/\/|[\w\-\.\+]+@)?\x02?(?:[\w\-\*]+\x02?\.)+\x02?(?:com|au|org|tv|net|info|jp|uk|us|cn|fr|mobi|gov|co|ly|me|vg|eu|ca|fm|am|ws|mx|sh)\x02?(?:\:\d+)?\x02?(?:\/[\w\.\/@\?\&\%\#\(\)\,\-\+\=\;\:\x02?]+\x02?[\w\/@\?\&\%\#\(\)\=\;\x02?]|\x02?\w\x02?|\x02?)?\x02?)\x02?/g;
  113. var CENSORED_URL = /\*\*\*[\/\?\#\%]/g;
  114.  
  115. // --- Greasemonkey loading ---
  116.  
  117. //Greasemonkey userscripts run in a separate environment and cannot use
  118. //global variables from the page directly. We needd to access them via unsafeWindow
  119. var myWindow;
  120. try{
  121. myWindow = unsafeWindow;
  122. }catch(e){
  123. myWindow = window;
  124. }
  125.  
  126. var $ = myWindow.jQuery;
  127. var CurrentChat = null;
  128.  
  129. // --- Filtering predicates ---
  130.  
  131. // Adapted from https://gist.github.com/andrei-m/982927
  132. // Compute the edit distance between the two given strings
  133. function min_edit(a, b) {
  134.  
  135. if(a.length === 0) return b.length;
  136. if(b.length === 0) return a.length;
  137.  
  138. var matrix = [];
  139.  
  140. // increment along the first column of each row
  141. for(var i = 0; i <= b.length; i++) {
  142. matrix[i] = [i];
  143. }
  144.  
  145. // increment each column in the first row
  146. for(var j = 0; j <= a.length; j++) {
  147. matrix[0][j] = j;
  148. }
  149.  
  150. // Fill in the rest of the matrix
  151. for(var i = 1; i <= b.length; i++) {
  152. for(var j = 1; j <= a.length; j++) {
  153. if(b.charAt(i-1) == a.charAt(j-1)){
  154. matrix[i][j] = matrix[i-1][j-1];
  155. } else {
  156. matrix[i][j] = 1 + Math.min(
  157. matrix[i-1][j-1], // substitution
  158. matrix[i][j-1] , // insertion
  159. matrix[i-1][j] // deletion
  160. );
  161. }
  162. }
  163. }
  164.  
  165. return matrix[b.length][a.length];
  166. }
  167.  
  168. //This regex recognizes messages that contain exactly a chat command,
  169. //without any extra words around. This includes compound democracy mode
  170. //commands like `up2left4` and `start9`.
  171. // (remember to escape the backslashes when building a regexes from strings!)
  172. var compound_command_regex = new RegExp("^((" + TPP_COMMANDS.join("|") + ")\\d*)+$", "i");
  173.  
  174. function word_is_command(word){
  175.  
  176. if(compound_command_regex.test(word)) return true;
  177.  
  178. for(var j=0; j<TPP_COMMANDS.length; j++){
  179. var cmd = TPP_COMMANDS[j];
  180.  
  181. if(min_edit(cmd, word) <= MINIMUM_DISTANCE_ERROR){
  182. return true;
  183. }
  184. }
  185. return false;
  186. }
  187.  
  188. function message_is_command(message){
  189. message = message.toLowerCase();
  190.  
  191. var segments = message.split(/[\d\s]+/);
  192.  
  193. for(var i=0; i<segments.length; i++){
  194. var segment = segments[i];
  195. if(!segment) continue;
  196. if(!word_is_command(segment)) return false;
  197. }
  198.  
  199. return true;
  200. }
  201.  
  202.  
  203. function message_is_spam(message) {
  204. message = message.toLowerCase();
  205.  
  206. for(var i=0; i < BANNED_WORDS.length; i++){
  207. if(0 <= message.indexOf(BANNED_WORDS[i])){
  208. return true;
  209. }
  210. }
  211.  
  212. // Determine if message is variant of "Guys, we need to beat Misty."
  213. var misty_score = 0;
  214. for (var i = 0; i < MISTY_SUBSTRINGS.length; i++) {
  215. if (message.indexOf(MISTY_SUBSTRINGS[i]) != -1) {
  216. misty_score++;
  217. if (misty_score > 1) {
  218. return true;
  219. }
  220. }
  221. }
  222.  
  223. return false;
  224. }
  225.  
  226. function is_whitelisted_url(url){
  227. //This doesnt actually parse the URLs but it
  228. //should do the job when it comes to filtering.
  229. for(var i=0; i<URL_WHITELIST.length; i++){
  230. if(0 <= url.indexOf(URL_WHITELIST[i])){
  231. return true;
  232. }
  233. }
  234. return false;
  235. }
  236.  
  237. function message_is_forbidden_link(message){
  238. message = message.toLowerCase();
  239.  
  240. if(CENSORED_URL.test(message)) return true;
  241.  
  242. var urls = message.match(URL_REGEX);
  243. if(!urls) return false;
  244.  
  245. for(var i=0; i<urls.length; i++){
  246. if(!is_whitelisted_url(urls[i])){
  247. return true;
  248. }
  249. }
  250.  
  251. return false;
  252. }
  253.  
  254. function message_is_donger(message){
  255. var nonASCII = 0;
  256. for(var i = 0; i < message.length; i++) {
  257. if(message.charCodeAt(i) > 127) {
  258. nonASCII++;
  259. if(nonASCII > MAXIMUM_NON_ASCII_CHARACTERS){
  260. return true;
  261. }
  262. }
  263. }
  264. return false;
  265. }
  266.  
  267. function message_is_small(message){
  268. return message.split(/\s/g).length < MINIMUM_MESSAGE_WORDS;
  269. }
  270.  
  271. function message_is_cyrillic(message){
  272. //Some people use cyrillic characters to write spam that gets past the filter.
  273. return /[\u0400-\u04FF]/.test(message);
  274. }
  275.  
  276. function convert_copy_paste(message){
  277. //Replace repetitive text with only one instance of it
  278. //Useful for text and links where people do
  279. // ctrl-c ctrl-v ctrl-v ctrl-v in order to increase the
  280. //size of the message.
  281. return message.replace(/(.{4}.*?)(\s*?\1)+/g, "$1");
  282. }
  283.  
  284. // --- Filtering ---
  285.  
  286. //Filters have predicates that are called for every message
  287. //to determine whether it should get dropped or not
  288. var filters = [
  289. { name: 'TppFilterCommand',
  290. comment: "Commands (up, down, anarchy, etc)",
  291. isActive: true,
  292. predicate: message_is_command
  293. },
  294.  
  295. { name: 'TppFilterLink',
  296. comment: "Non-whitelisted URLs",
  297. isActive: true,
  298. predicate: message_is_forbidden_link
  299. },
  300.  
  301. { name: 'TppFilterDonger',
  302. comment: "Ascii art and dongers",
  303. isActive: false,
  304. predicate: message_is_donger
  305. },
  306.  
  307. { name: 'TppFilterSmall',
  308. comment: "One-word messages",
  309. isActive: false,
  310. predicate: message_is_small
  311. },
  312.  
  313. { name: 'TppFilterSpam',
  314. comment: 'Spam',
  315. isActive: true,
  316. predicate: message_is_spam
  317. },
  318.  
  319. { name: 'TppFilterCyrillic',
  320. comment: 'Cyrillic characters',
  321. isActive: true,
  322. predicate: message_is_cyrillic
  323. }
  324. ];
  325.  
  326.  
  327. //Rewriters are applied to the text of a message
  328. //before it is inserted in the chat box
  329. var rewriters = [
  330. { name: 'TppFilterDuplicateURL',
  331. comment: "Copy pasted repetitions",
  332. isActive: true,
  333. rewriter: convert_copy_paste
  334. },
  335. ];
  336.  
  337. //Stylers are CSS classes that get toggled on/off
  338. var stylers = [
  339. { name: 'TppConvertAllcaps',
  340. comment: "Lowercase-only mode",
  341. isActive: true,
  342. element: '#chat',
  343. class: 'allcaps_filtered'
  344. },
  345. ];
  346.  
  347. function passes_active_filters(message){
  348. for(var i=0; i < filters.length; i++){
  349. var filter = filters[i];
  350. if(filter.isActive && filter.predicate(message)){
  351. //console.log("Filter", filter.name, message);
  352. return false;
  353. }
  354. }
  355. return true;
  356. }
  357.  
  358. function rewrite_with_active_rewriters(message){
  359. var newMessage = message;
  360. for(var i=0; i < rewriters.length; i++){
  361. var rewriter = rewriters[i];
  362. if(rewriter.isActive){
  363. newMessage = (rewriter.rewriter(newMessage) || newMessage);
  364. }
  365. }
  366. return newMessage;
  367. }
  368.  
  369. // --- UI ---
  370.  
  371. function initialize_ui(){
  372.  
  373. //TODO: #chat_line_list li.fromjtv
  374.  
  375. $("button.viewers").after('<a id="chat_filter_dropmenu_button" class="dropdown_glyph"><span></span><a>');
  376. $('#chat_filter_dropmenu_button').on('click', function(){
  377. $('#chat_filter_dropmenu').toggle();
  378. });
  379.  
  380. $('#chat_filter_dropmenu_button span')
  381. .css('background', 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAv0lEQVQ4jc3SIQ7CQBAF0C8rK5E9AhI5R1gccpLOn+UACARHwCO5Aq6HQHAUQsAhwJGmlNBdIOEnY18mfwb4u4hIYWaSOySnAABVrWKMt9xx97OqVlDVkbufPoAuZiYAgBBC6e5NBnJQ1eqpK5KbBKQJIZQvyyc5f4eQ3A66pJlJjLG3N3dfJr0FyUUHudZ1PUtCWls9IDPbJyN90OBeulHV8beg6lfQKgsSkaJ18qOZTbIgAHD3NcmdiBTZSGruBIYOSjStwb0AAAAASUVORK5CYII=)')
  382. .css('position', 'relative');
  383.  
  384. //Temporal fix to filter button style -- Begin
  385. $('#chat_filter_dropmenu_button')
  386. .css('background', '#c4c3c3')
  387. .css('border', 'none')
  388. .css('margin-left', '5px')
  389. .css('padding', '4px 3px 2px')
  390. .css('top', '10px');
  391. $('.chat-buttons-container').css('top', '60px');
  392. $('.send-chat-button').css('top', '10px');
  393. //Temporal fix to filter button style -- End
  394.  
  395. $('.send-chat-button').css('left', '90px');
  396. $('.chat-buttons-container').css('top', '60px');
  397. $('.chat-interface').append('<div id="chat_filter_dropmenu" class="dropmenu menu-like" style="position:absolute; bottom:45px; display:none;"><p style="margin-left:6px">Hide:</p></div>');
  398.  
  399.  
  400. var controlPanel = $('#chat_filter_dropmenu');
  401.  
  402. var customCssParts = [
  403. ".chat-messages .TppFiltered {display:none;} .filter_option{font-weight:normal; margin-bottom:0; color: #B9A3E3;}",
  404. "#chat.allcaps_filtered span.message{text-transform:lowercase;}"
  405. ];
  406.  
  407. $('head').append('<style>' + customCssParts.join("") + '</style>');
  408.  
  409. function add_option(option, update){
  410. controlPanel
  411. .append('<p class="dropmenu_action"><label for="' + option.name + '" class="filter_option"> <input type="checkbox" id="' + option.name + '">' + option.comment + '</label></p>');
  412.  
  413. $('#' + option.name)
  414. .on('change', function(){
  415. option.isActive = $(this).prop("checked");
  416. update(option);
  417.  
  418. })
  419. .prop('checked', option.isActive);
  420. }
  421.  
  422. filters.forEach(function(filter){
  423. add_option(filter, update_chat_with_filter);
  424. });
  425. $('#chat_filter_dropmenu').append('<p style="margin-left:6px;">Automatically rewrite:</p>');
  426. rewriters.forEach(function(rewriter){
  427. add_option(rewriter, function(rewriter){});
  428. });
  429. function update_css(styler){
  430. if(styler.isActive)
  431. $(styler.element).addClass(styler.class);
  432. else
  433. $(styler.element).removeClass(styler.class);
  434. }
  435. stylers.forEach(function(option){
  436. add_option(option, update_css);
  437. update_css(option);
  438. });
  439. }
  440.  
  441.  
  442. // --- Main ---
  443.  
  444. function update_chat_with_filter(){
  445. if(!CurrentChat) return; //Chat hasnt loaded yet.
  446.  
  447. $('.chat-line').each(function() {
  448. var chatLine = $(this);
  449. var chatText = chatLine.find(".message").text().trim();
  450.  
  451. if(passes_active_filters(chatText)){
  452. chatLine.removeClass("TppFiltered");
  453. }else{
  454. chatLine.addClass("TppFiltered");
  455. }
  456. });
  457. }
  458.  
  459. function initialize_filter(){
  460. CurrentChat = myWindow.App.Room.prototype;
  461.  
  462. update_chat_with_filter();
  463.  
  464. var original_insert_chat_line = CurrentChat.addMessage;
  465. CurrentChat.addMessage = function(info) {
  466. if(!passes_active_filters(info.message)){ return false }
  467. info.message = rewrite_with_active_rewriters(info.message);
  468. return original_insert_chat_line.apply(this, arguments);
  469. };
  470. }
  471.  
  472. $(function(){
  473. //Checking for the spinner being gone is a more reliable way to chack
  474. //if the CurrentChat is fully loaded.
  475. var chatLoadedCheck = setInterval(function () {
  476. if($(".loading-mask").length == 0){
  477. clearInterval(chatLoadedCheck);
  478. initialize_ui();
  479. initialize_filter();
  480. }
  481. }, 100);
  482.  
  483. });
  484.  
  485. }());
Add Comment
Please, Sign In to add comment