Robin Enhancement Script

Highlight mentions, make link clickable, use channels & automatically remove spam

目前为 2016-04-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Robin Enhancement Script
  3. // @namespace https://www.reddit.com/
  4. // @version 3.1.2
  5. // @description Highlight mentions, make link clickable, use channels & automatically remove spam
  6. // @author Bag
  7. // @author netnerd01
  8. // @match https://www.reddit.com/robin*
  9. // @grant none
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // ==/UserScript==
  13. (function() {
  14.  
  15. // Grab users username + play nice with RES
  16. var robin_user = $("#header-bottom-right .user a").first().text();
  17. var ignored_users = {};
  18.  
  19. // for spam counter - very important i know :P
  20. var blocked_spam_el = null;
  21. var blocked_spam = 0;
  22. var user_last_message = '';
  23. //
  24. var _robin_grow_detected = false;
  25.  
  26. // Play nice with Greasemonkey
  27. if(typeof GM_getValue === "undefined") GM_getValue = function(){return false;};
  28. if(typeof GM_setValue === "undefined") GM_setValue = function(){return false;};
  29.  
  30. /**
  31. * Pull tabber out in to semi-stand alone module
  32. * Big thanks to netnerd01 for his pre-work on this
  33. *
  34. * Basic usage - tabbedChannels.init( dom_node_to_add_tabs_to );
  35. * and hook up tabbedChannels.proccessLine(lower_case_text, jquery_of_line_container); to each line detected by the system
  36. */
  37. var tabbedChannels = new function(){
  38. var _self = this;
  39.  
  40. // Default options
  41. this.channels = ["~","*",".","%","$","#",";","^","<3",":gov","#rpg","@"];
  42. this.mode = 'single';
  43.  
  44. // internals
  45. this.unread_counts = {};
  46. this.$el = null;
  47. this.$opt = null;
  48. this.defaultRoomClasses = '';
  49. this.channelMatchingCache = [];
  50.  
  51. //channels user is in currently
  52. this.currentRooms = 0;
  53.  
  54. // When channel is clicked, toggle it on or off
  55. this.toggle_channel = function(e){
  56. var channel = $(e.target).data("filter");
  57. if(channel===null)return; // no a channel
  58.  
  59. if(!$("#robinChatWindow").hasClass("robin-filter-" + channel)){
  60. _self.enable_channel(channel);
  61. $(e.target).addClass("selected");
  62. // clear unread counter
  63. $(e.target).find("span").text(0);
  64. _self.unread_counts[channel] = 0;
  65. }else{
  66. _self.disable_channel(channel);
  67. $(e.target).removeClass("selected");
  68. }
  69.  
  70. // scroll everything correctly
  71. _scroll_to_bottom();
  72. };
  73.  
  74. // Enable a channel
  75. this.enable_channel = function(channel_id){
  76.  
  77. // if using room type "single", deslect other rooms on change
  78. if(this.mode == "single"){
  79. this.disable_all_channels();
  80. }
  81.  
  82. $("#robinChatWindow").addClass("robin-filter robin-filter-" + channel_id);
  83. $("#robinChatWindow").attr("data-channel-key", this.channels[channel_id]);
  84. this.currentRooms++;
  85. // unselect show all
  86. _self.$el.find("span.all").removeClass("selected");
  87. };
  88.  
  89. // disable a channel
  90. this.disable_channel = function(channel_id){
  91. $("#robinChatWindow").removeClass("robin-filter-" + channel_id);
  92. this.currentRooms--;
  93.  
  94. // no rooms selcted, run "show all"
  95. if(this.currentRooms == 0){
  96. this.disable_all_channels();
  97. }else{
  98. // Grab next channel name if u leave a room in multi mode
  99. $("#robinChatWindow").attr("data-channel-key", $(".robin-filters span.selected").first().data("filter-name"));
  100. }
  101. };
  102.  
  103. // turn all channels off
  104. this.disable_all_channels = function(e){
  105. $("#robinChatWindow").attr("class", _self.defaultRoomClasses);
  106. _self.$el.find(".robin-filters > span").removeClass("selected");
  107. this.currentRooms = 0;
  108.  
  109. _self.$el.find("span.all").addClass("selected");
  110. _scroll_to_bottom();
  111. };
  112.  
  113. // render tabs
  114. this.drawTabs = function(){
  115. html = '';
  116. for(var i in this.channels){
  117. if(typeof this.channels[i] === 'undefined') continue;
  118. html += '<span data-filter="' + i + '" data-filter-name="'+ this.channels[i] +'">' + this.channels[i] + ' (<span>0</span>)</span> ';
  119. }
  120. this.$el.find(".robin-filters").html(html);
  121. };
  122.  
  123. // Add new channel
  124. this.addChannel = function(new_channel){
  125. if(this.channels.indexOf(new_channel) === -1){
  126. this.channels.push(new_channel);
  127. this.unread_counts[this.channels.length-1] = 0;
  128. this.updateChannelMatchCache();
  129. this.saveChannelList();
  130. this.drawTabs();
  131.  
  132. // refresh everything after redraw
  133. this.disable_all_channels();
  134. }
  135. };
  136.  
  137. // remove existing channel
  138. this.removeChannel = function(channel){
  139. if(confirm("are you sure you wish to remove the " + channel + " channel?")){
  140. var idx = this.channels.indexOf(channel);
  141. delete this.channels[idx];
  142. this.updateChannelMatchCache();
  143. this.saveChannelList();
  144. this.drawTabs();
  145. // refresh everything after redraw
  146. this.disable_all_channels();
  147. }
  148. };
  149.  
  150.  
  151. // save channel list
  152. this.saveChannelList = function(){
  153. // clean array before save
  154. var channels = this.channels.filter(function (item) { return item != undefined });
  155. GM_setValue("robin-enhance-channels", channels);
  156. };
  157.  
  158. // Change chat mode
  159. this.changeChannelMode = function(e){
  160. _self.mode = $(this).data("type");
  161.  
  162. // swicth bolding
  163. $(this).parent().find("span").css("font-weight","normal");
  164. $(this).css("font-weight","bold");
  165. _self.disable_all_channels();
  166.  
  167. // Update mode setting
  168. GM_setValue("robin-enhance-mode", _self.mode);
  169. };
  170.  
  171. this.updateChannelMatchCache = function(){
  172. var order = this.channels.slice(0);
  173. order.sort(function(a, b){
  174. return b.length - a.length; // ASC -> a - b; DESC -> b - a
  175. });
  176. for(var i in order){
  177. order[i] = this.channels.indexOf(order[i]);
  178. }
  179. // sorted array of channel name indexs
  180.  
  181. this.channelMatchingCache = order;
  182. }
  183.  
  184. // Procces each chat line to create text
  185. this.proccessLine = function(text, $element){
  186. var i, idx, channel;
  187. for(i=0; i< this.channelMatchingCache.length; i++){
  188. idx = this.channelMatchingCache[i];
  189. channel = this.channels[idx];
  190.  
  191. if(typeof channel === 'undefined') continue;
  192.  
  193. if(text.indexOf(channel) === 0){
  194. $element.addClass("robin-filter-" + idx +" in-channel");
  195. this.unread_counts[idx]++;
  196. return;
  197. }
  198. }
  199. };
  200.  
  201. // If in one channel, auto add channel keys
  202. this.submit_helper = function(){
  203. if($("#robinChatWindow").hasClass("robin-filter")){
  204. // auto add channel key
  205. var channel_key = $("#robinChatWindow").attr("data-channel-key");
  206.  
  207. if($(".text-counter-input").val().indexOf("/me") === 0){
  208. $(".text-counter-input").val("/me " + channel_key + " " + $(".text-counter-input").val().substr(3));
  209. }else if($(".text-counter-input").val().indexOf("/") !== 0){
  210. // if its not a "/" command, add channel
  211. $(".text-counter-input").val(channel_key + " " + $(".text-counter-input").val());
  212. }
  213. }
  214. };
  215.  
  216. // Update everuything
  217. this.tick = function(){
  218. _self.$el.find(".robin-filters span").each(function(){
  219. if($(this).hasClass("selected")) return;
  220. $(this).find("span").text(_self.unread_counts[$(this).data("filter")]);
  221. });
  222. };
  223.  
  224. // Init tab zone
  225. this.init = function($el){
  226. // Load channels
  227. if(GM_getValue("robin-enhance-channels")){
  228. this.channels = GM_getValue("robin-enhance-channels");
  229. }
  230. if(GM_getValue("robin-enhance-mode")){
  231. this.mode = GM_getValue("robin-enhance-mode");
  232. }
  233.  
  234. // init counters
  235. for(var i in this.channels){
  236. this.unread_counts[i] = 0;
  237. }
  238.  
  239. // update channel cache
  240. this.updateChannelMatchCache();
  241.  
  242. // set up el
  243. this.$el = $el;
  244.  
  245. // Create inital markup
  246. this.$el.html("<span class='all selected'>Everything</span><span><div class='robin-filters'></div></span><span class='more'>[Options]</span>");
  247. this.$opt = $("<div class='robin-channel-add' style='display:none'><input name='add-channel'><button>Add channel</button> <span class='channel-mode'>Channel Mode: <span title='View one channel at a time' data-type='single'>Single</span> | <span title='View many channels at once' data-type='multi'>Multi</span></span></div>").insertAfter(this.$el);
  248.  
  249. // Attach events
  250. this.$el.find(".robin-filters").click(this.toggle_channel);
  251. this.$el.find("span.all").click(this.disable_all_channels);
  252. this.$el.find("span.more").click(function(){ $(".robin-channel-add").slideToggle(); });
  253. this.$el.find(".robin-filters").bind("contextmenu", function(e){
  254. e.preventDefault();
  255. e.stopPropagation();
  256. var chan_id = $(e.target).data("filter");
  257. if(chan_id===null)return; // no a channel
  258. _self.removeChannel(_self.channels[chan_id]);
  259. });
  260. // Form events
  261. this.$opt.find(".channel-mode span").click(this.changeChannelMode);
  262. this.$opt.find("button").click(function(){
  263. var new_chan = _self.$opt.find("input[name='add-channel']").val();
  264. if(new_chan != '') _self.addChannel(new_chan);
  265. _self.$opt.find("input[name='add-channel']").val('');
  266. });
  267.  
  268. $("#robinSendMessage").submit(this.submit_helper);
  269. // store default room class
  270. this.defaultRoomClasses = $("#robinChatWindow").attr("class");
  271.  
  272. // redraw tabs
  273. this.drawTabs();
  274.  
  275. // start ticker
  276. setInterval(this.tick, 1000);
  277. }
  278. };
  279.  
  280. /**
  281. * Check if a message is "spam"
  282. */
  283. var is_spam = function(line){
  284. return (
  285. // Hide auto vote messages
  286. (/^voted to (grow|stay|abandon)/.test(line)) ||
  287. // random unicode?
  288. (/[\u0080-\uFFFF]/.test(line)) ||
  289. // hide any auto voter messages
  290. (/\[.*autovoter.*\]/.test(line)) ||
  291. // Common bots
  292. (/^(\[binbot\]|\[robin-grow\])/.test(line)) ||
  293. // repeating chars in line (more than 5). e.g. aaaaaaa !!!!!!!!
  294. (/(.)\1{5,}/.test(line)) ||
  295. // Some common messages
  296. (/(voting will end in approximately|\[i spam the most used phrase\]|\[message from creator\]|\[.*bot.*\])/.test(line)) ||
  297. // no spaces = spam if its longer than 25 chars (dont filter links)
  298. (line.indexOf(" ") === -1 && line.length > 25 && line.indexOf("http") === -1) ||
  299. // repeating same word
  300. /(\b\S+\b)\s+\b\1\b/i.test(line)
  301. );
  302. };
  303.  
  304. /**
  305. * Check if a message is from an ignored user
  306. *
  307. */
  308. var is_ignored = function($usr, $ele){
  309. // no user name, go looking for when said it
  310. if($usr.length === 0){
  311. while($usr.length === 0){
  312. $ele = $ele.prev();
  313. $usr = $ele.find(".robin--username");
  314. }
  315. }
  316. // are they ignored?
  317. return (ignored_users[$usr.text()]);
  318. };
  319.  
  320. /**
  321. * Make links clickable
  322. *
  323. */
  324. var auto_link = function($msg){
  325. var text = $msg.html(); // read as html so stuff stays escaped
  326. // normal links
  327. text = text.replace(/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim, '<a target="blank" href="$&">$&</a>');
  328. // reddit subreddit links
  329. text = text.replace(/ \/r\/(\w+)/gim, ' <a target="blank" href="https://reddit.com/r/$1">/r/$1</a>');
  330. // update text
  331. $msg.html(text);
  332. };
  333.  
  334. /**
  335. * Mute a user
  336. */
  337. var _mute_user = function(usr){
  338. // Add to ignore list
  339. ignored_users[usr] = true;
  340. _render_muted_list();
  341. };
  342.  
  343. /**
  344. * un-mute a user
  345. */
  346. var _unmute_user = function(usr){
  347. // Add to ignore list
  348. delete ignored_users[usr];
  349. _render_muted_list();
  350. };
  351.  
  352. // Render list of ignored users
  353. var _render_muted_list = function(){
  354. var html = "<strong>Ignored users</strong><br>";
  355. for(var u in ignored_users){
  356. html += "<div data-usr='"+ u + "'>" + u + " - [unmute]</div>";
  357. }
  358. $("#muted_users").html(html);
  359. };
  360.  
  361. // Scroll chat back to bottom
  362. var _scroll_to_bottom = function(){
  363. $("#robinChatWindow").scrollTop($("#robinChatMessageList").height());
  364. };
  365.  
  366. var update_spam_count = function(){
  367. blocked_spam++;
  368. blocked_spam_el.innerHTML = blocked_spam;
  369. };
  370.  
  371. var fill_name = function(e){
  372. e.preventDefault();
  373. e.stopPropagation();
  374.  
  375. // if text area blank, prefill name. if not, stick it on the end
  376. if($(".text-counter-input").val() === ''){
  377. $(".text-counter-input").val($(this).text() + ' ').focus();
  378. }else{
  379. $(".text-counter-input").val($(".text-counter-input").val() + ' ' + $(this).text()).focus();
  380. }
  381. };
  382.  
  383. /**
  384. * Parse a link and apply changes
  385. */
  386. var parse_line = function($ele){
  387. var $msg = $ele.find(".robin-message--message");
  388. var $usr = $ele.find(".robin--username");
  389. var line = $msg.text().toLowerCase();
  390. // dont parse system messages
  391. if($ele.hasClass("robin--user-class--system")){
  392. if(line.indexOf("ratelimit | you are doing that too much") !== -1){
  393. $(".text-counter-input").val(user_last_message);
  394. }
  395. return;
  396. }
  397.  
  398. // If user is ignored or message looks like "Spam". hide it
  399. if (is_ignored($usr, $ele) || is_spam(line)) {
  400. $ele.addClass("spam-hidden");
  401. update_spam_count();
  402. }
  403.  
  404. // Highlight mentions
  405. if(line.indexOf(robin_user) !== -1){
  406. $ele.addClass("user-mention");
  407. }
  408.  
  409. // Make links clickable
  410. if(!_robin_grow_detected && line.indexOf("http") !== -1){
  411. auto_link($msg);
  412. }
  413.  
  414. // Add mute button to users
  415. if(!$ele.hasClass("robin--user-class--system") && $usr.text() != robin_user){
  416. $("<span style='font-size:.8em;cursor:pointer'> [mute] </span>").insertBefore($usr).click(function(){
  417. _mute_user($usr.text());
  418. });
  419. }
  420.  
  421. // Track channels
  422. tabbedChannels.proccessLine(line, $ele);
  423.  
  424. // bind click to use (override other click events if we can)
  425. $usr.bindFirst("click", fill_name);
  426. };
  427.  
  428.  
  429. // Detect changes, are parse the new message
  430. $("#robinChatWindow").on('DOMNodeInserted', function(e) {
  431. if ($(e.target).is('div.robin-message')) {
  432. // Apply changes to line
  433. parse_line($(e.target));
  434. }
  435. });
  436.  
  437. // When everything is ready
  438. $(document).ready(function(){
  439.  
  440. // Set default spam filter type
  441. $("#robinChatWindow").addClass("hide-spam");
  442.  
  443. // Add checkbox to toggle "hide" behaviors
  444. $("#robinDesktopNotifier").append("<label><input type='checkbox' checked='checked'>Hide spam completely (<span id='spamcount'>0</span> removed)</label>").click(function(){
  445. if($(this).find("input").is(':checked')){
  446. $("#robinChatWindow").removeClass("mute-spam").addClass("hide-spam");
  447. }else{
  448. $("#robinChatWindow").removeClass("hide-spam").addClass("mute-spam");
  449. }
  450. // correct scroll after spam filter change
  451. _scroll_to_bottom();
  452. });
  453.  
  454. blocked_spam_el = $("#spamcount")[0];
  455.  
  456. // Add Muted list & hook up unmute logic
  457. $('<div id="muted_users" class="robin-chat--sidebar-widget robin-chat--notification-widget"><strong>Ignored users</strong></div>').insertAfter($("#robinDesktopNotifier"));
  458. $('#muted_users').click(function(e){
  459. var user = $(e.target).data("usr");
  460. if(user) _unmute_user(user);
  461. });
  462.  
  463. // Init tabbed channels
  464. tabbedChannels.init($('<div id="filter_tabs"></div>').insertAfter("#robinChatWindow"));
  465.  
  466. // store i copy of last message, in case somthing goes wrong (rate limit)
  467. $("#robinSendMessage").submit(function(){
  468. user_last_message = $(".text-counter-input").val();
  469. });
  470.  
  471. });
  472.  
  473. // fix by netnerd01
  474. var stylesheet = document.createElement('style');
  475. document.head.appendChild(stylesheet);
  476. stylesheet = stylesheet.sheet;
  477.  
  478. // filter for channel
  479. stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message { display:none; }", 0);
  480. stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message.robin--user-class--system { display:block; }", 0);
  481. for(var c=0;c<35;c++){
  482. stylesheet.insertRule("#robinChatWindow.robin-filter.robin-filter-"+c+" div.robin-message.robin-filter-"+c+" { display:block; }", 0);
  483. }
  484.  
  485. // Styles for filter tabs
  486. stylesheet.insertRule("#filter_tabs {width:100%; display: table; table-layout: fixed; background:#d7d7d2; border-bottom:1px solid #efefed;}",0);
  487. stylesheet.insertRule("#filter_tabs > span {width:90%; display: table-cell;}",0);
  488. stylesheet.insertRule("#filter_tabs > span.all, #filter_tabs > span.more {width:60px; text-align:center; vertical-align:middle; cursor:pointer;}",0);
  489. stylesheet.insertRule("#filter_tabs > span.all.selected, #filter_tabs > span.all.selected:hover {background: #fff;}", 0);
  490. stylesheet.insertRule("#filter_tabs .robin-filters { display: table; width:100%;table-layout: fixed; '}", 0);
  491. stylesheet.insertRule("#filter_tabs .robin-filters > span { padding: 5px 2px;text-align: center; display: table-cell; cursor: pointer;width:2%; vertical-align: middle; font-size: 1.1em;}", 0);
  492. stylesheet.insertRule("#filter_tabs .robin-filters > span.selected, #filter_tabs .robin-filters > span:hover { background: #fff;}", 0);
  493. stylesheet.insertRule("#filter_tabs .robin-filters > span > span {pointer-events: none;}", 0);
  494.  
  495. stylesheet.insertRule(".robin-channel-add {padding:5px; display:none;}", 0);
  496. stylesheet.insertRule(".robin-channel-add input {padding: 2.5px; }", 0);
  497. stylesheet.insertRule(".robin-channel-add .channel-mode {float:right; font-size:1.2em;padding:5px;}", 0);
  498. stylesheet.insertRule(".robin-channel-add .channel-mode span {cursor:pointer}", 0);
  499. //mentions should show even in filter view
  500. stylesheet.insertRule("#robinChat #robinChatWindow div.robin-message.user-mention { display:block; font-weight:bold; }", 0);
  501.  
  502. // Add initial styles for "spam" messages
  503. stylesheet.insertRule("#robinChat #robinChatWindow.hide-spam div.robin-message.spam-hidden { display:none; }", 0);
  504. stylesheet.insertRule("#robinChat #robinChatWindow.mute-spam div.robin-message.spam-hidden { opacity:0.3; font-size:1.2em; }", 0);
  505.  
  506. // muted user box
  507. stylesheet.insertRule("#muted_users { font-size:1.2em; }", 0);
  508. stylesheet.insertRule("#muted_users div { padding: 2px 0; }", 0);
  509. stylesheet.insertRule("#muted_users strong { font-weight:bold; }", 0);
  510.  
  511. // FIX RES nightmode (ish) [ by Kei ]
  512. stylesheet.insertRule(".res-nightmode #robinChatWindow div.robin-message { color: #ccc; }", 0);
  513. stylesheet.insertRule(".res-nightmode .robin-chat--sidebar-widget { background: #222; color: #ccc;}", 0);
  514. stylesheet.insertRule(".res-nightmode .robin-room-participant { background: #222; color: #999;}", 0);
  515. stylesheet.insertRule(".res-nightmode #filter_tabs {background: rgb(51, 51, 51);}", 0);
  516. stylesheet.insertRule(".res-nightmode #filter_tabs .robin-filters > span.selected,.res-nightmode #filter_tabs .robin-filters > span:hover,.res-nightmode #filter_tabs > span.all.selected,.res-nightmode #filter_tabs > span.all:hover {background: rgb(34, 34, 34)}", 0);
  517. stylesheet.insertRule(".res-nightmode .robin-chat--input { background: #222 }", 0);
  518. stylesheet.insertRule(".res-nightmode .robin--presence-class--away .robin--username {color: #999;}", 0);
  519. stylesheet.insertRule(".res-nightmode .robin--presence-class--present .robin--username {color: #ccc;}", 0);
  520. stylesheet.insertRule(".res-nightmode #robinChat .robin--user-class--self .robin--username { color: #999; }", 0);
  521. stylesheet.insertRule(".res-nightmode .robin-chat--vote { background: #777; color: #ccc;}", 0);
  522. stylesheet.insertRule(".res-nightmode .robin-chat--buttons button.robin-chat--vote.robin--active { background: #ccc; color:#999; }", 0);
  523.  
  524. $(document).ready(function(){
  525. setTimeout(function(){
  526. // Play nice with robin grow (makes room for tab bar we insert)
  527. if($(".usercount.robin-chat--vote").length !== 0){
  528. _robin_grow_detected = true;
  529. stylesheet.insertRule("#robinChat.robin-chat .robin-chat--body { height: calc(100vh - 150px); }", 0);
  530. }
  531. },500);
  532. });
  533.  
  534. // Allow me to sneek functions in front of other libaries - used when working with robin grow >.< sorry guys
  535. //http://stackoverflow.com/questions/2360655/jquery-event-handlers-always-execute-in-order-they-were-bound-any-way-around-t
  536. $.fn.bindFirst = function(name, fn) {
  537. // bind as you normally would
  538. // don't want to miss out on any jQuery magic
  539. this.on(name, fn);
  540.  
  541. // Thanks to a comment by @Martin, adding support for
  542. // namespaced events too.
  543. this.each(function() {
  544. var handlers = $._data(this, 'events')[name.split('.')[0]];
  545. // take out the handler we just inserted from the end
  546. var handler = handlers.pop();
  547. // move it at the beginning
  548. handlers.splice(0, 0, handler);
  549. });
  550. };
  551.  
  552. })();