RaaW

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.

目前为 2014-10-10 提交的版本。查看 最新版本

// ==UserScript==
// @name RaaW
// @version 3.1.5
// @namespace RaaW
// @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 '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.voteAll(e, -1);
      });
      jQuery('#raawUpvoteComment').click(function(e) {
        that.voteAll(e, 1);
      });
      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 = this._getModhash();
    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 = this._getModhash();

          // 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
  // ////////////////////////////////////////////////////////////////////////

  /**
   * 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) {
    setTimeout(function() {
      console.log(data);
      jQuery.post('/api/vote', data).done(function(response) {
        jQuery(thing).hide(1000);
      }).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));
      }
    }
  },

  /**
   * 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();
      });
    });
  },

};

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();
});