// ==UserScript==
// @name Robin Enhancement Script
// @namespace https://www.reddit.com/
// @version 3.3.5
// @description Highlight mentions, make links clickable, add tabbed channels & automatically remove spam
// @author Bag, 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().toLowerCase();
var ignored_users = {};
// for spam counter - very important i know :P
var blocked_spam_el = null;
var blocked_spam = 0;
// via RobinEggs
var messageHistory = [];
var messageHistoryIndex = -1;
var _robin_grow_detected = false;
var colors = [
'rgba(255,0,0,0.1)',
'rgba(0,255,0,0.1)',
'rgba(0,0,255,0.1)',
'rgba(0,255,255,0.1)',
'rgba(255,0,255,0.1)',
'rgba(255,255,0,0.1)',
'rgba(211,211,211, .1)',
'rgba(0,100,0, .1)',
'rgba(255,20,147, .1)',
'rgba(184,134,11, .1)',
];
// Play nice with Greasemonkey
if(typeof GM_getValue === "undefined") GM_getValue = function(){return false;};
if(typeof GM_setValue === "undefined") GM_setValue = function(){return 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();
}else{
// Grab next channel name if u leave a room in multi mode
$("#robinChatWindow").attr("data-channel-key", $(".robin-filters span.selected").first().data("filter-name"));
}
};
// turn all channels off
this.disable_all_channels = function(e){
$("#robinChatWindow").attr("class", _self.defaultRoomClasses).attr("data-channel-key","");
_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);
};
// After creation of a new channel, go find if any content (not matched by a channel already) is relevant
this.reScanChannels = function(new_channel){
$("#robinChatWindow").find("div.robin-message").each(function(idx,item){
var line = $(item).find(".robin-message--message").text().toLowerCase();
tabbedChannels.proccessLine(line, $(item), true);
});
}
// 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();
// Populate content for channel
this.reScanChannels();
// 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();
// sub channels, will fall back to existing channels
this.reScanChannels();
// 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, rescan){
var i, idx, channel;
// If rescanning, clear any existing "channel" classes
if(typeof rescan !== 'undefined' && rescan === true){
$element.removeClass("in-channel");
for(i=0; i <= this.channels.length; i++){
$element.removeClass("robin-filter-" + i);
}
}
// Scann for channel identifiers
for(i=0; i< this.channelMatchingCache.length; i++){ // sorted so longer get picked out before shorter ones (sub channel matching)
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(/\s+\/r\/(\w+)\/?/gi, ' <a target="blank" href="https://reddit.com/r/$1">/r/$1</a>');
text = text.replace(/\s+\/u\/(\w+)\/?/gi, ' <a target="blank" href="https://reddit.com/u/$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());
};
// create persistant option
function createOption(name, click_action, default_state){
var checked_markup;
var key = "robin-enhance-" + name.replace(/\W/g, '');
var state = (typeof default_state !== "undefined") ? default_state : false;
// try and state if setting is defined
if(GM_getValue(key)){
state = (GM_getValue(key) === 'true') ? true : false;
}
// markup for state
checked_markup = (state === true) ? "checked='checked'" : "";
// render option
var $option = $("<label><input type='checkbox' "+checked_markup+">"+name+"</label>").click(function(){
var checked = $(this).find("input").is(':checked');
// persist state
if(checked != state){
GM_setValue(key, checked ? 'true' : 'false'); // true/false stored as strings, to avoid unset matching
state = checked;
}
click_action(checked, $(this));
});
// add to dom
$("#robinDesktopNotifier").append($option);
// init
click_action(state, $option)
};
// update spam count
var update_spam_count = function(){
blocked_spam++;
blocked_spam_el.innerHTML = blocked_spam;
};
// when name is clicked, fill it into the chat box
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();
}
};
// remove channel key from message
var remove_channel_key_from_message = function(message){
if($("#robinChatWindow").attr("data-channel-key")){
var offset = $("#robinChatWindow").attr("data-channel-key").length;
if(offset === 0) return message;
if(message.indexOf("/me") === 0){
return "/me "+ message.slice(offset+5);
}else{
return message.slice(offset+1);
}
}
return message;
}
/**
* 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(messageHistory[messageHistoryIndex-1]);
}
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 || line.indexOf("/r/") !== -1 || line.indexOf("/u/") !== -1)){
auto_link($msg);
}
// Add mute button to users
if(!$ele.hasClass("robin--user-class--system") && $usr.text().toLowerCase() != 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");
createOption("Hide spam completely (<span id='spamcount'>0</span> removed)", function(checked, ele){
if(checked){
$("#robinChat").removeClass("mute-spam").addClass("hide-spam");
}else{
$("#robinChat").removeClass("hide-spam").addClass("mute-spam");
}
// correct scroll after spam filter change
_scroll_to_bottom();
},true);
createOption("Use channel colors", function(checked, ele){
if(checked){
$("#robinChat").addClass("show-colors");
}else{
$("#robinChat").removeClass("show-colors");
}
// correct scroll after spam filter change
_scroll_to_bottom();
},false);
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(){
var user_last_message = $(".text-counter-input").val();
// if message history is to long, clear it out
if(messageHistory.length === 25){
messageHistory = messageHistory.shift();
}
messageHistory.push(remove_channel_key_from_message(user_last_message));
messageHistoryIndex = messageHistory.length;
});
// up for last message send, down for prev (if moving between em)
$('input.text-counter-input').on('keydown', function(e) {
if(e.keyCode == 38) {
e.preventDefault();
messageHistoryIndex--;
if(messageHistoryIndex > -1){
$(this).val(messageHistory[messageHistoryIndex]);
}
}else if(e.keyCode == 40){
e.preventDefault();
if(messageHistoryIndex <= messageHistory.length){
messageHistoryIndex++;
$(this).val(messageHistory[messageHistoryIndex]);
}else{
$(this).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);
var color;
for(var c=0;c<35;c++){
color = colors[(c % (colors.length))];
stylesheet.insertRule("#robinChat.show-colors #robinChatWindow div.robin-message.robin-filter-"+c+" { background: "+color+";}", 0);
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.hide-spam #robinChatWindow div.robin-message.spam-hidden { display:none; }", 0);
stylesheet.insertRule("#robinChat.mute-spam #robinChatWindow div.robin-message.spam-hidden { opacity:0.3; font-size:1.2em; }", 0);
stylesheet.insertRule("#robinChat.show-colors #robinChatWindow 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);
});
};
})();