Robin Enhancement Script

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

目前為 2016-04-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name		Robin Enhancement Script
// @namespace	https://www.reddit.com/
// @version		3.1.0
// @description	Highlight mentions, make link clickable, use channels & automatically remove spam
// @author		Bag
// @author		netnerd01
// @match		https://www.reddit.com/robin*
// @grant		none
// @grant		GM_setValue
// @grant		GM_getValue
// ==/UserScript==
(function() {

	// Grab users username + play nice with RES
	var robin_user = $("#header-bottom-right .user a").first().text();
	var ignored_users = {};

	// for spam counter - very important i know :P
	var blocked_spam_el = null;
	var blocked_spam = 0;
	var user_last_message = '';
	//
	var _robin_grow_detected = false;

	/**
	 * Pull tabber out in to semi-stand alone module
	 * Big thanks to netnerd01 for his pre-work on this
	 *
	 * Basic usage - tabbedChannels.init( dom_node_to_add_tabs_to );
	 * and hook up tabbedChannels.proccessLine(lower_case_text, jquery_of_line_container); to each line detected by the system
	 */
	var tabbedChannels = new function(){
		var _self = this;

		// Default options
		this.channels = ["~","*",".","%","$","#",";","^","<3",":gov","#rpg","@"];
		this.mode = 'single';

		// internals
		this.unread_counts = {};
		this.$el = null;
		this.$opt = null;
		this.defaultRoomClasses = '';
		this.channelMatchingCache = [];

		//channels user is in currently
		this.currentRooms = 0;

		// When channel is clicked, toggle it on or off
		this.toggle_channel = function(e){
			var channel = $(e.target).data("filter");
			if(channel===null)return; // no a channel

			if(!$("#robinChatWindow").hasClass("robin-filter-" + channel)){
				_self.enable_channel(channel);
				$(e.target).addClass("selected");
				// clear unread counter
				$(e.target).find("span").text(0);
				_self.unread_counts[channel] = 0;
			}else{
				_self.disable_channel(channel);
				$(e.target).removeClass("selected");
			}

			// scroll everything correctly
			_scroll_to_bottom();
		};

		// Enable a channel
		this.enable_channel = function(channel_id){

			// if using room type "single", deslect other rooms on change
			if(this.mode == "single"){
				this.disable_all_channels();
			}

			$("#robinChatWindow").addClass("robin-filter robin-filter-" + channel_id);
			$("#robinChatWindow").attr("data-channel-key", this.channels[channel_id]);
			this.currentRooms++;
			// unselect show all 
			_self.$el.find("span.all").removeClass("selected");
		};

		// disable a channel
		this.disable_channel = function(channel_id){	
			$("#robinChatWindow").removeClass("robin-filter-" + channel_id);
			this.currentRooms--;

			// no rooms selcted, run "show all"
			if(this.currentRooms == 0) this.disable_all_channels();
		};

		// turn all channels off
		this.disable_all_channels = function(e){
			$("#robinChatWindow").attr("class", _self.defaultRoomClasses);
			_self.$el.find(".robin-filters > span").removeClass("selected");
			this.currentRooms = 0;

			_self.$el.find("span.all").addClass("selected");
			_scroll_to_bottom();
		};

		// render tabs
		this.drawTabs = function(){
			html = '';
			for(var i in this.channels){
				if(typeof this.channels[i] === 'undefined') continue;
				html += '<span data-filter="' + i + '" data-filter-name="'+ this.channels[i] +'">' + this.channels[i] + ' (<span>0</span>)</span> '; 
			}
			this.$el.find(".robin-filters").html(html);
		};

		// Add new channel
		this.addChannel = function(new_channel){
			if(this.channels.indexOf(new_channel) === -1){
				this.channels.push(new_channel);
				this.unread_counts[this.channels.length-1] = 0;
				this.updateChannelMatchCache();
				this.saveChannelList();
				this.drawTabs();

				// refresh everything after redraw
				this.disable_all_channels();
			}
		};

		// remove existing channel
		this.removeChannel = function(channel){
			if(confirm("are you sure you wish to remove the " + channel + " channel?")){
				var idx = this.channels.indexOf(channel);
				delete this.channels[idx];
				this.updateChannelMatchCache();
				this.saveChannelList();
				this.drawTabs();
				// refresh everything after redraw
				this.disable_all_channels();
			}
		};


		// save channel list
		this.saveChannelList = function(){
			// clean array before save
			var channels = this.channels.filter(function (item) { return item != undefined });
			GM_setValue("robin-enhance-channels", channels);
		};

		// Change chat mode
		this.changeChannelMode = function(e){
			_self.mode = $(this).data("type");

			// swicth bolding
			$(this).parent().find("span").css("font-weight","normal");
			$(this).css("font-weight","bold");
			_self.disable_all_channels();

			// Update mode setting
			GM_setValue("robin-enhance-mode", _self.mode);
		};

		this.updateChannelMatchCache = function(){
			var order = this.channels.slice(0);
			order.sort(function(a, b){
			  return b.length - a.length; // ASC -> a - b; DESC -> b - a
			});
			for(var i in order){
				order[i] = this.channels.indexOf(order[i]);
			}
			// sorted array of channel name indexs

			this.channelMatchingCache = order;
		}

		// Procces each chat line to create text
		this.proccessLine = function(text, $element){
			var i, idx, channel;
			for(i=0; i< this.channelMatchingCache.length; i++){
				idx = this.channelMatchingCache[i];
				channel = this.channels[idx];

				if(typeof channel === 'undefined') continue;

				if(text.indexOf(channel) === 0){
					$element.addClass("robin-filter-" + idx +" in-channel");
					this.unread_counts[idx]++;
					return;
				}
			}
		};

		// If in one channel, auto add channel keys
		this.submit_helper = function(){
			if($("#robinChatWindow").hasClass("robin-filter")){
				// auto add channel key
				var channel_key = $("#robinChatWindow").attr("data-channel-key");

				if($(".text-counter-input").val().indexOf("/me") === 0){
					$(".text-counter-input").val("/me " + channel_key + " " + $(".text-counter-input").val().substr(3));
				}else if($(".text-counter-input").val().indexOf("/") !== 0){
					// if its not a "/" command, add channel
					$(".text-counter-input").val(channel_key + " " + $(".text-counter-input").val());
				}
			}
		};

		// Update everuything
		this.tick = function(){
			_self.$el.find(".robin-filters span").each(function(){
				if($(this).hasClass("selected")) return;
				$(this).find("span").text(_self.unread_counts[$(this).data("filter")]);
			});
		};

		// Init tab zone
		this.init = function($el){
			// Load channels
			if(GM_getValue("robin-enhance-channels")){
				this.channels = GM_getValue("robin-enhance-channels");
			}
			if(GM_getValue("robin-enhance-mode")){
				this.mode = GM_getValue("robin-enhance-mode");
			}

			// init counters
			for(var i in this.channels){
				this.unread_counts[i] = 0;
			}

			// update channel cache
			this.updateChannelMatchCache();

			// set up el
			this.$el = $el;

			// Create inital markup
			this.$el.html("<span class='all selected'>Everything</span><span><div class='robin-filters'></div></span><span class='more'>[Options]</span>");
			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);

			// Attach events
			this.$el.find(".robin-filters").click(this.toggle_channel);
			this.$el.find("span.all").click(this.disable_all_channels);
			this.$el.find("span.more").click(function(){ $(".robin-channel-add").slideToggle(); });
			this.$el.find(".robin-filters").bind("contextmenu", function(e){
				e.preventDefault();
				e.stopPropagation();
				var chan_id = $(e.target).data("filter");
				if(chan_id===null)return; // no a channel
				_self.removeChannel(_self.channels[chan_id]);
			});
			// Form events
			this.$opt.find(".channel-mode span").click(this.changeChannelMode);
			this.$opt.find("button").click(function(){
				var new_chan = _self.$opt.find("input[name='add-channel']").val();
				if(new_chan != '') _self.addChannel(new_chan);
				_self.$opt.find("input[name='add-channel']").val('');
			});
			

			$("#robinSendMessage").submit(this.submit_helper);
			
			// store default room class
			this.defaultRoomClasses = $("#robinChatWindow").attr("class");

			// redraw tabs
			this.drawTabs();

			// start ticker
			setInterval(this.tick, 1000);
		}
	};

	/**
	 * Check if a message is "spam"
	 */
	var is_spam = function(line){
		return (
			// Hide auto vote messages
			(/^voted to (grow|stay|abandon)/.test(line)) ||
			// random unicode?
			(/[\u0080-\uFFFF]/.test(line)) ||
			// hide any auto voter messages
			(/\[.*autovoter.*\]/.test(line)) ||
			// Common bots
			(/^(\[binbot\]|\[robin-grow\])/.test(line)) ||
			// repeating chars in line (more than 5). e.g. aaaaaaa !!!!!!!!
			(/(.)\1{5,}/.test(line)) ||
			// Some common messages
			(/(voting will end in approximately|\[i spam the most used phrase\]|\[message from creator\]|\[.*bot.*\])/.test(line)) ||
			// no spaces = spam if its longer than 25 chars (dont filter links)
			(line.indexOf(" ") === -1 && line.length > 25 && line.indexOf("http") === -1) ||
			// repeating same word
			/(\b\S+\b)\s+\b\1\b/i.test(line)
		);
	};

	/**
	 * Check if a message is from an ignored user
	 *
	 */
	var is_ignored = function($usr, $ele){
		// no user name, go looking for when said it
		if($usr.length === 0){
			while($usr.length === 0){
				$ele = $ele.prev();
				$usr = $ele.find(".robin--username");
			}
		}
		// are they ignored?
		return (ignored_users[$usr.text()]);
	};

	/**
	 * Make links clickable
	 *
	 */
	var auto_link = function($msg){
		var text = $msg.html(); // read as html so stuff stays escaped
		// normal links
		text = text.replace(/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim, '<a target="blank" href="$&">$&</a>');
		// reddit subreddit links
		text = text.replace(/ \/r\/(\w+)/gim, ' <a target="blank" href="https://reddit.com/r/$1">/r/$1</a>');
		// update text
		$msg.html(text);
	};

	/**
	 * Mute a user
	 */
	var _mute_user = function(usr){
		// Add to ignore list
		ignored_users[usr] = true;
		_render_muted_list();
	};

	/**
	 * un-mute a user
	 */
	var _unmute_user = function(usr){
		// Add to ignore list
		delete ignored_users[usr];
		_render_muted_list();
	};

	// Render list of ignored users
	var _render_muted_list = function(){
		var html = "<strong>Ignored users</strong><br>";
		for(var u in ignored_users){
			html += "<div data-usr='"+ u + "'>" + u + " - [unmute]</div>";
		}
		$("#muted_users").html(html);
	};

	// Scroll chat back to bottom
	var _scroll_to_bottom = function(){
		$("#robinChatWindow").scrollTop($("#robinChatMessageList").height());
	};

	var update_spam_count = function(){
		blocked_spam++;
		blocked_spam_el.innerHTML = blocked_spam;
	};

	var fill_name = function(e){
		e.preventDefault();
		e.stopPropagation();

		// if text area blank, prefill name. if not, stick it on the end
		if($(".text-counter-input").val() === ''){
			$(".text-counter-input").val($(this).text() + ' ').focus();
		}else{
			$(".text-counter-input").val($(".text-counter-input").val() + ' ' + $(this).text()).focus();
		}
	};

	/**
	 * Parse a link and apply changes
	 */
	var parse_line = function($ele){
		var $msg = $ele.find(".robin-message--message");
		var $usr = $ele.find(".robin--username");
		var line = $msg.text().toLowerCase();
		// dont parse system messages
		if($ele.hasClass("robin--user-class--system")){
			if(line.indexOf("ratelimit | you are doing that too much") !== -1){
				$(".text-counter-input").val(user_last_message);
			}
			return;
		}

		// If user is ignored or message looks like "Spam". hide it
		if (is_ignored($usr, $ele) || is_spam(line)) {
			$ele.addClass("spam-hidden");
			update_spam_count();
		}

		// Highlight mentions
		if(line.indexOf(robin_user) !== -1){
			$ele.addClass("user-mention");
		}

		// Make links clickable
		if(!_robin_grow_detected && line.indexOf("http") !== -1){
			auto_link($msg);
		}

		// Add mute button to users
		if(!$ele.hasClass("robin--user-class--system") && $usr.text() != robin_user){
			$("<span style='font-size:.8em;cursor:pointer'> [mute] </span>").insertBefore($usr).click(function(){
				_mute_user($usr.text());
			});
		}

		// Track channels
		tabbedChannels.proccessLine(line, $ele);

		// bind click to use (override other click events if we can)
		$usr.bindFirst("click", fill_name);
	};


	// Detect changes, are parse the new message
	$("#robinChatWindow").on('DOMNodeInserted', function(e) {
		if ($(e.target).is('div.robin-message')) {
			// Apply changes to line
			parse_line($(e.target));
		}
	});

	// When everything is ready
	$(document).ready(function(){

		// Set default spam filter type
		$("#robinChatWindow").addClass("hide-spam");

		// Add checkbox to toggle "hide" behaviors
		$("#robinDesktopNotifier").append("<label><input type='checkbox' checked='checked'>Hide spam completely (<span id='spamcount'>0</span> removed)</label>").click(function(){
			if($(this).find("input").is(':checked')){
				$("#robinChatWindow").removeClass("mute-spam").addClass("hide-spam");
			}else{
				$("#robinChatWindow").removeClass("hide-spam").addClass("mute-spam");
			}
			// correct scroll after spam filter change
			_scroll_to_bottom();
		});

		blocked_spam_el = $("#spamcount")[0];

		// Add Muted list & hook up unmute logic
		$('<div id="muted_users" class="robin-chat--sidebar-widget robin-chat--notification-widget"><strong>Ignored users</strong></div>').insertAfter($("#robinDesktopNotifier"));
		$('#muted_users').click(function(e){
			var user = $(e.target).data("usr");
			if(user) _unmute_user(user);
		});

		// Init tabbed channels
		tabbedChannels.init($('<div id="filter_tabs"></div>').insertAfter("#robinChatWindow"));

		// store i copy of last message, in case somthing goes wrong (rate limit)
		$("#robinSendMessage").submit(function(){
			user_last_message = $(".text-counter-input").val();
		});

	});

	// fix by netnerd01
	var stylesheet = document.createElement('style');
	document.head.appendChild(stylesheet);
	stylesheet = stylesheet.sheet;

	// filter for channel
	stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message { display:none; }", 0);
	stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message.robin--user-class--system  { display:block; }", 0);
	for(var c=0;c<35;c++){
		stylesheet.insertRule("#robinChatWindow.robin-filter.robin-filter-"+c+" div.robin-message.robin-filter-"+c+" { display:block; }", 0);
	}

	// Styles for filter tabs
	stylesheet.insertRule("#filter_tabs {width:100%; display: table; table-layout: fixed; background:#d7d7d2; border-bottom:1px solid #efefed;}",0);
	stylesheet.insertRule("#filter_tabs > span {width:90%; display: table-cell;}",0);
	stylesheet.insertRule("#filter_tabs > span.all, #filter_tabs > span.more {width:60px; text-align:center; vertical-align:middle; cursor:pointer;}",0);
	stylesheet.insertRule("#filter_tabs > span.all.selected, #filter_tabs > span.all.selected:hover {background: #fff;}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters { display: table; width:100%;table-layout: fixed; '}", 0);
	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);
	stylesheet.insertRule("#filter_tabs .robin-filters > span.selected, #filter_tabs .robin-filters > span:hover { background: #fff;}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters > span > span {pointer-events: none;}", 0);

	stylesheet.insertRule(".robin-channel-add  {padding:5px; display:none;}", 0);
	stylesheet.insertRule(".robin-channel-add input {padding: 2.5px; }", 0);
	stylesheet.insertRule(".robin-channel-add .channel-mode {float:right; font-size:1.2em;padding:5px;}", 0);
	stylesheet.insertRule(".robin-channel-add .channel-mode span {cursor:pointer}", 0);
	//mentions should show even in filter view
	stylesheet.insertRule("#robinChat #robinChatWindow div.robin-message.user-mention { display:block; font-weight:bold; }", 0);

	// Add initial styles for "spam" messages
	stylesheet.insertRule("#robinChat #robinChatWindow.hide-spam div.robin-message.spam-hidden { display:none; }", 0);
	stylesheet.insertRule("#robinChat #robinChatWindow.mute-spam div.robin-message.spam-hidden { opacity:0.3; font-size:1.2em; }", 0);

	// muted user box
	stylesheet.insertRule("#muted_users { font-size:1.2em; }", 0);
	stylesheet.insertRule("#muted_users div { padding: 2px 0; }", 0);
	stylesheet.insertRule("#muted_users strong { font-weight:bold; }", 0);

	// FIX RES nightmode (ish) [ by Kei ]
	stylesheet.insertRule(".res-nightmode #robinChatWindow div.robin-message { color: #ccc; }", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--sidebar-widget { background: #222; color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode .robin-room-participant { background: #222; color: #999;}", 0);
	stylesheet.insertRule(".res-nightmode #filter_tabs {background: rgb(51, 51, 51);}", 0);
	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);
	stylesheet.insertRule(".res-nightmode .robin-chat--input { background: #222 }", 0);
	stylesheet.insertRule(".res-nightmode .robin--presence-class--away .robin--username {color: #999;}", 0);
	stylesheet.insertRule(".res-nightmode .robin--presence-class--present .robin--username {color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode #robinChat .robin--user-class--self .robin--username { color: #999; }", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--vote { background: #777; color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--buttons button.robin-chat--vote.robin--active { background: #ccc; color:#999; }", 0);

	$(document).ready(function(){
		setTimeout(function(){
			// Play nice with robin grow (makes room for tab bar we insert)
			if($(".usercount.robin-chat--vote").length !== 0){
				_robin_grow_detected = true;
				stylesheet.insertRule("#robinChat.robin-chat .robin-chat--body { height: calc(100vh - 150px); }", 0);
			}
		},500);
	});

	// Allow me to sneek functions in front of other libaries - used when working with robin grow >.< sorry guys
	//http://stackoverflow.com/questions/2360655/jquery-event-handlers-always-execute-in-order-they-were-bound-any-way-around-t
	$.fn.bindFirst = function(name, fn) {
		// bind as you normally would
		// don't want to miss out on any jQuery magic
		this.on(name, fn);

		// Thanks to a comment by @Martin, adding support for
		// namespaced events too.
		this.each(function() {
			var handlers = $._data(this, 'events')[name.split('.')[0]];
			// take out the handler we just inserted from the end
			var handler = handlers.pop();
			// move it at the beginning
			handlers.splice(0, 0, handler);
		});
	};

})();

QingJ © 2025

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