Hermes HIT exporter

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

当前为 2016-08-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();
			})
	);
});