// ==UserScript==
// @name RaaW2
// @version 3.1.9
// @namespace RaaW2
// @run-at document-end
// @description Reddit as a Weapon script. Parts and idea by /u/noeatnosleep, enhanced by /u/enim, /u/creesch, /u/skeeto, and /u/djimbob. RaaW adds links for page-wide voting and reporting. It adds a 'report to /r/spam' link, an 'analyze user submission domains' link, and a 'report to /r/botwatchman' link to userpages. RaaW disables the np. domain. RaaW Adds a 'show source' button for comments. DISCLIAMER: Use this at your own risk. If the report button is misued, you could be shadowbanned.
// @include http://www.reddit.com/user/*
// @include http://www.reddit.com/r/*
// @include http://*reddit.com/*
// @include https://www.reddit.com/user/*
// @include https://www.reddit.com/r/*
// @include https://*reddit.com/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js
// ==/UserScript==
this.jQuery = jQuery.noConflict(true);
// define a basic object that we can extend with our functions so we do not accidentally
// override other stuff
var RaaW = {
// ////////////////////////////////////////////////////////////////////////
// constants
// ////////////////////////////////////////////////////////////////////////
// some css properties for the links in the toolbox
LINK_CSS: {
'color': '#000',
},
// ////////////////////////////////////////////////////////////////////////
// instance variables
// ////////////////////////////////////////////////////////////////////////
raawToolbar: false,
// true if we are moderator on the current page (by checking if .moderator is present)
// in <body class="...">
isModerator: false,
currentPage: 'user',
// ////////////////////////////////////////////////////////////////////////
// various helper functions
// ////////////////////////////////////////////////////////////////////////
/**
* Function grabs the username of the current viewed profile.
*
* Returns:
* (string) username or undefined if not found
*/
_getUsername: function() {
return jQuery(document).find('.pagename.selected').text();
},
_getModhash: function() {
return unsafeWindow.reddit.modhash;
},
// ////////////////////////////////////////////////////////////////////////
// initialization
// ////////////////////////////////////////////////////////////////////////
/**
* Initialize RaaW. Will fetch some values like current page and after
* that initialize the toolbar.
*/
init: function() {
// first gather all the information needed
this._loadEnvironment();
// now add some elements we will need
this._injectElements();
// add the toolbar
this._generateToolbar();
// after we created everything connect it
this._registerListener();
},
/**
* Load environment values like current page.
*/
_loadEnvironment: function() {
// set current page
this.currentPage = document.URL.split('reddit.com')[1].split('/')[1];
// check if we are moderator
this.isModerator = jQuery('body').hasClass('moderator');
},
/**
* Adds/modifies needed elements to the reddit page (e.g. 'toggle source' links).
*/
_injectElements: function() {
// add links 'toogle source' to comments
var toggleSourceCodeEl = jQuery('<li><a class="raawToggleSourceCode" href="#">view source</a></li>');
jQuery('.entry .flat-list').append(toggleSourceCodeEl);
//disable .np
if (document.documentElement.lang === 'np') {
document.documentElement.lang = 'en-us';
}
// add subscriber class to body tag
jQuery('body').addClass('subscriber');
// replace links on the page
Array.forEach( document.links, function(a) {
a.href = a.href.replace( "https://i.imgur.com", "http://imgur.com");
a.href = a.href.replace( "https://imgur.com", "http://imgur.com");
});
// set checkbox 'limit my search to /r/...' checked
jQuery('form#search input[name="restrict_sr"]').prop('checked', true);
// add mod only stuff
if(this.isModerator === true) {
this._injectSaveAsMod();
this._injectNuke();
}
},
/**
* Register all click listener for the RaaW toolbar links. We do not distingish if we
* are on /user or something else. There should be no noticeable impact on performance
* and we save some maintenance effort.
*/
_registerListener: function() {
// we don't want js to bind 'this' to the global object. therefore we use a trick.
// whenever you need a 'this' reference inside one of the functions pointing to
// the RaaW object use 'that'
var that = this;
// register click handler for the user toolbar links
jQuery('#raawReportComment').click(function(e) {
that.reportAll(e);
});
jQuery('#raawBotwatchmanSend').click(function(e) {
that.botwatchmanSend(e);
});
jQuery('#raawAnalyzeSend').click(function(e) {
that.analyzeSend(e);
});
jQuery('#raawReportUserToSpam').click(function(e) {
that.reportUserToSpam(e);
});
jQuery('#raawAdminSend').click(function(e) {
that.adminSend(e);
});
// register handler for the other toolbar links
jQuery('#raawDownvoteComment').click(function(e) {
that.voteAll(e, -1);
});
jQuery('#raawUpvoteComment').click(function(e) {
that.voteAll(e, 1);
});
jQuery('#raawComposeNew').click(function(e) {
that.composeNew(e);
});
jQuery('.raawToggleSourceCode').click(function(e) {
that.toggleSourceCode(e);
});
},
// ////////////////////////////////////////////////////////////////////////
// toolbar stuff
// ////////////////////////////////////////////////////////////////////////
/**
* Helper function used to create an a-element.
*
* Parameters:
* id (string) - id attribute value
* href (string) - href attribute value
* text (string) - elements text
*
* Returns:
* jQuery element instance
*/
_generateToolbarLink: function(id, href, text) {
var link = jQuery('<a id="' + id + '" href="' + href + '">' + text + '</a>');
jQuery(link).css(this.LINK_CSS);
return link;
},
/**
* Generate the toolbar on top of the page.
*/
_generateToolbar: function() {
// apply some styles to the header
jQuery('#header').css({
'paddingTop': '18px'
});
// create the new raaw toolbar and insert into body
this.raawToolbar = jQuery('<div id="raawToolbar"></div>');
jQuery('body').prepend(this.raawToolbar);
// apply style to the new toolbar
jQuery(this.raawToolbar).css({
'color': '#000'
, 'background-color': '#f0f0f0'
, 'border-bottom': '1px solid #000'
, 'font-family': 'erdana, arial, helvetica, sans-serif'
, 'font-size': '90%'
, 'height': '12px'
, 'padding': '3px 0px 3px 6px'
, 'text-transform': 'uppercase'
, 'width': '100%'
, 'z-index': '+999999'
, 'position': 'fixed'
, 'top': '0'
});
// fill toolbar with content depending on parsed page
var toolbarLinks = new Array();
if(this.currentPage === 'user') {
toolbarLinks.push(this._generateToolbarLink('raawReportComment', '#', 'REPORT ALL'));
toolbarLinks.push(this._generateToolbarLink('raawBotwatchmanSend', '#', ' | /R/BOTWATCHMAN'));
toolbarLinks.push(this._generateToolbarLink('raawAnalyzeSend', '#', ' | ANALYZE'));
toolbarLinks.push(this._generateToolbarLink('raawReportUserToSpam', '#', ' | /R/SPAM'));
toolbarLinks.push(this._generateToolbarLink('raawAdminSend', '#', ' | ADMIN'));
} else {
toolbarLinks.push(this._generateToolbarLink('raawDownvoteComment', '#', 'DOWNVOTE ALL'));
toolbarLinks.push(this._generateToolbarLink('raawUpvoteComment', '#', ' | UPVOTE ALL'));
toolbarLinks.push(this._generateToolbarLink('raawComposeNew', '#', ' | COMPOSE'));
}
for(i = 0; i < toolbarLinks.length; i++) {
jQuery(this.raawToolbar).append(toolbarLinks[i]);
}
},
// ////////////////////////////////////////////////////////////////////////
// functions for user toolbar
// ////////////////////////////////////////////////////////////////////////
/**
* Report a given item using the reddit api.
*
* Parameters:
* fullname (string) - fullname of item to report
* el (jQuery el) - element to report (just for easier coding)
* timeout (int) - timeout before request
*/
_reportItem: function(fullname, el, timeout) {
var that = this;
setTimeout(function() {
var data= {
'api_type': 'json'
, 'thing_id': fullname
, 'other_reason': 'spam'
, 'uh': that._getModhash()
};
jQuery.post('http://www.reddit.com/api/report', data).done(function(response) {
jQuery(el).hide(1000);
}).error(function(response) {
console.log(response);
});
}, timeout);
},
/**
* Report all items on the page.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
reportAll: function(click) {
click.preventDefault();
var isConfirmed = confirm("This will report all items on the page.");
if (isConfirmed === true) {
// load all fullname of the comments on the page
var i = 0;
var that = this;
jQuery('div#siteTable .thing').each(function(index, el) {
var fullname = jQuery(el).attr('data-fullname');
that._reportItem(fullname, el, (400 * i) + 100);
i++;
});
// not accurate but will do
alert('All items on this page were reported.');
} else {
alert('Report canceled');
}
},
/**
* Open a new window to submit a user to /r/botwatchman.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
botwatchmanSend: function(click) {
click.preventDefault();
var username = this._getUsername();
window.open('http://www.reddit.com/r/botwatchman/submit?title=overview for ' + username + '&url=http://www.reddit.com/user/' + username);
},
/**
* Send a new message to /u/analyzereddit with subject 'analyze' and a username
* as message.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
analyzeSend: function(click) {
click.preventDefault();
var username = this._getUsername();
window.open('http://www.reddit.com/message/compose/?to=analyzereddit&subject=analyze&message=' + username);
},
/**
* Open a new window to report a user to /r/spam.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
reportUserToSpam: function(click) {
click.preventDefault();
var username = this._getUsername();
window.open('http://www.reddit.com/r/spam/submit?title=overview for '+ username + '&resubmit=true&url=http://www.reddit.com/user/' + username);
},
/**
* Open a new window to send a new message to /r/reddit.com.
*/
adminSend: function(click){
click.preventDefault();
var username = this._getUsername();
window.open('http://www.reddit.com/message/compose/?to=/r/reddit.com&subject=spammer&message=/u/'+ username + '/submitted');
},
// ////////////////////////////////////////////////////////////////////////
// functions for default toolbar
// ////////////////////////////////////////////////////////////////////////
/**
* Makes an asynch ajax call to the reddit API after waiting for the given amount
* of time.
*
* Parameters:
* data (object) - data to send (dir, uh, id)
* thing (jQuery el) - element the vote belongs to
* timeout (int) - time to wait in miliseconds
*/
_voteCallAPI: function(data, thing, timeout, callback) {
setTimeout(function() {
jQuery.post('/api/vote', data).done(function(response) {
callback(data, thing);
}).error(function(response) {
console.log('Error voting on item!');
console.log(response);
});
}, timeout);
},
/**
* Up- or downvote all comment on a page.
*
* Parameters:
* event (jQuery click event)
* dir (int) - 1 upvote, -1 downvote, 0 none
*/
voteAll: function(event, dir) {
event.preventDefault();
var things = jQuery('div.sitetable div.thing');
// gather the required fullnames to call the API
for(i = 0; i < things.length; i++) {
var thing = things[i];
var fullname = jQuery(thing).attr('data-fullname');
if(typeof fullname !== 'undefined') {
// send request to the api
var data= {
'dir': dir,
'id': fullname,
'uh': this._getModhash()
};
this._voteCallAPI(data, thing, 100+(i*400), function(data, thing) {
var upArrow = jQuery(thing).find('div.arrow[aria-label="upvote"]');
var downArrow = jQuery(thing).find('div.arrow[aria-label="downvote"]');
var midcol = jQuery(thing).find('div.midcol');
// not the fanciest way but prevents unexpected behaivour
if(data.dir === 1) {
jQuery(upArrow).addClass('upmod');
jQuery(upArrow).removeClass('up');
jQuery(downArrow).addClass('down')
jQuery(downArrow).removeClass('downmod');
jQuery(midcol).removeClass('dislikes');
jQuery(midcol).addClass('likes');
} else if(data.dir === -1) {
jQuery(upArrow).addClass('up');
jQuery(upArrow).removeClass('upmod');
jQuery(downArrow).removeClass('down');
jQuery(downArrow).addClass('downmod');
jQuery(midcol).addClass('dislikes');
jQuery(midcol).removeClass('likes');
}
});
}
}
},
/**
* Open a new window to compose a new message.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
composeNew: function(click) {
click.preventDefault();
window.open('http://www.reddit.com/message/compose/');
},
// ////////////////////////////////////////////////////////////////////////
// 'view source' related functions
// ////////////////////////////////////////////////////////////////////////
/**
* Helper function to fetch the sourcecode of comments/links/messages using the
* reddit api.
*
* Parameters:
* url (string) - because of the diversity of the api provide a url with the needed attributes
* fullname (string) - fullname needed to search if loading messages
* callback (function(source)) - callback function to call when api call is done
*/
_fetchSourceCode: function(url, fullname, callback) {
jQuery.getJSON(url).done(function(response) {
// check what type of posting we're looking at (check api for more information)
var postingType = response.data.children[0].kind;
// unfortunately the returned json object has no unified structure so
// we need a bit more logic here
var source;
if(postingType === 't1') { // comment
source = response.data.children[0].data.body;
} else if(postingType === 't3') { // link (post); will be empty for videos or similiar
source = response.data.children[0].data.selftext;
} else if(postingType === 't4') { // message
// the current api url loads a message thread so we need to find the
// desired message
rawData = response.data.children[0].data;
if(rawData.name === fullname) {
source = rawData.body;
} else {
// search through replies
var replies = rawData.replies.data.children;
for(var i = 0; i < replies.length; i++) {
var replyRaw = replies[i].data;
if(replyRaw.name === fullname) {
source = replyRaw.body;
break;
}
}
}
}
callback(source);
});
},
/**
* Create a textarea to display source code
*
* Parameters:
* source (string) - source code to display
* fullname (string) - fullname of link/comment/message so we can later identify if we already loaded the source
* prependTo (jQuery element) - element to prepend the textarea to
*/
_createSourceCodeTextarea: function(source, fullname, prependTo) {
// create a textarea to display source and add it to the dom
var textAreaEl = jQuery('<textarea class="'+fullname+'">'+source+'</textarea>');
jQuery(textAreaEl).css({
'display': 'block'
, 'width': '90%'
, 'height': '100px'
});
// insert textarea
jQuery(prependTo).prepend(textAreaEl);
},
/**
* Toggle source code.
*
* Parameters:
* click (jQuery click event) - the jQuery click event.
*/
toggleSourceCode: function(click) {
click.preventDefault();
// grab the clicked link element
var linkEl = jQuery(click.target);
// get the data-fullname attribute to provide an id to 'throw at the api'
var dataFullname = jQuery(linkEl).closest('.thing').attr('data-fullname');
var isTextAreaPresent = jQuery('textarea.'+dataFullname);
if(isTextAreaPresent.length == 1) {
jQuery(isTextAreaPresent).toggle();
} else {
// figure out the element where we're going to insert the textarea
var prependTo = jQuery(linkEl).parent().parent();
// do an ajax request to fetch the data from the api
// because we cannot fetch a message and a link/comment with the same api call
// we need to figure out, if we are on a message site
var apiURL;
if(this.currentPage === 'message') {
// cut off t4_ for filtering
messageId = dataFullname.slice(3, dataFullname.length);
apiURL = '/message/messages/.json?mid='+messageId+'&count=1';
} else {
apiURL = '/api/info.json?id=' + dataFullname;
}
var that = this;
this._fetchSourceCode(apiURL, dataFullname, function(source) {
that._createSourceCodeTextarea(source, dataFullname, prependTo);
});
}
},
// ////////////////////////////////////////////////////////////////////////
// 'save as mod' related stuff
// ////////////////////////////////////////////////////////////////////////
/**
* Place a 'save as mod' in the comment forms.
*
* Parameters:
* el (jQuery element) - form element to place the button in; leave out to inject into all
*/
_injectSaveAsMod: function(el) {
var that = this;
if(typeof el === 'undefined') {
el = false;
}
// no element given -> inject into all forms
var injectHere = new Array();
if(el === false) {
injectHere = jQuery('.usertext-buttons');
} else {
injectHere.push(jQuery(el).find('.usertext-buttons')[0]);
}
// inject between save and cancel
for(i = 0; i < injectHere.length; i++) {
// element where the buttons life in
var divEl = injectHere[i];
// button to inject; register click function...
var buttonEl = jQuery('<button type="button" class="raawSaveAsMod">save as mod</button>');
jQuery(buttonEl).click(function(e) {
that.saveAsMod(e);
});
// find save button and add save as mod after that
jQuery(divEl).find('button[type="submit"]').after(buttonEl);
}
// remove the buttons from the edit form
jQuery('div.entry .usertext-buttons button.raawSaveAsMod').remove();
// find all reply links
var replyButtons = jQuery('ul.flat-list li a').filter(function(index, el) {
return jQuery(el).text() === 'reply';
});
for(i = 0; i < replyButtons.length; i++) {
var button = replyButtons[i];
jQuery(button).click(function(e) {
setTimeout(function() {
var allButtons = jQuery('button.raawSaveAsMod');
for(i = 0; i < allButtons.length; i++) {
var button = allButtons[i];
jQuery(button).off('click');
jQuery(button).click(function(e) {
that.saveAsMod(e);
});
}
}, 500);
});
}
},
/**
* Method will prevent a comment form to submit in the first place. Will fetch the
* thing id to use it for distinguishing. After that will submit the form and when the
* submit is finished distinguish the comment itself.
*
* Parameters:
* click (jQuery click event)
*/
saveAsMod: function(click) {
click.preventDefault();
var form = jQuery(click.target).closest('form.usertext');
// get parent
var hiddenInput = jQuery(form).children('input[name="thing_id"]')[0];
var parent = jQuery(hiddenInput).val();
// get comment text
var textarea = jQuery(form).find('textarea')[0];
var commentText = jQuery(textarea).val();
var modhash = this._getModhash();
// post comment
data = {
'api_type': 'json'
, 'text': commentText
, 'thing_id': parent
, 'uh': modhash
};
jQuery.post('/api/comment', data).done(function(response) {
if(response.json.errors.length > 0) {
console.log('Error while posting comment:');
console.log(response.json.errors);
alert('Error while posting comment. Please check the console for more information!');
return;
}
// distinguish
var commentData = response.json.data.things[0].data;
var data = {
'id': commentData.id
, 'api_type': 'json'
, 'how': 'yes'
, 'uh': modhash
};
jQuery.post('/api/distinguish', data).done(function(response) {
if(response.json.errors.length > 0) {
console.log('Error while posting comment:');
console.log(response.json.errors);
alert('Error while posting comment. Please check the console for more information!');
return;
}
location.reload();
});
});
},
// ////////////////////////////////////////////////////////////////////////
// nuke functions
// ////////////////////////////////////////////////////////////////////////
/**
* Find the a comment with the given id in a response object.
*
* Parameters:
* commentId (string) - something like t1_abc
* response (object) - response object returned by a API call
*
* Returns:
* (object) or undefined
*/
_findCommentInResponse: function(commentId, response) {
var searchedComment;
// build a search queue to go through
var search = new Array();
for(var i = 0; i < response.length; i++) {
var listing = response[i].data.children;
for(var n = 0; n < listing.length; n++) {
var content = listing[n];
// if data is something else than a comment skip
if(content.kind !== 't1') {
continue;
} else {
// the comment is the one we search
if(content.data.id === commentId) {
return content;
} else {
// comment is not what we search but maybe one of his replies?
// add replies to search queue
if(typeof content.data.replies !== 'undefined' && content.data.replies !== '') {
search = search.concat(content.data.replies.data.children);
}
}
}
}
}
while(search.length > 0) {
var currentObj = search.pop();
// check if this is the right comment
if(currentObj.data.id === commentId) {
return currentObj;
}
// add all the replies of this comment to the search array
if(currentObj.data.replies !== '') {
search = search.concat(currentObj.data.replies.data.children);
}
}
return searchedComment;
},
/**
* Will find all replies to the given comment.
*
* Parameters:
* obj (object) - thing object returned by a API call
*
* Returns:
* (Array) Maybe an empty array if no replies where found. Array holds the ids as a
* string. E.g. '1234', 'a23pav', ...
*/
_findAllReplies: function(obj) {
var replies = new Array();
// check if there are replies
if(obj.data.replies === '') {
return replies;
}
var search = new Array();
for(var i = 0; i < obj.data.replies.data.children.length; i++) {
var reply = obj.data.replies.data.children[i];
replies.push(reply.data.id);
if(typeof reply.data.replies !== 'undefined' && reply.data.replies !== '') {
search = search.concat(reply.data.replies.data.children);
}
}
while(search.length > 0) {
var currentObj = search.pop();
// 'more' occures if there are more than 10 entries
if(currentObj.kind === 'more') {
continue;
}
// add the id of the current reply
replies.push(currentObj.data.id);
// add all replies to the reply to the search
if(currentObj.data.replies !== '') {
search = search.concat(currentObj.data.replies.data.children);
}
}
return replies;
},
/**
* Remove the given comment.
*
* Parameters:
* commentId (string) - id of the comment to remove
* timeout (int) - timeout in milliseconds
*/
_nukeComment: function(commentId, timeout) {
var that = this;
setTimeout(function() {
var data = {
'id': 't1_' + commentId
, 'uh': that._getModhash()
}
jQuery.post('/api/remove', data).done(function(response) {
var el = jQuery('div.thing[data-fullname="t1_'+commentId+'"]');
jQuery(el).hide(1000, function() {
jQuery(el).remove();
});
}).error(function(response) {
console.log(response);
});
}, timeout);
},
/**
* Inject the nuke button.
*/
_injectNuke: function() {
var nukeButton = jQuery('<a href="#" class="raawNuke" title="Nuke!">[Nuke]</a>');
// add click listener
var that = this;
jQuery(nukeButton).click(function(e) {
e.preventDefault();
var dataFullname = jQuery(this).closest('div.thing').attr('data-fullname');
var link = jQuery('#siteTable').find('div.thing').attr('data-fullname').slice(3); // cut off t3_
// get all the comments for this thread and find the right parent comment
jQuery.getJSON('/comments/'+link+'.json').done(function(response) {
var searchedComment = that._findCommentInResponse(dataFullname.slice(3), response);
if(typeof searchedComment !== 'undefined') {
// extract all children
var removeThese = that._findAllReplies(searchedComment);
// add the parent itself; but remove t1_ because thats how the other look to!
removeThese.push(dataFullname.slice(3));
for(var i = 0; i < removeThese.length; i++) {
var childsDataFullname = removeThese[i];
that._nukeComment(removeThese[i], (i * 750) + 100);
}
}
});
});
// insert button
jQuery('div.commentarea div.thing p.tagline span.userattrs').prepend(nukeButton);
}
};
// initialize when document loaded
jQuery(document).ready(function() {
RaaW.init();
});