Hermes HIT exporter

Adds an Export button to MTurk HIT capsules to share HITs on forums, reddit, etc.

目前為 2016-08-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Hermes HIT exporter
// @namespace    mobiusevalon.tibbius.com
// @version      2.4
// @author       Mobius Evalon <[email protected]>
// @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=144091
// @include      /^https{0,1}:\/\/\w{0,}\.?mturk\.com\/mturk\/(?:searchbar|viewsearchbar|sortsearchbar|findhits|viewhits|sorthits)/
// @exclude      /&hit_scraper$/
// @grant        none
// ==/UserScript==

$(document).ready(function() {
	script_version = "2.4 beta";
	function hit_info($element) {
		function scrape_from_tooltip(tt) {
			return $("a[id*='"+tt+".tooltip']",$element).parent().next().text().collapse_whitespace();
		}
		// basic HIT info that must be scraped off the page
		var obj = {
			hit_name:$("a.capsulelink[href='#']",$element).first().text().collapse_whitespace(),
			hit_id:$("a[href*='groupId']",$element).first().attr("href").match(/groupId=([A-Z0-9]{30})(?:&|$)/)[1],
			hit_desc:scrape_from_tooltip("description"), //$("a[id*='description.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hit_time:scrape_from_tooltip("duration_to_complete"), //$("a[id*='duration_to_complete.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hits_available:scrape_from_tooltip("number_of_hits"), //$("a[id*='number_of_hits.tooltip']",$element).parent().next().text().collapse_whitespace(),
			hit_reward:scrape_from_tooltip("reward"), //$("a[id*='reward.tooltip']",$element).parent().next().text().collapse_whitespace(),
			requester_name:$("a[href*='selectedSearchType=hitgroups']",$element).first().text().collapse_whitespace(),
			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
		obj.preview_link = ("https://www.mturk.com/mturk/preview?groupId="+obj.hit_id);
		obj.panda_link = ("https://www.mturk.com/mturk/previewandaccept?groupId="+obj.hit_id);
		obj.requester_hits = ("https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="+obj.requester_id);
		obj.contact_requester = ("https://www.mturk.com/mturk/contact?requesterId="+obj.requester_id+"&requesterName="+obj.requester_name);
		obj.to_reviews = ("https://turkopticon.ucsd.edu/"+obj.requester_id);

		// parse qualifications
		var $qual_anchor = $("a[id*='qualificationsRequired.tooltip']",$element);
		if($qual_anchor.parent().next().text().trim() === "None") obj.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());});
			obj.quals = quals.join("; ");
		}

		obj.author_url = "https://greasyfork.org/en/users/9665-mobius-evalon";
		obj.shortened_author_url = "http://goo.gl/jqpg0h";
		obj.hermes_version = script_version;
		obj.hermes_url = "https://greasyfork.org/en/scripts/21175-hermes-hit-exporter";
		obj.shortened_hermes_url = "http://goo.gl/bNdTBj";

		// by default the user has provided no completion time estimate, so this block needs to not be displayed until the user provides that info
		obj.pph_block = "";

		localStorage.hermes_hit = JSON.stringify(obj);
		return obj;
	}

	function get_template(t) {
		var format = (t || $("#hhe_export_format").val());
		return (localStorage.getItem("hermes_"+format+"_template") || default_template(format));
	}

	function display_template() {
		var obj = localstorage_obj("hermes_hit"),
			template = get_template($("#hhe_export_format").val());
		$("#hhe_update_time, #hhe_export_format").prop("disabled",false);
		$.each(obj,function(key,val) {
			if(key.slice(-6) === "_block") template = template.replace(new RegExp(("<"+key+":.*?>"),"gi"),val);
			else template = template.replace(new RegExp(("\\{"+key+"\\}"),"gi"),val);
		});
		// date_time has to handled here because putting it in the hit obj would cache the data instead of
		// using current information.  the other replace is to remove unused blocks
		template = template.replace(/{date_time}/ig,new Date().toString()).replace(/<.+?_block:(.*?)>/gi,"$1");
		$("#hhe_export_output").show().text(template);
	}

	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}<pph_block:/{my_time} ({hourly_rate}/hour)>\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]: <to_block:[Pay: [color={to_pay_color}]{to_pay}[/color]] [Fair: [color={to_fair_color}]{to_fair}[/color]] [Fast: [color={to_fast_color}]{to_fast}[/color]] [Comm: [color={to_comm_color}]{to_comm}[/color]]>\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}<pph_block:/{my_time} ({hourly_rate}/hour)>  \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}): <to_block:\\[Pay: {to_pay}\\] \\[Fair: {to_fair}\\] \\[Fast: {to_fast}\\] \\[Comm: {to_comm}\\]>  \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}<pph_block:/{my_time} ({hourly_rate}/hour)>  \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_block:[Pay: {to_pay}] [Fair: {to_fair}] [Fast: {to_fast}] [Comm: {to_comm}]> [{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}<pph_block:/{my_time} ({hourly_rate}/hour)> 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 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) {
		var mirrors = [
				"https://ns4t.net/yourls-api.php?action=bulkshortener&title=MTurk&signature=39f6cf4959"
			],
			params = "",
			hit = localstorage_obj("hermes_hit"),
			template = get_template($("#hhe_export_format").val()),
			tokens = [],
			result = {};

		function exit() {
			if($.type(callback) === "function") callback(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 = template.match(/(\{shortened_[^}]+\})/gi);
		if(tokens.length) {
			$.each(tokens,function(key,val) {
				if(!hit.hasOwnProperty(val.slice(1,-1))) params += ("&urls[]="+hit[val.slice(11,-1)]);
			});
		}

		if(params.length) {
			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 url_shortening_complete(result) {
		var hit = localstorage_obj("hermes_hit"),
			template = get_template($("#hhe_export_format").val());

		$.each(result,function(key,val) {
			hit[key] = val;
		});

		localStorage.hermes_hit = JSON.stringify(hit);
		display_template();
	}

	function to_complete(result) {
		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));
		}

		var hit_obj = localstorage_obj("hermes_hit"),
			template = get_template($("#hhe_export_format").val());

		if(result.hasOwnProperty(hit_obj.requester_id)) {
			if($.type(result[hit_obj.requester_id]) === "object") {
				$.each(result[hit_obj.requester_id].attrs,function(k,v) {
					hit_obj["to_"+k] = v;
					hit_obj["to_"+k+"_color"] = color_prop(v);
					hit_obj["to_"+k+"_symbols"] = symbol_prop(v);
				});
			}
			else hit_obj.to_block = "[None]";
		}
		else hit_obj.to_block = "[Error]";

		localStorage.hermes_hit = JSON.stringify(hit_obj);
		if(contains_short_url_tokens(template)) {
			status("Shortening URLs...");
			url_shortener(url_shortening_complete);
		}
		else display_template();
	}

	function json_obj(json) {
		var obj;
		if(typeof json === "string" && json.trim().length) {
			try {obj = JSON.parse(json);}
			catch(e) {console.log("Malformed JSON object.  Error message from JSON library: ["+e.message+"]");}
		}
		return obj;
	}

	function localstorage_obj(key) {
		var obj = json_obj(localStorage.getItem(key));
		if(typeof obj !== "object") localStorage.removeItem(key);
		return obj;
	}

	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);
	}

	function contains_to_tokens(tmp) {
		return /{to_(?:pay|fair|fast|comm|graphic)(?:_symbols|_color)?}/i.test(tmp);
	}

	function contains_short_url_tokens(tmp) {
		return /{shortened_.+}/i.test(tmp);
	}

	function status(s) {
		$("div#hermes_export_window textarea#hhe_export_output").show().text(s);
	}

	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(
		$("<style/>")
			.attr("type","text/css")
			.text("#hermes_export_button {height: 16px; font-size: 10px; font-weight: bold; border: 1px solid; margin-left: 5px; padding: 0px 5px; background-color: transparent;} "+
				  "#hermes_export_window {position: fixed; left: 15vw; top: 10vh; background-color: #a5ccdd; border: 2px solid #5c9ebf; border-radius: 10px;} "+
			  	  "#hermes_export_window textarea {width: 400px; height: 250px; margin: 0px auto; display: block;} "+
			  	  "#hermes_export_window h1 {margin: 10px 0px; padding: 0px; font-size: 150%; font-weight: bold; text-align: center;} "+
			  	  "#hermes_export_window input#hhe_completion_time {width: 40px; text-align: center; margin: 0px 5px;} "+
			  	  "#hermes_export_window button#hhe_close {display: block; margin: 10px auto; clear: both;} "+
			  	  "#hermes_export_window select#hhe_export_format {margin: 0px 5px;} "+
			  	  "#hermes_export_window div.hhe_options div.left {display: inline-block; float: left; text-align: left;} "+
			  	  "#hermes_export_window div.hhe_options div.right {display: inline-block; float: right; text-align: right;} "+
			  	  "#hermes_export_window button {font: inherit;} "+
				  ".noscroll {overflow: hidden;} ")
	);
	$("body").append(
		$("<div/>")
			.attr("id","hermes_export_window")
			.append(
				$("<h1/>")
					.text("Hermes HIT exporter"),
				$("<textarea/>")
					.attr("id","hhe_export_output")
					.hide(),
				$("<textarea/>")
					.attr("id","hhe_edit_template")
					.hide(),
				$("<div/>")
					.attr("class","hhe_options")
					.append(
						$("<div/>")
							.attr("class","left")
							.append(
								$("<select/>")
									.attr("id","hhe_export_format")
									.append(
										$("<option/>")
											.attr("value","vbulletin")
											.text("vBulletin"),
										$("<option/>")
											.attr("value","markdown")
											.text("Markdown"),
										$("<option/>")
											.attr("value","plaintext")
											.text("Plain text"),
										$("<option/>")
											.attr("value","irc")
											.text("IRC")
									)
									.change(function() {
										localStorage.hermes_export_format = $(this).val();
										if($("#hhe_mode_swap").text() === "Edit")
										{
											var obj = localstorage_obj("hermes_hit"),
												tmp = get_template($(this).val());
											if(!obj.hasOwnProperty("to_pay") && contains_to_tokens(tmp)) {
												status("Gathering Turkopticon data for "+obj.requester_name+"...");
												olympian_turkopticon([obj.requester_id],to_complete);
											}
											else if(contains_short_url_tokens(tmp)) {
												status("Shortening URLs...");
												url_shortener(url_shortening_complete);
											}
											else display_template();
										}
									}),
								$("<button/>")
									.attr("id","hhe_mode_swap")
									.text("Edit")
									.click(function() {
										if($(this).text() === "Edit") {
											$("#hhe_edit_template").show().text(get_template($("#hhe_export_format").val()));
											$("#hhe_export_output").hide();
											$("#hhe_reset_template").show();
											$("#hhe_export_format, #hhe_update_time").prop("disabled",true);
											$(this).text("Done");
										}
										else {
											localStorage.setItem(("hermes_"+$("#hhe_export_format").val()+"_template"),$("#hhe_edit_template").val());
											$("#hhe_export_format, #hhe_update_time").prop("disabled",false);
											$("#hhe_edit_template, #hhe_reset_template").hide();
											$(this).text("Edit");
											display_template();
										}
									}),
								$("<button/>")
									.attr("id","hhe_reset_template")
									.text("Reset")
									.click(function() {
										if($("#hhe_mode_swap").text() === "Done" && confirm("Are you sure you want to reset the "+$("#hhe_export_format option:selected").text()+" template to default?\n\nThis can't be undone.")) {
											localStorage.removeItem("hermes_"+$("#hhe_export_format").val()+"_template");
											$("#hhe_edit_template").val(get_template($("#hhe_export_format").val()));
										}
									})
									.hide()
							),
						$("<div/>")
							.attr("class","right")
							.append(
								$("<label/>")
									.append(
										document.createTextNode("Time:"),
										$("<input/>")
											.attr({
												"type":"text",
									   			"id":"hhe_completion_time"
											})
							   		),
								$("<button/>")
									.text("Update")
									.attr("id","hhe_update_time")
									.click(function() {
										var hit = localstorage_obj("hermes_hit"),
											time = $("#hhe_completion_time").val(),
											mins = 0,
											secs = 0;

										if(time.indexOf(":") > -1) {
											mins = Math.floor(time.split(":")[0]*1);
											secs = Math.floor(time.split(":")[1]*1);
										}
										else {
											mins = Math.floor(time*1);
											secs = (((time*1)-mins)*60);
										}

										// in case some smart aleck enters something like 1:75
										while(secs > 59) {
											mins++;
											secs -= 60;
										}

										if((mins+secs) <= 0) {
											if(hit.hasOwnProperty("my_time")) delete hit.my_time;
											if(hit.hasOwnProperty("hourly_rate")) delete hit.hourly_rate;
											hit.pph_block = "";
										}
										else {
											hit.my_time = (""+mins+":"+zero_pad(secs,2));
											hit.hourly_rate = "$"+(((hit.hit_reward.slice(1)*1)/((mins*60)+secs))*3600).toFixed(2);
											if(hit.hasOwnProperty("pph_block")) delete hit.pph_block;
										}
										localStorage.hermes_hit = JSON.stringify(hit);
										display_template();
									})
							)
					),
				$("<button/>")
					.attr("id","hhe_close")
					.text("Close")
					.click(function() {
						$("#hermes_export_window").hide();
						$("body").removeClass("noscroll");
					})
			)
			.hide()
	);

	$("a.capsulelink").after(
		$("<button/>")
			.attr("id","hermes_export_button")
			.text("Export")
			.click(function() {
				reset_interface();

				var obj = hit_info($(this).closest("table").parent().closest("table")),
					template = get_template($("#hhe_export_format").val());

				$("div#hermes_export_window").show();
				$("body").addClass("noscroll"); // so that quick scroll flicks won't scroll the page behind the export window, which is really annoying

				// only query turkopticon if to tokens are present in the template
				if(contains_to_tokens(template)) {
					status("Gathering Turkopticon data for "+obj.requester_name+"...");
					olympian_turkopticon([obj.requester_id],to_complete);
				}
				else if(contains_short_url_tokens(template)) {
					status("Shortening URLs...");
					url_shortener(url_shortening_complete);
				}
				else display_template();
			})
	);
});