// ==UserScript==
// @name RaaW
// @version 3.1.1
// @namespace RaaW
// @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 = 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',
modHash: false,
// ////////////////////////////////////////////////////////////////////////
// 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();
},
// ////////////////////////////////////////////////////////////////////////
// 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 'save as mod' button
if(this.isModerator === true) {
this._injectSaveAsMod();
}
},
/**
* 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.downvoteAll(e)
});
jQuery('#raawUpvoteComment').click(function(e) {
that.upvoteAll(e)
});
jQuery('#raawComposeNew').click(function(e) {
that.composeNew(e)
});
//jQuery('.raawToggleSourceCode').click(this.toggleSourceCode);
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
* modhash (string) - modhash to use for reporting
*/
_reportItem: function(fullname, modhash) {
var modhash = reddit.modhash;
jQuery.post('http://www.reddit.com/api/report', {'id': fullname, 'uh': modHash});
},
/**
* 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) {
var that = this;
var modhash = reddit.modhash;
// load all fullname of the comments on the page
jQuery('.commentarea .thing').each(function(index, el) {
this._reportItem(fullname, modhash);
fullnames.push(jQuery(el).attr('data-fullname'));
});
// 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&message=/u/'+ username);
},
// ////////////////////////////////////////////////////////////////////////
// functions for default toolbar
// ////////////////////////////////////////////////////////////////////////
/**
* Downvote various comments on all kinds of pages.
*
* Returns:
* (boolean)
*/
downvoteAll: function(click) {
click.preventDefault();
// we are on a user page
if(this.currentPage === 'user') {
var items = jQuery('#siteTable').find('.arrow.down');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function() {
el.click();
},100 + ( i * 400 ));
});
return false;
} else {
// all div.comment elements on the page
var comments = jQuery('.comment');
// no comments found
if(comments.length == 0) {
var items = jQuery('#siteTable').find('.arrow.down');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function(){
el.click();
},100 + ( i * 400 ));
});
return false;
} else {
var items = jQuery('.commentarea').find('.arrow.down');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function(){
el.click();
},100 + ( i * 400 ));
});
return false;
}
}
},
/**
* Upvote various comments on all kinds of pages.
*
* Returns:
* (boolean)
*/
upvoteAll: function(click) {
click.preventDefault();
// we are on a user page
if (this.currentPage === 'user'){
var items = jQuery('#siteTable').find('.arrow.up');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function(){
el.click();
},100 + ( i * 400 ));
});
return false;
}
else {
// all div.comment elements on the page
var comments = jQuery('.comment');
// no comments found
if (comments.length === 0) {
var items = jQuery('#siteTable').find('.arrow.up');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function() {
el.click();
},100 + ( i * 400 ));
});
return false;
} else {
var items = jQuery('.commentarea').find('.arrow.up');
Array.prototype.forEach.call(items, function(el, i){
setTimeout(function() {
el.click();
},100 + ( i * 400 ));
});
return false;
}
}
},
/**
* 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
// ////////////////////////////////////////////////////////////////////////
/**
* Register a new event listener in the given div.
*
* Parameters:
* div (jQuery element) - div with the class .usertext-buttons
*/
_updateSavesAsModListener: function(form) {
},
/**
* 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 = reddit.modhash;
// 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();
});
});
},
};
RaaW.Nuke = {
init: function() {
if(document.querySelector('body.moderator')) { // only execute if you are a moderator
var nuke_button = new Array();
var divels = document.querySelectorAll('div.noncollapsed');
var comment_ids = new Array();
var use_image = false;
// create img DOM element to clone
if(use_image) {
try {
var img_element = document.createElement('img');
img_element.setAttribute('alt', 'Nuke!');
img_element.setAttribute('src', chrome.extension.getURL('nuke.png'));
} catch(e) {
use_image = false;
}
}
for (var i = 0; i < divels.length; i++) {
var author_link = divels[i].querySelector('p.tagline>a.author,p.tagline>span.author,p.tagline>em');
// p.tagline>a.author is normal comment;
// some author deleted comments seem to have either
// p.tagline>span.author or p.tagline>em
comment_ids[i] = divels[i].getAttribute('data-fullname');
// console.log(i + ':' + comment_ids);
if(author_link) {
// create link DOM element with img inside link
nuke_button[i] = document.createElement('a')
nuke_button[i].setAttribute('href', 'javascript:void(0)');
nuke_button[i].setAttribute('title', 'Nuke!');
nuke_button[i].setAttribute('id', 'nuke_'+i);
if(use_image) {
nuke_button[i].appendChild(img_element.cloneNode(true));
} else {
nuke_button[i].innerHTML= "[Nuke]";
}
// append after the author's name
author_link.parentNode.insertBefore(nuke_button[i], author_link.nextSibling);
// Add listener for click; using IIFE to function with _i as value of i when created; not when click
nuke_button[i].addEventListener('click',
(function(_i) {
return function() {
var continue_thread = divels[_i].querySelectorAll('span.morecomments>a');
var comment_str = " comments?";
if(continue_thread.length > 0) {
comment_str = "+ comments (more after expanding collapsed threads; there will be a pause before the first deletion to retrieve more comments)?";
}
var delete_button = divels[_i].querySelectorAll('form input[value="removed"]~span.option.error a.yes,a[onclick^="return big_mod_action(jQuery(this), -1)"]');
// form input[value="removed"]~span.option.error a.yes -- finds the yes for normal deleting comments.
// a.pretty-button.neutral finds the 'remove' button for flagged comments
if (confirm("Are you sure you want to nuke the following " + delete_button.length + comment_str)) {
for (var indx=0; indx < continue_thread.length; indx++) {
var elmnt = continue_thread[indx];
setTimeout(function() {
var event = document.createEvent('UIEvents');
event.initUIEvent('click', true, true, window, 1);
elmnt.dispatchEvent(event);
}, 2000*indx); // wait two seconds before each ajax call before clicking each "load more comments"
}
if(indx > 0) {
setTimeout(function() {
delete_function(comment_ids[_i])
}, 2000*(indx + 2)); // wait 4s after last ajax "load more comments"
} else {
delete_function(comment_ids[_i]); // call immediately if not "load more comments"
}
}
}
})(i)); // end of IIFE (immediately invoked function expression)
}
}
}
},
//nuke (djimbob)
delete_function: function(thread_root) {
var elmnts = document.getElementsByClassName('id-'+thread_root)[0].querySelectorAll('form input[value="removed"]~span.option.error a.yes,a[onclick^="return big_mod_action(jQuery(this), -1)"]');
for(var i=0; i < elmnts.length; i++) {
setTimeout((function(_elmnt) {
return function() {
var event = document.createEvent('UIEvents');
event.initUIEvent('click', true, true, window, 1);
_elmnt.dispatchEvent(event);
};
})(elmnts[i]), 1500*i); // 1.5s timeout prevents overloading reddit.
};
}
};
// initialize when document loaded
jQuery(document).ready(function() {
RaaW.init();
RaaW.Nuke.init();
});