// ==UserScript== // @name Hermes HIT exporter // @namespace mobiusevalon.tibbius.com // @version 2.5 // @author Mobius Evalon // @description Adds an Export button to MTurk HIT capsules to share HITs on forums, reddit, etc. // @license Creative Commons Attribution-ShareAlike 4.0; http://creativecommons.org/licenses/by-sa/4.0/ // @require https://code.jquery.com/jquery-1.12.4.min.js // @require https://greasyfork.org/scripts/22593-mount-olympus/code/Mount%20Olympus.js?version=144469 // @include /^https{0,1}:\/\/\w{0,}\.?mturk\.com\/mturk\/(?:searchbar|viewsearchbar|sortsearchbar|findhits|viewhits|sorthits)/ // @exclude /&hit_scraper$/ // @grant none // @downloadURL none // ==/UserScript== $(document).ready(function() { // properties and methods prepended with an underscore are not meant to be used outside of the object literal hermes_template = { // properties _format:"", _template:"", tokens: {}, // methods __reset_tokens:function() { this.tokens = { hermes_version:"2.5", author_url:"https://greasyfork.org/en/users/9665-mobius-evalon", hermes_url:"https://greasyfork.org/en/scripts/21175-hermes-hit-exporter", shortened_author_url:"http://goo.gl/jqpg0h", shortened_hermes_url:"http://goo.gl/bNdTBj" }; }, _async_turkopticon:function(result) { // used as a callback in the olympian library's turkopticon function function color_prop(n) { switch(Math.round(n*1)) { case 0: return "black"; case 1: return "red"; case 2: return "red"; case 3: return "orange"; default: return "green"; } } function symbol_prop(n) { n = Math.round(n*1); var filled = "⚫⚫⚫⚫⚫", empty = "⚪⚪⚪⚪⚪"; return (filled.slice(0,n)+empty.slice(n)); } if(result.hasOwnProperty(this.tokens.requester_id)) { if($.type(result[this.tokens.requester_id]) === "object") { $.each(result[this.tokens.requester_id].attrs,function(k,v) { hermes_template.tokens["to_"+k] = v; hermes_template.tokens["to_"+k+"_color"] = color_prop(v); hermes_template.tokens["to_"+k+"_symbols"] = symbol_prop(v); }); } else this.tokens.to_wrapper = "[None]"; } else this.tokens.to_wrapper = "[Error]"; this._process(); }, _async_url_shorten:function(result) { // used as a callback for url_shortener() if(Object.keys(result).length) { $.each(result,function(key,val) { hermes_template.tokens[key] = val; }); if(this.tokens.hasOwnProperty("shortened_url_wrapper")) delete this.tokens.shortened_url_wrapper; } else this.tokens.shortened_url_wrapper = "[Error]"; this._process(); }, _contains_short_url_tokens:function() { return /\{shortened_[^}]+\}/i.test(this._template); }, _contains_to_tokens:function () { return /{to_(?:pay|fair|fast|comm|graphic)(?:_symbols|_color)?}/i.test(this._template); }, _expand_tokens:function() { var tmp = this._template; $.each(this.tokens,function(key,val) { if(key.slice(-8) === "_wrapper") tmp = tmp.replace(new RegExp(("<"+key+":[^>]+>"),"gi"),val); else tmp = tmp.replace(new RegExp(("\\{"+key+"\\}"),"gi"),val); }); tmp = tmp.replace(/{date_time}/ig,new Date().toString()).replace(/<[^>]+_wrapper:([^>]+)>/gi,"$1"); return tmp; }, _process:function() { // this function is called repeatedly to determine the status of the template display if(!this.tokens.hasOwnProperty("to_pay") && !this.tokens.hasOwnProperty("to_wrapper") && this._contains_to_tokens()) { output("Gathering Turkopticon data for "+this.tokens.requester_name+"..."); olympian_turkopticon([this.tokens.requester_id],this._async_turkopticon,this); } else if(!this.tokens.hasOwnProperty("shortened_url_wrapper") && this._contains_short_url_tokens() && this.urls_to_shorten().length) { output("Shortening URLs..."); url_shortener(this._async_url_shorten,this); } else this.display(); }, begin:function(obj) { if($.type(obj) === "object" && obj.hasOwnProperty("hit") && obj.hasOwnProperty("format")) { this.__reset_tokens(); this.mturk_info(obj.hit); this.switch_format(obj.format); } else throw new Error("Hermes HIT exporter: template was initialized with improper arguments"); }, compute_pph:function(time) { var mins = 0, secs = 0; if(time.indexOf(":") > -1) { mins = Math.floor(time.split(":")[0]*1); secs = Math.floor(time.split(":")[1]*1); // in case some smart aleck enters something like 1:75 while(secs > 59) { mins++; secs -= 60; } } else { mins = Math.floor(time*1); secs = (((time*1)-mins)*60); } if((mins+secs) <= 0) { if(this.tokens.hasOwnProperty("my_time")) delete this.tokens.my_time; if(this.tokens.hasOwnProperty("hourly_rate")) delete this.tokens.hourly_rate; this.tokens.pph_wrapper = ""; } else { this.tokens.my_time = (""+mins+":"+zero_pad(secs,2)); this.tokens.hourly_rate = "$"+(((this.tokens.hit_reward.slice(1)*1)/((mins*60)+secs))*3600).toFixed(2); if(this.tokens.hasOwnProperty("pph_wrapper")) delete this.tokens.pph_wrapper; } this._process(); }, display:function() { $("#hhe_update_time, #hhe_export_format").prop("disabled",false); output(this._expand_tokens(this._template)); }, get_raw_template:function(string) { return (localStorage.getItem("hermes_"+string+"_template") || default_template(string)); }, mturk_info:function($element) { function scrape_from_tooltip(tt) { return $("a[id*='"+tt+".tooltip']",$element).parent().next().text().collapse_whitespace(); } // basic HIT info that can be scraped off the page this.tokens.hit_name = $("a.capsulelink[href='#']",$element).first().text().collapse_whitespace(); this.tokens.hit_id = $("a[href*='groupId']",$element).first().attr("href").match(/groupId=([A-Z0-9]{30})(?:&|$)/)[1]; this.tokens.hit_desc = scrape_from_tooltip("description"); this.tokens.hit_time = scrape_from_tooltip("duration_to_complete"); this.tokens.hits_available = scrape_from_tooltip("number_of_hits"); this.tokens.hit_reward = scrape_from_tooltip("reward"); this.tokens.requester_name = $("a[href*='selectedSearchType=hitgroups']",$element).first().text().collapse_whitespace(); this.tokens.requester_id = $("a[href*='requesterId']",$element).first().attr("href").match(/requesterId=([A-Z0-9]{12,14})(?:&|$)/)[1]; // link properties for convenience, since these are long URLs that only use one bit of previously collected info this.tokens.preview_link = ("https://www.mturk.com/mturk/preview?groupId="+this.tokens.hit_id); this.tokens.panda_link = ("https://www.mturk.com/mturk/previewandaccept?groupId="+this.tokens.hit_id); this.tokens.requester_hits = ("https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="+this.tokens.requester_id); this.tokens.contact_requester = ("https://www.mturk.com/mturk/contact?requesterId="+this.tokens.requester_id+"&requesterName="+this.tokens.requester_name); this.tokens.to_reviews = ("https://turkopticon.ucsd.edu/"+this.tokens.requester_id); // parse qualifications var $qual_anchor = $("a[id*='qualificationsRequired.tooltip']",$element); if($qual_anchor.parent().next().text().trim() === "None") this.tokens.quals = "None"; else { var quals = []; $("tr:not(:first-of-type) td:first-of-type",$qual_anchor.closest("table")).each(function() {quals.push($(this).text().collapse_whitespace());}); this.tokens.quals = quals.join("; "); } // by default the user has provided no completion time estimate, so this wrapper needs to not be displayed until the user provides that info this.tokens.pph_wrapper = ""; }, switch_format:function(string) { string = (string || $("#hhe_export_format").val()); this._format = string; this._template = this.get_raw_template(string); this._process(); }, template_edited:function() { this._template = this.get_raw_template(this._format); this._process(); }, urls_to_shorten:function() { return this._template .match(/(\{shortened_[^}]+\})/gi) .filter(function(val) { return !hermes_template.tokens.hasOwnProperty(val.slice(1,-1)); }); } }; function default_template(format) { if(format === "vbulletin") return "[url={preview_link}][color=blue]{hit_name}[/color][/url] [[url={panda_link}][color=blue]PANDA[/color][/url]]\n"+ "[b]Reward[/b]: {hit_reward}\n"+ "[b]Time allowed[/b]: {hit_time}\n"+ "[b]Available[/b]: {hits_available}\n"+ "[b]Description[/b]: {hit_desc}\n"+ "[b]Qualifications[/b]: {quals}\n\n"+ "[b]Requester[/b]: [url={requester_hits}][color=blue]{requester_name}[/color][/url] [[url={contact_requester}][color=blue]Contact[/color][/url]]\n"+ "[url={to_reviews}][color=blue][b]Turkopticon[/b][/color][/url]: \n"+ "[size=8px]Generated {date_time} with [url={hermes_url}][color=blue]Hermes HIT Exporter[/color][/url] {hermes_version} by [url={author_url}][color=blue]Mobius Evalon[/color][/url][/size]"; else if(format === "markdown") return "[{hit_name}]({preview_link}) \\[[PANDA]({panda_link})\\] \n"+ "**Reward**: {hit_reward} \n"+ "**Time allowed**: {hit_time} \n"+ "**Available**: {hits_available} \n"+ "**Description**: {hit_desc} \n"+ "**Qualifications**: {quals}\n\n"+ "**Requester**: [{requester_name}]({requester_hits}) \\[[Contact]({contact_requester})\\] \n"+ "[**Turkopticon**]({to_reviews}): \n"+ "^Generated {date_time} with [Hermes HIT Exporter]({hermes_url}) {hermes_version} by [Mobius Evalon]({author_url})"; else if(format === "plaintext") return "{hit_name} [{preview_link}] \n"+ "Reward: {hit_reward} \n"+ "Time allowed: {hit_time} \n"+ "Available: {hits_available} \n"+ "Description: {hit_desc} \n"+ "Qualifications: {quals}\n\n"+ "Requester: {requester_name} [{requester_hits}] \n"+ "Turkopticon: [{to_reviews}] \n"+ "Generated {date_time} with Hermes HIT Exporter {hermes_version} by Mobius Evalon [{hermes_url}]"; else if(format === "irc") return "{hit_name} [ View: {shortened_preview_link} PANDA: {shortened_panda_link} ] "+ "Reward: {hit_reward} Time: {hit_time} | "+ "Requester: {requester_name} [ HITs: {shortened_requester_hits} TO: {shortened_to_reviews} ] Pay={to_pay} Fair={to_fair} Fast={to_fast} Comm={to_comm}"; } function output(s) { $("div#hermes_export_window textarea#hhe_export_output").show().text(s); } function reset_interface() { $("#hhe_edit_template").hide(); $("#hhe_export_output").hide(); $("#hhe_mode_swap").text("Edit"); $("#hhe_update_time").prop("disabled",true); $("#hhe_completion_time").val(""); $("#hhe_export_format").prop("disabled",true).val(localStorage.hermes_export_format || "vbulletin"); } function url_shortener(callback,scope) { var mirrors = [ "https://ns4t.net/yourls-api.php?action=bulkshortener&title=MTurk&signature=39f6cf4959" ], params = "", tokens = [], result = {}; function exit() { if($.type(callback) === "function") callback.call(scope,result); } // since we can never know which or how many shortened url tokens will appear because of // template editing, and since this function may be called when url shortening has already // been completed (switching template format), we have to check to make sure that we actually need // to query for shortened urls in the first place tokens = hermes_template.urls_to_shorten(); if(tokens.length) { params = ("&urls[]="+tokens.join("&urls[]=")); olympian_get_request(mirrors,params,function(response) { if(response.length) { response = response.split(";"); $.each(tokens,function(key,val) { result[val.slice(1,-1)] = response[key]; }); } else console.log("Hermes HIT exporter: url shortening service appeared to be queried successfully but returned no data"); exit(); }); } else exit(); } function zero_pad(a,l,r) { function repeat(g) { var s = ""; while(s.length < (l-a.length)) s += g.charAt(0); return s; } if($.type(a) !== "string") a = (""+a); if(r) return (a+repeat("0")); else return (repeat("0")+a); } Date.prototype.toString = function() { return (""+this.getDate()+" "+this.getMonthString().slice(0,3)+" "+this.getFullYear()+", "+this.getTwoDigitUTCHours()+":"+this.getTwoDigitUTCMinutes()+"."+this.getTwoDigitUTCSeconds()+" UTC"); }; Date.prototype.getMonthString = function() { switch(this.getMonth()) { case 0: return "January"; case 1: return "February"; case 2: return "March"; case 3: return "April"; case 4: return "May"; case 5: return "June"; case 6: return "July"; case 7: return "August"; case 8: return "September"; case 9: return "October"; case 10: return "November"; case 11: return "December"; } }; Date.prototype.getTwoDigitUTCHours = function() { return zero_pad(this.getUTCHours(),2); }; Date.prototype.getTwoDigitUTCMinutes = function() { return zero_pad(this.getUTCMinutes(),2); }; Date.prototype.getTwoDigitUTCSeconds = function() { return zero_pad(this.getUTCSeconds(),2); }; String.prototype.collapse_whitespace = function() { // both regular expressions could go in the same statement, but removing html may leave extraneous space behind return this.replace(/<[^>]*>/g,"").replace(/\s{2,}/g," ").trim(); }; $("head").append( $("