Greasy Fork is available in English.

AO3 anti-kudos

Lets you leave anti-kudos on terrible fic.

// ==UserScript==
// @name        AO3 anti-kudos
// @namespace   ao3
// @description Lets you leave anti-kudos on terrible fic.
// @include     http://archiveofourown.org/*
// @include     https://archiveofourown.org/*
// @version     1.2
// @require  https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant		GM_setValue
// @grant 	GM_listValues
// @grant 	GM_getValue
// @grant		GM_xmlhttpRequest
// @grant		GM.setValue
// @grant 	GM.listValues
// @grant 	GM.getValue
// @grant		GM.xmlHttpRequest
// @require     http://code.jquery.com/jquery-2.1.0.min.js
// @require 	https://cdnjs.cloudflare.com/ajax/libs/aws-sdk/2.1.35/aws-sdk.min.js
// ==/UserScript==
//******* CONFIG OPTIONS ******* //
window.antikudosConfig = {
    hideCrap: false,
    // set to true if you don't want to see works you've left anti-kudos on.
	boldCrap: true
	// set to false if you don't want the number of anti-kudos to be bolded.
};


 
// ***************************** //

var options = window.antikudosConfig;

(async function(){
  
  AWS.config.update({accessKeyId: 'AKIAIBDCUKBAGRNARCIQ', secretAccessKey: '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'});
  AWS.config.region = 'us-west-1'; 
  var myBlacklist = await GM.listValues();
  
  var works = $('li.blurb');
  if (works[0]) {
    if (options.hideCrap === true) hideWorks(works);
    addAntikudosStats(works, false);
      return;
  }else{
    works = $('dl.meta');
    if (works[0]) {addAntikudosStats(works, true); } else {return;}
  }
  
  var workID = $('#kudo_commentable_id').val();
  var didWeClick = await GM.getValue(workID.toString(), false);
  var clickedButton = false;
  var buttons = $('div.feedback').find('li');
  var messageSpace = $('#kudos_message');

  var antikudoText = '<li id=\"antikudosbutton\"><a>Anti-Kudos ♠</a></li>';
  var antikudos = buttons.find(':contains(\'Next Chapter\')').length ? buttons.eq(2).after(antikudoText) : buttons.eq(1).after(antikudoText);
  listAntikudos(workID, false, didWeClick);
	
  $('#antikudosbutton').click(function(){
      if (clickedButton) return;
      if (didWeClick){
        messageSpace.addClass("comment_error").append("You have already expressed your hatred for this fic. :)");
      }else{
			
      GM.xmlHttpRequest({
        method: "POST",
        url: "http://ec2-52-8-177-39.us-west-1.compute.amazonaws.com/",
        data: workID,
        headers: {'Content-Type': 'text/plain'},
        onerror: function(dummy){
          messageSpace.addClass("comment_error").append("Something went wrong! Please reload and try again.");
        },
        onload: async function(response){
          if (response.status === 200){
            if (response.responseText === 'Item was added!') {
                  messageSpace.addClass("notice").append("Thank you for leaving anti-kudos!");
              listAntikudos(workID, true, didWeClick);
              await GM.setValue(workID.toString(), true);
            }else{
              messageSpace.addClass("comment_error").append("Something went wrong! Server message: "+ response.responseText+" Please reload and try again.");
            }
          }else{
            messageSpace.addClass("comment_error").append("Something went wrong! Status code " + response.status + ". Please reload and try again.");
          } 
        } 
      }); 
      }
      clickedButton = true; 
  }); 
})();


function listAntikudos(thisworkID, justClicked, didWeClick){
	getOneFromDatabase(thisworkID, function(numAntis){	
		if (justClicked === false && (numAntis === 0 || numAntis === "N/A")) {return;}
		var antikudoListText = 'left anti-kudos on this work!</p>'
		if (justClicked || didWeClick){
			numAntis--; 
			if (numAntis === 0){
				antikudoListText = '<p class=\"antikudos kudos\">You '+antikudoListText;
			}else if (numAntis === 1){
				antikudoListText = '<p class=\"antikudos kudos\">You and '+numAntis+' hater '+antikudoListText;
			}else{
				antikudoListText = '<p class=\"antikudos kudos\">You and '+numAntis+' haters '+antikudoListText;
			}
		}else{
			if (numAntis === 1){
				antikudoListText = '<p class=\"antikudos kudos\">'+numAntis+' hater '+antikudoListText;
			}else{
				antikudoListText = '<p class=\"antikudos kudos\">'+numAntis+' haters '+antikudoListText;
			}
		}
		
		if ($.find('p.antikudos').length > 0){
			$('p.antikudos').replaceWith(antikudoListText);
		}else{
			var kudosList = $('#kudos');
			var antikudosList = kudosList.find('p.kudos').length ? kudosList.find('p.kudos').before(antikudoListText) : kudosList.append(antikudoListText);			
		}
		$('p.antikudos').css('background', 'url(\"http://i.imgur.com/XYu9WJD.png\") no-repeat');
	}); 
}

function addAntikudosStats(works, isWorkPage){
	var thisworkID;
	var numAntikudos = [0];
	
	if (isWorkPage){
		thisworkID = $('#kudo_commentable_id').val();
		
		getOneFromDatabase(thisworkID, function(output){
			numAntikudos[0] = output;
			displayNumberAntikudos(works, numAntikudos, thisworkID);
		}); 
	}else{
		var tableName = 'numAntiKudosTable';
		var table = new AWS.DynamoDB({params: {TableName: tableName}});
		var worksParams = {
			"RequestItems" : {
				"numAntiKudosTable" : {
					"Keys" : []
				}
			}
		};
		var theseworkIDs = [];
		var workLink;
		works.each(function(index, value){
			if ($(this).hasClass('work')){
				theseworkIDs[index] = $(this).attr('id').slice(5);
			}else{
				workLink = $(this).find('h4.heading').find('a').attr('href');
				if (workLink.slice(1,2) === 'w'){
					theseworkIDs[index] = workLink.slice(7);				
				}else{
					theseworkIDs[index] = index;
				}
			}
			worksParams.RequestItems.numAntiKudosTable.Keys[index] = {"workID" : {"S" : theseworkIDs[index].toString()}}; 
		}); 
		table.batchGetItem(worksParams, function(err, data){
			if (err) {console.log('error');
			}else{
				for (var i=0; i < theseworkIDs.length; i++){
					if (data.Responses.numAntiKudosTable.length > 0){
						for (var j=0; j < data.Responses.numAntiKudosTable.length; j++){
							if (theseworkIDs[i].toString() === data.Responses.numAntiKudosTable[j].workID.S ){
								numAntikudos[i] = data.Responses.numAntiKudosTable[j].numAntiKudos.N;
								break;
							}
							numAntikudos[i] = 0;
						}
					}else{
						numAntikudos[i] = 0;
					}
				}
			}
			displayNumberAntikudos(works, numAntikudos, theseworkIDs);
		});
	}
}

   
function hideWorks(works){
        var hideMe;
        var workID;
        works.each(function(){
			workID = $(this).attr('id').slice(5);
			hideMe = compare(workID);					
			if (hideMe) $(this).hide();     
        }); 
} 

function compare(workID){
	for(var i = 0; i < myBlacklist.length; i++){
		if (workID === myBlacklist[i]){
			return true;
		}
	}
	return false;
}

function displayNumberAntikudos(works, numAntikudos, theseworkIDs){
	works.each(function(index,value){	
		if (!$(this).hasClass('work') && $(this).find('h4.heading').find('a').attr('href').slice(1,2) !== 'w'){ return true; }
		var stats = $(this).find('dl.stats');
		var statText;
		var eqNum = 2;
		
		if (stats.find(":contains('Comments:')").length) eqNum++;
		if (stats.find(":contains('Kudos:')").length) eqNum++;
		if (stats.find(":contains('Updated:')").length) eqNum++;
		
		if (works.length > 1){
			statText = "<dt>Anti-Kudos: </dt><dd><a href=\"/works/"+theseworkIDs[index]+"#comments\">"+numAntikudos[index]+"</a></dd>";
			
		}else{
			statText = "<dt>Anti-Kudos: </dt><dd>"+numAntikudos[index]+"</dd>";
		}     
		if (options.boldCrap === true) { statText = "<b>"+statText+"</b>"; }
    stats.find("dd").eq(eqNum).after(statText);	
		$(this).attr('antikudos', numAntikudos[index]);
	});
	
	if (works.length > 1){
		var headerMenu = $('ul.primary.navigation.actions');
		var  antikudosMenu = $('<li class="dropdown"></li>').html('<a>Anti-Kudos</a>');
		headerMenu.find('li.search').before(antikudosMenu);
		var dropMenu = $('<ul class="menu dropdown-menu"></li>');
		antikudosMenu.append(dropMenu);
		
		var buttonSortAsc = $('<li></li>').html('<a>Sort on this page (ascending)</a>');
		buttonSortAsc.click(function() { sortUp(); });
		
		var buttonSortDesc = $('<li></li>').html('<a>Sort on this page (descending)</a>');
		buttonSortDesc.click(function() { sortDown(); });
		
		dropMenu.append(buttonSortAsc);
		dropMenu.append(buttonSortDesc);
	}
} 

function getOneFromDatabase(thisworkID, callback){
	var response;
	var table = new AWS.DynamoDB({params: {TableName: 'numAntiKudosTable'}});
	var workParams = {
		TableName: 'numAntiKudosTable',
		Key: {
			workID: {S: thisworkID.toString()}
		},
		ProjectionExpression: 'numAntiKudos'
	};
	table.getItem(workParams, function(err, data){
		if (err){response = "N/A"; 
		}else{
			if(Object.getOwnPropertyNames(data).length === 0){
				response = 0;
			}else{
				response = parseInt(data.Item.numAntiKudos.N,10);
			}
		}
		callback(response);
	}); 	
} 

function sortUp(){
	
	var work_list = $('li.blurb').first().parent();
	var blurbs = work_list.children('li');

	blurbs.sort(function(a, b) {
	   return parseInt(a.getAttribute('antikudos')) - parseInt(b.getAttribute('antikudos'));
	});

	blurbs.detach().appendTo(work_list);

}

function sortDown(){
	
	var work_list = $('li.blurb').first().parent();
	var blurbs = work_list.children('li');

	blurbs.sort(function(a, b) {
	   return parseInt(b.getAttribute('antikudos')) - parseInt(a.getAttribute('antikudos'));
	});

	blurbs.detach().appendTo(work_list);

}