// ==UserScript== // @name HIT Forker // @version 1.1.1 // @description Monitors mturk.com for HITs // @author ThisPoorGuy // @icon https://i.imgur.com/RaPUMRP.png // @include https://worker.mturk.com/?finder_beta_test // @include https://worker.mturk.com/?hit_forker // @grant GM_log // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @connect turkerview.com // @require https://code.jquery.com/jquery-3.1.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/1.0.8/purify.min.js // @namespace https://greasyfork.org/users/163167 // @downloadURL none // ==/UserScript== // Acknowledgements // The core of this script was forked/stolen/adapted from Kadauchi's Hit Finder Beta script. Coding assistance in spots // provided by Salem Beats and ChrisTurk. TurkerView was created by ChrisTurk. This script hooks into JR Panda Crazy // Slothbear provided the code for text to speech, thanks! // Changelog // 1.1.1 = Fixed an issue with the TV API where it will stop if it encounters an error // 1.1 - Added new TurkerView API code // 1.0.6.1 - Further input sanatization improvements // 1.0.5 - Minor update to change the soundjay links to https to stop mixed content complaining. // 1.0.4 - Added an I button to the HIT log/HIT list so you can just click once to add something to the include list. // 1.0.3 - Further work on input sanitization // 1.0.2 - Fixed the issue where some asshat decided to inject code into a HIT name. THIS IS WHY WE CAN'T HAVE NICE THINGS. // 1.0.1 - Fixed IRC Export function. You're welcome one guy using IRC exports. // 1.0.0 - Added Text to speech for include list hits. There's a TTS checkbox in the show config section, this will override any sounds set up on your include list with a // text to speech notification. In order for the alert to trigger you need an include list entry with play sound enabled. Which sound you pick won't matter as this // will ignore that setting and use a vocal alert. Thanks to slothbear for the code. // 0.8.1.2 - Fixed an issue exporting HITS that have quotes in the title. // 0.8.1.1 - Added a limit to the number of characters you can stick into the fields on adding something to the block list to prevent accidental pasting of a block list import into the wrong spot. // 0.8.1 - Fixed a couple of export formatting bugs. // 0.8.0 - Added the ability to press a button to launch a qualification test if you don't qualify for something and a test is availible. // Working on adding requesting for auto-grant quals, but that's...trickier. // 0.7.6 - Fixed issue with the search qualified check box not working. // 0.7.5 - Added abilible hits to the Log display. Note this only shows the data from the time when the HIT was first seen, not any subsequent scans of it. // 0.7.4 - Removed column for masters, it wasn't used for anything and was broken anyway. Also removed hide masters because it worked off of that code, which wasn't working anyway // 0.7.3 - Returned a semi-colon to it's rightful position, even though it wasn't missed // 0.7.2 - Made it so that the Panda button sets pandas without once=true set. Whoops. // 0.7.0 - Big under the hood changes with how TV and TO review scores are gathered. // 0.6.5 - Further modifications to prevent availibility of external sites causing HF to stop randomly. // 0.6.4 - Send RequesterID over to PandaCrazy along with the other info. // 0.6.3 - Bringing PC functionality in house // 0.6.1 - Fixed panda button shading // 0.6.0 - Re-jiggered panda crazy integration code to use Salem's PC library. Now detects if panda crazy is actually running when you click. // 0.5.3 - Slight modifications to break integration with JR Panda Crazy, due to a memory leak issue. Will reach out to dev to see if we can clear it up and be happy together... // 0.5.2 - Code cleanup. // 0.5.1 - Fixed the panda buttons on the Found hits table. // 0.5.0 - Hitting the panda button now sends full HIT name, pay and requester name info to panda crazy too. Have you noticed these version number leaps are pretty arbitratry? // - Fixed a stupid typo in a variable name. // 0.4.3 - Switched TO request from a .get call to a .ajax call with a timeout to prevent the entire thing from exploding when TO's servers do. // 0.4.2 - Link to TV Requester profile in export. // 0.4.1 - Show/Hide HITs and Logged Hits settings are now saved across sessions // 0.4.0 - Blocking a HIT or a Requester will now remove that Hit or all hits from said requester from display in the Hit Log // 0.3.5 - Added a button to hide the new HITS table. Moved the button to hide the logged hits for consistancy. // 0.3.2 - Modified icon for desktop notifications. Added Requester TV score to hit export // 0.3.1 - Fix for amazon screwing with things. Thanks ChrisTurk! // 0.3.0 - Under the hood changes, removed code for running on www, added new launch URL, old ?finder_beta will be phased out eventually // 0.2.9 - Now acceptable to people with red/green color blindness! // 0.2.6 - Fixed a minor error which caused colors to not work properly. // 0.2.5 - Changed Coloration to respect TV reviews FIRST and then fall back on TO Values. Also changed colors. // 0.2.0 - Added TurkerView Hourly ratings to HIT results, fixed export links. // 0.1.5 - Some minor UI tweaks // 0.1.4 - Added some indication that you have already clicked a button to send HIT to PC. Only works in log currently. // 0.1.3 - Fixed issue with the Panda buttons in the HIT log not having the right GID // 0.1.2 - Fixed an issue with HITs that have double quotes in the title not working with the ignore hit by title button. I think. // 0.1.1 - Cleaned up the header, removed unused audio files // 0.1.0 - Made modifications to launch links with worker website. Added buttons to send information to PandaCrazy directly instead of copying link // TODO: // Remove www code // Clean up interface // Delete the above todos because they're already done. const ver = GM_info.scriptMetaStr.match(/version.*?(\d+.*)/)[1]; var worker = true; var _config = JSON.parse(localStorage.getItem('_finder')) || {}; _config.tv_api_key = localStorage.getItem('turkerview_api_key') || ''; var blocklist = JSON.parse(localStorage.getItem('_finder_bl')) || {}; var includelist = JSON.parse(localStorage.getItem('_finder_il')) || {}; // Compatability check if (_config.version !== '1.1') { _config = {}; } var config = { version : _config.version || '1.1', delay : _config.delay || '3', type : _config.type || 'LastUpdatedTime%3A1&pageSize=', size : _config.size || '25', rew : _config.rew || '0.00', avail : _config.avail || '0', mto : _config.mto || '0.00', alert : _config.alert || '0', qual : _config.hasOwnProperty('qual') ? _config.qual : true, new : _config.hasOwnProperty('new') ? _config.new : true, newaudio : _config.newaudio || 'beep', pb : _config.hasOwnProperty('pb') ? _config.pb : false, to : _config.hasOwnProperty('to') ? _config.to : false, tv : _config.hasOwnProperty('tv') ? _config.tv : true, nl : _config.hasOwnProperty('nl') ? _config.nl : false, bl : _config.hasOwnProperty('bl') ? _config.bl : false, m : _config.hasOwnProperty('m') ? _config.m : false, tts : _config.hasOwnProperty('tts') ? _config.tts : false, push : _config.push || 'access_token_here', tv_api_key : _config.tv_api_key || '', theme : _config.theme || 'default', custom : _config.custom || {main: 'FFFFFF', primary: 'CCCCCC', secondary: '111111', text: '000000', link: '0000EE', visited: '551A8B', prop : false}, to_theme : _config.to_theme || '1', h_hidden : _config.h_hidden || '0', l_hidden : _config.l_hidden || '0' }; console.log(config); console.log(config.tv_api_key); var themes = { 'default' : {main: 'FFFFFF', primary: 'CCCCCC', secondary: '111111', menu: '373b44', menutext: 'FFFFFF', text: '000000', link: '0000EE', visited: '551A8B', prop : true}, 'light' : {main: 'FFFFFF', primary: 'CCCCCC', secondary: '111111', menu: '373b44', menutext: 'FFFFFF', text: '000000', link: '0000EE', visited: '551A8B', prop : true}, 'dark' : {main: '404040', primary: '666666', secondary: 'FFFFFF', menu: '202020', menutext: 'FFFFFF', text: 'FFFFFF', link: 'FFFFFF', visited: 'B3B3B3', prop : true}, 'darker' : {main: '000000', primary: '262626', secondary: 'FFFFFF', menu: '373b44', menutext: 'FFFFFF', text: 'FFFFFF', link: 'FFFFFF', visited: 'B3B3B3', prop : true}, 'custom' : config.custom }; var turkerview = { }; var turkerview_update = 0; var requesters = [ ]; var tvTimeoutCache = [ ]; var searches = 0, logged = 0, hitlog = {}, noti_delay = [], push_delay = []; const ViewHeaders = new Headers([ ['X-VIEW-KEY', config.tv_api_key], ['X-APP-KEY', 'HIT Forker'], ['X-APP-VER', ver] //SemVer ]); // General Configuration variables var url, upd, num, rew, minrew, searchqual, pandaurl; url = 'https://worker.mturk.com/?'; pandaurl = 'https://worker.mturk.com'; upd = '&sort=updated_desc&page_size='; num = '&sort=num_hits_desc&page_size='; rew = '&sort=reward_desc&page_size='; minrew = '&filters%5Bmin_reward%5D='; searchqual = '&filters%5Bqualified='; var PandaCrazy = (function createPandaCrazy() { let _self = this; let _lastSentPingTime; let _lastReceivedPongTime; let _onlineSinceLastPing; let _pcListener; const MAX_WAIT_FOR_PANDA_CRAZY_RESPONSE_MS = 1000; function ping() { _lastSentPingTime = Date.now(); localStorage.setItem("JR_message_ping_pandacrazy", `{"theTarget": "${Math.random()}"}`); } function hasIndicatedOnlineSinceLastPing() { if(_lastSentPingTime !== undefined && _lastReceivedPongTime !== undefined) { return _lastReceivedPongTime >= _lastSentPingTime; } else { return undefined; } } function online() { function respondToStorage(resolve, reject, e) { if(e.key.includes("JR_message_pong") && Boolean(e.newValue)) { _lastReceivedPongTime = Date.now(); let pongData = JSON.parse(e.newValue); let lag = Number(pongData.time) - Number(_lastReceivedPongTime); if(hasIndicatedOnlineSinceLastPing()) { resolve("online"); } } } let isOnlinePromise = new Promise((resolve, reject) => { setTimeout(() => {reject("timeout");}, MAX_WAIT_FOR_PANDA_CRAZY_RESPONSE_MS); if(_pcListener) {window.removeEventListener("storage", _pcListener);} _pcListener = respondToStorage.bind(window, resolve, reject); window.addEventListener("storage", _pcListener); /* window.addEventListener("storage", e => { // console.log("Storage Event", e); if(e.key.includes("JR_message_pong") && Boolean(e.newValue)) { _lastReceivedPongTime = Date.now(); let pongData = JSON.parse(e.newValue); let lag = Number(pongData.time) - Number(_lastReceivedPongTime); if(hasIndicatedOnlineSinceLastPing()) { resolve("online"); } } }); */ }); ping(); return isOnlinePromise; } function addJob(gid, once, metadata) { let commandString = once ? "addOnceJob" : "addJob"; localStorage.setItem("JR_message_pandacrazy", JSON.stringify({ time: Date.now(), command: commandString, data: { groupId: gid, title: (metadata ? metadata.hitTitle || metadata.title : undefined), requesterName: (metadata ? metadata.requesterName : undefined), requesterId: (metadata ? metadata.requesterID || metadata.requesterId || metadata.rid : undefined), pay: (metadata ? metadata.hitValue || metadata.pay : undefined), duration: (metadata ? metadata.duration : undefined), hitsAvailable: (metadata ? metadata.hitsAvailable : undefined) } })); } function startJob(gid) { localStorage.setItem("JR_message_pandacrazy", JSON.stringify({ time: Date.now(), command: "startcollect", data: { groupId: gid } })); } return { addJob, startJob, ping, online }; })(); const SPEECH_VOICE = 3; //0 - 21ish const SPEECH_RATE = 0.9; //1 - 10 (default is 1) const SPEECH_VOLUME = 1; //0 - 1 (default is 1) const SPEECH_LANG = 'en-US'; //(default is 'en') //this is what does it all! unsafeWindow.slothbearsTTS = function(obj) { let phrase = "Hit Found!" + obj.name; var speech = new Speech(); if (speech.supported()) { speech.speak(phrase); } }; var Speech = function() { }; Speech.voices = null; (function() { if ('speechSynthesis' in window) { // First call to getVoices may be null...later an event indicates when it is loaded Speech.voices = window.speechSynthesis.getVoices(); // Save voices when loaded after first call window.speechSynthesis.onvoiceschanged = function() { Speech.voices = window.speechSynthesis.getVoices(); }; } })(); Speech.prototype.supported = function() { return Speech.voices !== null; }; Speech.prototype.speak = function(text) { if (Speech.voices !== null) { var speech = new SpeechSynthesisUtterance(text); speech.rate = SPEECH_RATE; speech.voice = speechSynthesis.getVoices()[SPEECH_VOICE]; speech.lang = SPEECH_LANG; speech.volume = SPEECH_VOLUME; window.speechSynthesis.speak(speech); } }; $('head').html( '