Skip to content

Instantly share code, notes, and snippets.

@rschrieken
Last active December 11, 2015 17:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rschrieken/f7505d2f37128335ceef to your computer and use it in GitHub Desktop.
Save rschrieken/f7505d2f37128335ceef to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Match against peers in review
// @namespace http://stackoverflow.com/users/578411/rene
// @version 0.3
// @description how you reviewed against your peers
// @author rene
// @match *://stackoverflow.com/review/*/history*
// @grant none
// ==/UserScript==
/*global $:false, window:false, unsafeWindow:false */
(function ($, window) {
"use strict";
var tasks = [],
intervalTime = 200, // 200ms (make this larger when throttled often)
per = 90000, // milliseconds
penalty = 60000, // msec to wait after 503
rate = 150, // per 90000 milliseconds (make this smaller when throttled often, but on 80 you're safe )
interval;
// for each review put a task in the queue
function addTasks(tasks) {
$('table.history-table > tbody > tr > td:nth-child(3) > a').each(function () {
var a = $(this);
tasks.push(a);
a.parent().append($('<span class="match" style="float:right; padding-right:20px; "></span>').html('&hellip;'));
//if (tasks.length > 20) {return false; }
});
}
// find the key, return the index
function find(arr, val) {
var index = -1,
j;
for (j = 0; j < arr.length; j = j + 1) {
if (arr[j].key === val) {
index = j;
break;
}
}
return index;
}
// update the outcomes array based on the html
// in instruction
function handleInstruction(instruction, outcomes) {
var index,
outcome;
if (instruction.hasClass('review-results')) {
outcome = instruction.find('b').text();
index = find(outcomes, outcome);
if (index === -1) {
outcomes.push({ key: outcome, value: 1});
} else {
outcomes[index].value = outcomes[index].value + 1;
}
}
}
// parse the review outcome html and return an array
// with outcomes and it's number of occurences
function handleInstructions(instructions) {
var i,
outcomes = [];
// stangely enough instructions doesn't behave fully as a
// jquery object, hence the juggling here
for (i = 0; i < instructions.length; i = i + 1) {
handleInstruction($(instructions[i]), outcomes);
}
//sort on value
return outcomes.sort(function (a, b) {
return a.value > b.value ? -1 : a.value === b.value ? 0 : 1;
});
}
// if the postback results are in process and show the (mis)match
function postresultHandler(data, url) {
var stats = $(data),
status = $(data.instructions),
peers = handleInstructions($(stats[0].instructions)), // rank hold the reviews of your peers
rank = find(peers, url.text().trim()), // url is your own review
match,
comp = false,
more = false,
audit = false;
if (typeof status[0].textContent === 'string') {
comp = status[0].textContent.indexOf('Review completed') > 0;
more = status[0].textContent.indexOf('needs more reviews from other users') > 0;
audit = status[0].textContent.indexOf('Review audit') > 0;
}
console.log(status[0].textContent);
console.log('ua:%s, complete:%s need more:%s audit:%s',stats.isUnavailable, comp, more, audit);
if (comp) {
if (rank === 0) { // if on top, match
match = { html: '=', color: 'green'};
} else if (rank > 0 && peers[rank].value === peers[0].value) { // not on top, but equal
match = { html: '~', color: 'orange'};
} else { // no match
match = { html: '!', color: 'red'};
}
} else {
if (more) {
match = { html: '.', color: 'black'};
} else if (audit) {
match = { html: 'A', color: 'blue'};
} else if (!stats.isUnavailable) {
match = { html: ' ', color: 'black'};
} else {
console.log(status[0].textContent);
match = { html: '?', color: 'black'};
}
}
url.parent().find('span.match')
.html(match.html)
.css('color', match.color);
}
// get array with timestamps from localstorage
function getThrottle() {
var calls = window.localStorage.getItem('se-throttle');
if (calls === null) {
calls = [ Date.now() ];
} else {
calls = JSON.parse(calls);
if (!Array.isArray(calls)) {
calls = [ Date.now() ];
}
}
return calls;
}
// update timestamp array for throttle
function setThrottle(time) {
var calls = getThrottle(),
i;
if (time === undefined) {
time = Date.now();
}
for(i = 0;
((i < calls.length - 1) && (calls[0] < Date.now() - per));
i = i + 1) {
calls.shift();
}
if (calls.length > rate) {
calls.shift();
}
calls.push(time);
window.localStorage.setItem('se-throttle', JSON.stringify(calls));
}
// gets called by the setInterval
function taskWorker() {
var url = tasks.shift(),
partReviewId = 3,
partQueue = 2,
// http://meta.stackexchange.com/a/214527/158100
reviewTypeMap = {
"suggested-edits": 1,
close: 2,
"low-quality-posts": 3,
"first-posts": 4,
"late-answers": 5,
reopen : 6,
triage : 10,
helper: 11
},
parts;
if (url !== undefined) {
parts = url.attr('href').split('/');
$.post('/review/next-task/' + parts[partReviewId],
{
taskTypeId: reviewTypeMap[parts[partQueue]], /* triage -> 10 */
fkey: window.StackExchange.options.user.fkey
},
function (data) {
setThrottle();
postresultHandler(data, url);
})
.fail(function (xhr, stat, error) {
// Service Unavailable means we're throttled, panic
console.log(xhr);
if (xhr.status === 503) {
// wait a full minute to get free
setThrottle(Date.now() + 60000);
}
});
} else {
setThrottle();
window.clearInterval(interval);
}
}
// check if we are within the throttle boundaries
function isAllowed() {
var calls = getThrottle(),
timepassed;
timepassed = Date.now() - calls[0];
// console.log(timepassed);
return (((calls.length < rate) ||
(timepassed > per)) &&
(calls[calls.length-1] < Date.now()));
}
// handle a task
function task() {
if (isAllowed()) {
taskWorker();
} else {
console.log('<< throttle >>');
}
}
function init(tasks, time) {
addTasks(tasks);
return window.setInterval(task, time);
}
interval = init(tasks, intervalTime);
}($ || unsafeWindow.$, window || unsafeWindow));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment