Skip to content

Instantly share code, notes, and snippets.

@cuylerstuwe
Created April 30, 2018 20:08
Show Gist options
  • Select an option

  • Save cuylerstuwe/4ed58de2160d640b03d9ce4122e2bf4f to your computer and use it in GitHub Desktop.

Select an option

Save cuylerstuwe/4ed58de2160d640b03d9ce4122e2bf4f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Web Panda Default Handler
// @namespace salembeats
// @version 11.0
// @description Latest update: Fixed the value in the "survey accepted" notification so that it always uses 2 decimal places for the cents value.
// @author Cuyler Stuwe (salembeats)
// @include https://worker.mturk.com/requesters/handleWebPanda*
// @include https://worker.mturk.com/requesters/registerWebPanda
// @grant GM_xmlhttpRequest
// @connect worker.mturk.com
// @connect amazon.com
// @connect mturk.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @grant GM_notification
// @icon https://i.imgur.com/snRSm80.gif
// @require https://greasyfork.org/scripts/36173-panda-crazy-helper-emulation/code/Panda%20Crazy%20Helper%20Emulation.js?version=243252
// ==/UserScript==
const IS_BENCHMARKING_ENABLED = true;
if(IS_BENCHMARKING_ENABLED === false) {
console.time = ()=>{};
console.timeEnd = ()=>{};
}
const SHOULD_INDICATE_TURN_WITH_FULL_YELLOW_SCAN = false;
// const GID_IN_DATABASE_EMOJI = "πŸ”"; // Using icons rather than emoji now.
const POKEBALL_IMAGE = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Pok%C3%A9_Ball_icon.svg/2000px-Pok%C3%A9_Ball_icon.svg.png";
const CAUGHT_IMAGE = POKEBALL_IMAGE;
const REPEAT_IMAGE = "https://cdn3.iconfinder.com/data/icons/unicons-vector-icons-pack/32/repeat2-512.png";
const NEW_IMAGE = "http://www.pvhc.net/img1/wmdxhwvvvzadyfetxqda.png";
const CAUGHT_IMAGE_SIZE_PX = 16;
const CAUGHT_IMAGE_WIDTH_PX = CAUGHT_IMAGE_SIZE_PX;
const CAUGHT_IMAGE_HEIGHT_PX = CAUGHT_IMAGE_SIZE_PX;
const REPEAT_IMAGE_SIZE_PX = CAUGHT_IMAGE_SIZE_PX;
const REPEAT_IMAGE_WIDTH_PX = REPEAT_IMAGE_SIZE_PX;
const REPEAT_IMAGE_HEIGHT_PX = REPEAT_IMAGE_SIZE_PX;
const NEW_IMAGE_HEIGHT_PX = 16;
window.addEventListener("beforeunload", function(event) {
if(location.href.includes("registerWebPanda")) {return;}
let newInstanceCount;
GM_setValue("web-panda-instance-count", newInstanceCount = GM_getValue("web-panda-instance-count") - 1);
GM_setValue("web-panda-last-removed", handlerIndex);
if(GM_getValue("web-panda-turn") >= GM_getValue("web-panda-instance-count")) {
GM_setValue("web-panda-turn", GM_getValue("web-panda-instance-count")-1);
}
if(newInstanceCount === 0) {
GM_deleteValue("web-panda-turn");
}
});
console.time("Initial cache load and parse");
let storedMetadata;
var gidMetadataCache = ( (storedMetadata = GM_getValue("gidMetadataCache")) ? JSON.parse(storedMetadata) : {} );
console.timeEnd("Initial cache load and parse");
function getMetadata(gid, key) {
if(!Boolean(gidMetadataCache[gid])) {return undefined;}
else if(gidMetadataCache[gid][key] === null || gidMetadataCache[gid][key] === "") {return undefined;}
else {return gidMetadataCache[gid][key];}
}
function hasMetadata(gid, key) {
if(!Boolean(gidMetadataCache[gid])) {return false;}
if(gidMetadataCache[gid][key] === undefined || gidMetadataCache[gid][key] === "") {return false;}
return true;
}
function createFreshMetadataGID(key, value) {
let newMetadataGID = {
createdTimestamp: Date.now(),
updatedTimestamp: Date.now()
};
if(key) {
newMetadataGID[key] = value;
}
return newMetadataGID;
}
function setMetadata(gid, key, value) {
if(!Boolean(gidMetadataCache[gid])) {
gidMetadataCache[gid] = createFreshMetadataGID(key,value);
}
else {gidMetadataCache[gid][key] = value;}
}
function saveStoredMetadata() {
console.time("Metadata storing");
GM_setValue("gidMetadataCache", JSON.stringify(gidMetadataCache));
console.timeEnd("Metadata storing");
}
function getStoredMetadata() {
console.time("Metadata retrieval and parsing");
let returnVal = JSON.parse(GM_getValue("gidMetadataCache") || "{}");
console.timeEnd("Metadata retrieval and parsing");
return returnVal;
}
function mergeUndefinedMetadata(gid, metadataChunk) {
let metadataKeys = Object.keys(metadataChunk);
if(!Boolean(gidMetadataCache[gid])) { gidMetadataCache[gid] = createFreshMetadataGID(); }
for(let metadataKey of metadataKeys) {
let existingValue = gidMetadataCache[metadataKey];
if(existingValue === undefined || existingValue === "") {
gidMetadataCache[gid][metadataKey] = metadataChunk[metadataKey];
}
}
}
function mergeUndefinedMetadataAndSave(gid, metadataChunk) {
mergeUndefinedMetadata(gid, metadataChunk);
saveStoredMetadata();
}
document.head.innerHTML = "";
document.body.innerHTML = "";
/*
Notification.requestPermission().then( function(permission) {
if (permission === 'denied') {
console.log("not allowed to use notifications.");
return;
}
if (permission === 'default') {
console.log("didn't decide whether or not to allow notifications.");
return;
}
console.log(`Notification permission`, permission);
});
*/
/*
var notifyFinalWage = new Notification(
`title`, {
body: `body`,
icon: `https://248qms3nhmvl15d4ne1i4pxl-wpengine.netdna-ssl.com/wp-content/uploads/2017/12/be-760x400.png`
});
*/
const EMPTY_STRING_ARRAY_RESPONSE = [""];
const READYSTATE_FINISHED = 4;
let handlerIndex;
let statusMessages = [];
var pandaTimeoutHandle;
var pingFunction;
statusMessages.messageIdIndex = 1;
statusMessages.addStatus = function(message, maxMessages = 10) {
this.unshift(`${this.messageIdIndex}: ${message}`);
this.messageIdIndex++;
while(this.length > maxMessages) {
this.pop();
}
};
statusMessages.renderHTML = function() {
let aggregate = "";
for(let i = 0; i < this.length; i++) {
aggregate += this[i] + "<br/>";
}
return aggregate;
};
const PANDA_DELAY_DEFAULT = 1000;
document.head.insertAdjacentHTML("afterbegin", `
<head>
<style>
.tiny-caps {
font-size: 0.7em;
text-transform: uppercase;
}
.hit-value {
font-weight: bold;
color: green;
}
</style>
</head>`);
document.body.insertAdjacentHTML("afterbegin", `
<body>
<div><span class="tiny-caps">Title:</span> <span id="hitTitle" style="color:purple; font-weight: bold;"></span> <span id="loadedFromCacheIndicator"></span> <span id="caughtSpan"></span> <span class="tiny-caps">Description:</span> <span id="hitDescription" style="color:purple; font-weight: bold;"></span></div>
<div><span class="tiny-caps">Requester:</span> <span id="requesterName" style="color:purple; font-weight: bold;"></span> <span class="tiny-caps">Requester ID:</span> <span id="rid" style="color:purple; font-weight: bold;"></span></div>
<div>Close On Accept: <input type='checkbox' id='closeOnAccept'> Index: <span id='instanceIndex'></span> / Instances: <span id='instanceCount'></span> / Turn: <span id='turn'></span> <span id="scanIndicator"></span></div>
<div>Running Panda for GID: <span id="gidSpan"></span> (<span class="hit-value">$<span id="hitValue"></span></span>)</div>
<div>Found at: <span id="sourceURL"></span></div>
<div>Panda Delay: <input id="pandaDelay" type="range" min="700" max="3000" value=${PANDA_DELAY_DEFAULT}> <span id="pandaDelayMsDisplay">${PANDA_DELAY_DEFAULT}</span>ms <span style='border: 1px dashed black; padding: 4px; background-color: #FFFFE0;'>Transfer to PC: <button id='transferToPandaCrazy'>HIT</button> <button id='transferAllToPandaCrazy'>All HITs</button> </span></div>
<div id="status"></div>
<div id="lastResponse"></div>
</body>
`);
function transferToPandaCrazy() {
PandaCrazy.addJob(document.querySelector("#gidSpan").innerText.trim());
window.close();
}
document.getElementById("transferToPandaCrazy").addEventListener("click", function(event) {
transferToPandaCrazy();
});
GM_addValueChangeListener("transfer-to-panda-crazy", function(name, oldValue, newValue, remote) {
transferToPandaCrazy();
GM_setValue("web-panda-instance-count", 0);
GM_deleteValue("web-panda-turn");
});
document.getElementById("transferAllToPandaCrazy").addEventListener("click", function(event) {
GM_setValue("transfer-to-panda-crazy", Math.random());
});
GM_addValueChangeListener("web-panda-instance-count", function(name, oldValue, newValue, remote) {
document.querySelector("#instanceCount").innerHTML = newValue;
if(handlerIndex >= newValue - 1) {
handlerIndex--;
if(handlerIndex <= 0) {handlerIndex = 0;}
document.querySelector("#instanceIndex").innerHTML = handlerIndex;
}
else {
}
clearTimeout(pandaTimeoutHandle);
pandaTimeoutHandle = setTimeout(pingFunction, pandaDelay);
});
GM_addValueChangeListener("web-panda-turn", function(name, oldValue, newValue, remote) {
document.querySelector("#turn").innerHTML = newValue;
});
GM_addValueChangeListener("web-panda-last-removed", function(name, oldValue, newValue, remote) {
document.querySelector("#instanceIndex").innerHTML = handlerIndex;
});
GM_addValueChangeListener("push-new-panda-delay-value", function(name, oldValue, newValue, remote) {
pandaDelay = newValue;
document.querySelector("#pandaDelay").value = pandaDelay;
clearTimeout(pandaTimeoutHandle);
pandaTimeoutHandle = setTimeout(pingFunction, pandaDelay);
document.getElementById("pandaDelayMsDisplay").innerText = pandaDelay;
});
let pandaDelay = PANDA_DELAY_DEFAULT;
let pandaDelayElement = document.getElementById("pandaDelay");
let pandaDelayMsDisplay = document.getElementById("pandaDelayMsDisplay");
let statusDisplay = document.getElementById("status");
let closeOnAccept = document.getElementById("closeOnAccept");
let scanIndicator = document.getElementById("scanIndicator");
let hitTitleSpan = document.getElementById("hitTitle");
let hitDescriptionSpan = document.getElementById("hitDescription");
let requesterNameSpan = document.getElementById("requesterName");
let requesterIDSpan = document.getElementById("rid");
let hitValueSpan = document.getElementById("hitValue");
let sourceURLSpan = document.getElementById("sourceURL");
let loadedFromCacheIndicator = document.getElementById("loadedFromCacheIndicator");
let caughtSpan = document.getElementById("caughtSpan");
function isMissingSomeMetadata() {
return hitTitleSpan.innerText === "" || hitDescriptionSpan.innerText === "" ||
requesterNameSpan.innerText === "" || requesterIDSpan.innerText === "";
}
pandaDelayElement.addEventListener("change", function(event) {
pandaDelay = event.target.value;
pandaDelayMsDisplay.innerText = pandaDelay;
GM_setValue( "push-new-panda-delay-value", event.target.value );
});
function cycleToNextHandler() {
document.body.style.backgroundColor = "white";
GM_setValue( "web-panda-turn", (GM_getValue("web-panda-turn") + 1) % GM_getValue("web-panda-instance-count") );
}
if(window.location.href.includes("registerWebPanda")) {
document.getElementById("status").innerHTML = "<p style='background-color: red; color: white; display: inline-block;'>Check around your title bar for an allow/deny prompt, or a diamond-shaped icon to click to allow this app to handle web+panda:// links.</p>";
// This is how you register the Web Panda protocol in your app.
navigator.registerProtocolHandler(
"web+panda", // This will always be "web+panda".
"https://worker.mturk.com/requesters/handleWebPanda?gid=%s", // %s will contain a URL-encoded version of the entire web+panda://(stuff) URL, including the protocol.
" Web Panda Handler (Default Handler)"); // User-readable string for the protocol rule.
}
else { // We're actually trying to grab a panda.
let instanceCount;
let storedPrevInstanceCount = GM_getValue("web-panda-instance-count");
if(storedPrevInstanceCount < 0) {storedPrevInstanceCount = 0;}
GM_setValue("web-panda-instance-count", instanceCount = (storedPrevInstanceCount || 0) + 1);
handlerIndex = instanceCount - 1;
document.querySelector("#instanceIndex").innerHTML = handlerIndex;
// Basic parsing of the web+panda protocol.
let webPandaURL = decodeURIComponent(window.location.href.match(/gid=(.*)/)[1]);
let webPandaURLWithDecodedArguents = decodeURIComponent(webPandaURL);
let parsedWebPandaURL = new URL(webPandaURL);
let webPandaURLParams = parsedWebPandaURL.searchParams;
let pandaGID = (webPandaURL.match(/3[A-Za-z0-9]{29}/) || EMPTY_STRING_ARRAY_RESPONSE)[0].toUpperCase();
let pandaURL = `https://worker.mturk.com/projects/${pandaGID}/tasks/accept_random`;
let once = webPandaURLParams.get("once") === "true" || webPandaURLParams.get("batchOrSurvey") === "survey";
let hitTitle = getMetadata(pandaGID, "hitTitle") || (webPandaURLWithDecodedArguents.includes("hitTitle=") ? webPandaURLParams.get("hitTitle") : "");
let hitDescription = getMetadata(pandaGID, "hitDescription") || (webPandaURLWithDecodedArguents.includes("hitDescription=") ? webPandaURLParams.get("hitDescription") : "");
let requesterName = getMetadata(pandaGID, "requesterName") || (webPandaURLWithDecodedArguents.includes("requesterName=") ? webPandaURLParams.get("requesterName") : "");
let requesterID = getMetadata(pandaGID, "rid") || (webPandaURLWithDecodedArguents.includes("rid=") ? webPandaURLParams.get("rid") : "");
let hitValue = Number ( getMetadata(pandaGID, "hitValue") || (webPandaURLWithDecodedArguents.includes("hitValue=") ? webPandaURLParams.get("hitValue") : "") );
let priority = getMetadata(pandaGID, "priority") || (webPandaURLWithDecodedArguents.includes("priority=") ? webPandaURLParams.get("priority") : "");
let sourceURL = getMetadata(pandaGID, "contextURL") || (webPandaURLWithDecodedArguents.includes("contextURL=") ? webPandaURLParams.get("contextURL") : "");
if(Boolean(gidMetadataCache[pandaGID])) {
// loadedFromCacheIndicator.innerHTML = `${GID_IN_DATABASE_EMOJI}`; // Using icons instead of emoji now.
loadedFromCacheIndicator.innerHTML = `<img title="You've run this catcher in the past. First run: ${new Date(gidMetadataCache[pandaGID].createdTimestamp).toLocaleString()}" src="${REPEAT_IMAGE}" width="${REPEAT_IMAGE_WIDTH_PX}px" height="${REPEAT_IMAGE_HEIGHT_PX}px" />`;
}
else {
loadedFromCacheIndicator.innerHTML = `<img title="This is the first time you've run this catcher. Started at: ${Date.now().toLocaleString()}" src="${NEW_IMAGE}" height="${NEW_IMAGE_HEIGHT_PX}px" />`;
}
if(Boolean(gidMetadataCache[pandaGID]) &&
Boolean(gidMetadataCache[pandaGID].caught)) {
caughtSpan.innerHTML = `<img title="You've caught this HIT at least once in the past. First catch: ${new Date(gidMetadataCache[pandaGID].caught).toLocaleString()}" src=${CAUGHT_IMAGE} width="${CAUGHT_IMAGE_WIDTH_PX}" height="${CAUGHT_IMAGE_HEIGHT_PX}">`;
}
mergeUndefinedMetadataAndSave(pandaGID, {
hitTitle,
hitDescription,
requesterName,
rid: requesterID,
hitValue,
priority,
contextURL: sourceURL
});
let sourceURLParsed;
if(sourceURL) {
sourceURLParsed = new URL(sourceURL);
}
hitTitleSpan.innerText = hitTitle;
hitDescriptionSpan.innerText = hitDescription;
requesterNameSpan.innerText = requesterName;
requesterIDSpan.innerText = requesterID;
hitValueSpan.innerText = hitValue.toLocaleString("US", {minimumFractionDigits: 2});
sourceURLSpan.innerHTML = `<a href="${sourceURL}" target="_blank">${sourceURLParsed || {hostname: ""}.hostname}</a>`;
if(once) {
closeOnAccept.checked = true;
}
document.getElementById("gidSpan").innerText = pandaGID || "[INVALID GID]";
// Basic single-GID cycler as default web+panda behavior.
// Have your app register and parse the protocol for more advanced behavior.
if(pandaGID) {
document.title = `searching: ${pandaGID}`;
}
else {
document.title = `ERROR: NO GID FOUND`;
}
if(GM_getValue("web-panda-turn") === undefined) {
GM_setValue("web-panda-turn", 0);
}
(function pingGID() {
pingFunction = pingGID;
pandaTimeoutHandle = setTimeout(pingGID, pandaDelay);
if(GM_getValue("web-panda-turn") !== handlerIndex) {return;}
if(SHOULD_INDICATE_TURN_WITH_FULL_YELLOW_SCAN) {document.body.style.backgroundColor = "yellow";}
scanIndicator.innerHTML = "πŸ’‘ [SCANNING]";
GM_xmlhttpRequest({
method: "GET",
url: pandaURL,
headers: {"Accept": "application/json"},
onreadystatechange: (response) => {
if(response.readyState === READYSTATE_FINISHED) {
const signinURLPrefix = "https://www.amazon.com/ap/signin";
const taskIDPrefix = `{"task_id`;
if(response.finalUrl.substr(0, signinURLPrefix.length) === signinURLPrefix) {
statusMessages.addStatus("<span style='color:red;'>❌ Logged out.</span>");
}
else if(response.responseText.includes("Page request rate exceeded")) {
statusMessages.addStatus("<span style='color:red;'>❌ Page Request Error.</span>");
}
else if(response.responseText.substr(0,taskIDPrefix.length) === taskIDPrefix) {
if(isMissingSomeMetadata()) {
console.time("Parse Mturk Response");
let acceptedHitInfo = JSON.parse(response.responseText);
console.timeEnd("Parse Mturk Response");
let hitsAvailable = acceptedHitInfo.project.assignable_hits_count;
let hitRewardUSD = acceptedHitInfo.project.monetary_reward.amount_in_dollars;
let hitTitle = acceptedHitInfo.project.title;
let hitDescription = acceptedHitInfo.project.description;
let requesterName = acceptedHitInfo.project.requester_name;
let requesterId = acceptedHitInfo.project.requester_id;
mergeUndefinedMetadataAndSave(pandaGID, {
hitTitle,
hitDescription,
requesterName,
rid: requesterId,
hitValue: hitRewardUSD
});
hitTitleSpan.innerText = hitTitle;
hitDescriptionSpan.innerText = hitDescription;
requesterNameSpan.innerText = requesterName;
requesterIDSpan.innerText = requesterId;
hitValueSpan.innerText = hitRewardUSD;
// console.log(acceptedHitInfo, response.responseText);
}
if(gidMetadataCache[pandaGID].caught === undefined) {
gidMetadataCache = getStoredMetadata(); // Make sure we're working with the latest stored DB before we commit changes.
setMetadata(pandaGID, "caught", Date.now());
saveStoredMetadata();
caughtSpan.innerHTML = `<img title="You've caught this HIT at least once in this session." src=${CAUGHT_IMAGE} width="${CAUGHT_IMAGE_WIDTH_PX}" height="${CAUGHT_IMAGE_HEIGHT_PX}">`;
}
statusMessages.addStatus("<span style='color:green;'>βœ” Accepted!</span>");
document.title = `FOUND GID: ${pandaGID}`;
}
else {
statusMessages.addStatus("<span style='color:orange;'>❌ Failed to accept.</span>");
}
if(document.title.substr(0,5) !== "FOUND") {
document.title = `searching (${statusMessages.messageIdIndex - 1}): ${pandaGID}`;
}
cycleToNextHandler();
scanIndicator.innerHTML = "";
statusDisplay.innerHTML = statusMessages.renderHTML();
if(document.title.substr(0,5) === "FOUND") {
if(closeOnAccept.checked) {
GM_notification({
title: `Accepted HIT And Closed`,
text: `Title: ${hitTitleSpan.innerText}\n` +
`Requester: ${requesterNameSpan.innerText}` +
`Desc: ${hitDescriptionSpan.innerText}\n` +
`Value: $${Number(hitValueSpan.innerText).toLocaleString("US", {minimumFractionDigits: 2})}`,
timeout: 0
});
window.close();
}
}
}
}
});
})();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment