[MTurk Worker] HIT Exporter for Slack

Allows you to export HITs as formatted text with short, plain, bbcode or markdown styling.

  1. // ==UserScript==
  2. // @name [MTurk Worker] HIT Exporter for Slack
  3. // @namespace https://github.com/Kadauchi
  4. // @version 1.0.31
  5. // @description Allows you to export HITs as formatted text with short, plain, bbcode or markdown styling.
  6. // @author Kadauchi
  7. // @icon http://i.imgur.com/oGRQwPN.png
  8. // @include https://worker.mturk.com/*
  9. // @grant GM_setClipboard
  10. // ==/UserScript==
  11.  
  12. /* globals GM_setClipboard */
  13.  
  14. const hitExports = `all` // Valid options are: `all`, `short`, `plain`, `bbcode` or `markdown`
  15. const turkerview = true // Use turkerview in HIT exports
  16. const turkopticon = true // Use turkopticon in HIT exports
  17. const turkopticon2 = true // Use turkopticon2 in HIT exports
  18.  
  19. async function short (event, object) {
  20. window.alert(`Short exports are not supported yet`)
  21. }
  22.  
  23. async function plain (event, object) {
  24. const hit = object || JSON.parse(event.target.dataset.hit)
  25. const requesterReview = await getRequesterReview(hit.requester_id)
  26. const reviewsTemplate = []
  27.  
  28. if (requesterReview.turkerview !== undefined) {
  29. const tv = requesterReview.turkerview
  30. const tvRatings = tv.ratings
  31.  
  32. reviewsTemplate.push([
  33. `TV:`,
  34. `[Hrly: ${tvRatings.hourly}]`,
  35. `[Pay: ${tvRatings.pay}]`,
  36. `[Fast: ${tvRatings.fast}]`,
  37. `[Comm: ${tvRatings.comm}]`,
  38. `[Rej: ${tv.rejections}]`,
  39. `[ToS: ${tv.tos}]`,
  40. `[Blk: ${tv.blocks}]`,
  41. `• https://turkerview.com/requesters/${hit.requester_id}`
  42.  
  43. ].join(` `))
  44. } else if (turkerview === true) {
  45. reviewsTemplate.push(`TV: No Reviews https://turkerview.com/requesters/${hit.requester_id}`)
  46. }
  47.  
  48. if (requesterReview.turkopticon !== undefined) {
  49. const to = requesterReview.turkopticon
  50. const toAttrs = to.attrs
  51.  
  52. reviewsTemplate.push([
  53. `TO:`,
  54. `[Pay: ${toAttrs.pay}]`,
  55. `[Fast: ${toAttrs.fast}]`,
  56. `[Comm: ${toAttrs.comm}]`,
  57. `[Fair: ${toAttrs.fair}]`,
  58. `[Reviews: ${to.reviews}]`,
  59. `[ToS: ${to.tos_flags}]`,
  60. `• https://turkopticon.ucsd.edu/${hit.requester_id}`
  61. ].join(` `))
  62. } else if (turkopticon === true) {
  63. reviewsTemplate.push(`TO: No Reviews https://turkopticon.ucsd.edu/${hit.requester_id}`)
  64. }
  65.  
  66. if (requesterReview.turkopticon2 !== undefined) {
  67. const to2 = requesterReview.turkopticon2
  68. const to2Recent = to2.recent
  69.  
  70. reviewsTemplate.push([
  71. `TO2:`,
  72. `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
  73. `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
  74. `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
  75. `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
  76. `[Rej: ${to2Recent.rejected[0]}]`,
  77. `[ToS: ${to2Recent.tos[0]}]`,
  78. `[Brk: ${to2Recent.broken[0]}]`,
  79. `https://turkopticon.info/requesters/${hit.requester_id}`
  80. ].join(` `))
  81. } else if (turkopticon2 === true) {
  82. reviewsTemplate.push(`TO2: No Reviews https://turkopticon.info/requesters/${hit.rid}`)
  83. }
  84.  
  85. const exportTemplate = [
  86. `Title: ${hit.title} https://worker.mturk.com/projects/${hit.hit_set_id}/tasks • https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random`,
  87. `Requester: ${hit.requester_name} https://worker.mturk.com/requesters/${hit.requester_id}/projects`,
  88. reviewsTemplate.join(`\n`),
  89. `Reward: ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
  90. `Duration: ${hit.assignment_duration_in_seconds.toTimeString()}`,
  91. `Available: ${hit.assignable_hits_count}`,
  92. `Description: ${hit.description}`,
  93. `Qualifications: ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  94. ].filter((item) => item !== undefined).join(`\n`)
  95.  
  96. GM_setClipboard(exportTemplate)
  97.  
  98. const notification = new window.Notification(`Plain HIT Export has been copied to your clipboard.`)
  99. setTimeout(notification.close.bind(notification), 10000)
  100. }
  101.  
  102. async function bbcode (event, object) {
  103. const hit = object || JSON.parse(event.target.dataset.hit)
  104. const requesterReview = await getRequesterReview(hit.requester_id)
  105. const reviewsTemplate = []
  106.  
  107. const ratingColor = (rating) => {
  108. if (rating > 3.99) {
  109. return `[color=#00cc00]${rating}[/color]`
  110. } else if (rating > 2.99) {
  111. return `[color=#cccc00]${rating}[/color]`
  112. } else if (rating > 1.99) {
  113. return `[color=#cc6600]${rating}[/color]`
  114. } else if (rating > 0.00) {
  115. return `[color=#cc0000]${rating}[/color]`
  116. }
  117. return rating
  118. }
  119.  
  120. const percentColor = (rating) => {
  121. if (rating[1] > 0) {
  122. const percent = Math.round(rating[0] / rating[1] * 100)
  123.  
  124. if (percent > 79) {
  125. return `[color=#00cc00]${percent}%[/color] of ${rating[1]}`
  126. } else if (percent > 59) {
  127. return `[color=#cccc00]${percent}%[/color] of ${rating[1]}`
  128. } else if (percent > 39) {
  129. return `[color=#cc6600]${percent}%[/color] of ${rating[1]}`
  130. }
  131. return `[color=#cc0000]${percent}%[/color] of ${rating[1]}`
  132. }
  133. return `---`
  134. }
  135.  
  136. const goodBadColor = (rating) => {
  137. return `[color=${rating === 0 ? `#00cc00` : `#cc0000`}]${rating}[/color]`
  138. }
  139.  
  140. if (requesterReview.turkerview !== undefined) {
  141. const tv = requesterReview.turkerview
  142.  
  143. reviewsTemplate.push([
  144. `[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:`,
  145. `[Hrly: ${tv.ratings.hourly}]`,
  146. `[Pay: ${ratingColor(tv.ratings.pay)}]`,
  147. `[Fast: ${ratingColor(tv.ratings.fast)}]`,
  148. `[Comm: ${ratingColor(tv.ratings.comm)}]`,
  149. `[Rej: ${goodBadColor(tv.rejections)}]`,
  150. `[ToS: ${goodBadColor(tv.tos)}]`,
  151. `[Blk: ${goodBadColor(tv.blocks)}][/b]`
  152. ].join(` `))
  153. } else if (turkerview === true) {
  154. reviewsTemplate.push(`[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:[/b] No Reviews`)
  155. }
  156.  
  157. if (requesterReview.turkopticon !== undefined) {
  158. const to = requesterReview.turkopticon
  159. const toAttrs = to.attrs
  160.  
  161. if (toAttrs) {
  162. reviewsTemplate.push([
  163. `[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:`,
  164. `[Pay: ${ratingColor(toAttrs.pay)}]`,
  165. `[Fast: ${ratingColor(toAttrs.fast)}]`,
  166. `[Comm: ${ratingColor(toAttrs.comm)}]`,
  167. `[Fair: ${ratingColor(toAttrs.fair)}]`,
  168. `[Reviews: ${to.reviews}]`,
  169. `[ToS: ${goodBadColor(to.tos_flags)}][/b]`
  170. ].join(` `))
  171. } else {
  172. reviewsTemplate.push(`[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:[/b] No Reviews`)
  173. }
  174. } else if (turkopticon === true) {
  175. reviewsTemplate.push(`[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:[/b] No Reviews`)
  176. }
  177.  
  178. if (requesterReview.turkopticon2 !== undefined) {
  179. const to2 = requesterReview.turkopticon2
  180. const to2Recent = to2.recent
  181.  
  182. reviewsTemplate.push([
  183. `[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:`,
  184. `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
  185. `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
  186. `[Res: ${percentColor(to2Recent.comm)}]`,
  187. `[Rec: ${percentColor(to2Recent.recommend)}]`,
  188. `[Rej: ${goodBadColor(to2Recent.rejected[0])}]`,
  189. `[ToS: ${goodBadColor(to2Recent.tos[0])}]`,
  190. `[Brk: ${goodBadColor(to2Recent.broken[0])}][/b]`
  191. ].join(` `))
  192. } else if (turkopticon2 === true) {
  193. reviewsTemplate.push(`[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:[/b] No Reviews`)
  194. }
  195.  
  196. const exportTemplate = [
  197. `[b]Title:[/b] [url=https://worker.mturk.com/projects/${hit.hit_set_id}/tasks]${hit.title}[/url] | [url=https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random]PANDA[/url]`,
  198. `[b]Requester:[/b] [url=https://worker.mturk.com/requesters/${hit.requester_id}/projects]${hit.requester_name}[/url] [${hit.requester_id}] ([url=https://worker.mturk.com/requesters/${hit.requester_id}]Contact[/url])`,
  199. reviewsTemplate.join(`\n`),
  200. `[b]Reward:[/b] ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
  201. `[b]Duration:[/b] ${hit.assignment_duration_in_seconds.toTimeString()}`,
  202. `[b]Available:[/b] ${hit.assignable_hits_count}`,
  203. `[b]Description:[/b] ${hit.description}`,
  204. `[b]Qualifications:[/b] ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  205. ].filter((item) => item !== undefined).join(`\n`)
  206.  
  207. GM_setClipboard(`[table][tr][td]${exportTemplate}[/td][/tr][/table]`)
  208.  
  209. const notification = new window.Notification(`BBCode HIT Export has been copied to your clipboard.`)
  210. setTimeout(notification.close.bind(notification), 10000)
  211. }
  212.  
  213. async function markdown (event, object) {
  214. const hit = object || JSON.parse(event.target.dataset.hit)
  215. const requesterReview = await getRequesterReview(hit.requester_id)
  216. const reviewsTemplate = []
  217.  
  218. if (requesterReview.turkerview !== undefined) {
  219. const tv = requesterReview.turkerview
  220. const tvRatings = tv.ratings
  221.  
  222. reviewsTemplate.push([
  223. `**[TV](https://turkerview.com/requesters/${hit.requester_id}):**`,
  224. `[Hrly: ${tvRatings.hourly}]`,
  225. `[Pay: ${tvRatings.pay}]`,
  226. `[Fast: ${tvRatings.fast}]`,
  227. `[Comm: ${tvRatings.comm}]`,
  228. `[Rej: ${tv.rejections}]`,
  229. `[ToS: ${tv.tos}]`,
  230. `[Blk: ${tv.blocks}]`
  231. ].join(` `))
  232. } else if (turkerview === true) {
  233. reviewsTemplate.push(`TV: No Reviews https://turkerview.com/requesters/${hit.requester_id}`)
  234. }
  235.  
  236. if (requesterReview.turkopticon !== undefined) {
  237. const to = requesterReview.turkopticon
  238. const toAttrs = to.attrs
  239.  
  240. reviewsTemplate.push([
  241. `**[TO](https://turkopticon.ucsd.edu/${hit.requester_id}):**`,
  242. `[Pay: ${toAttrs.pay}]`,
  243. `[Fast: ${toAttrs.fast}]`,
  244. `[Comm: ${toAttrs.comm}]`,
  245. `[Fair: ${toAttrs.fair}]`,
  246. `[Reviews: ${to.reviews}]`,
  247. `[ToS: ${to.tos_flags}]`
  248. ].join(` `))
  249. } else if (turkopticon === true) {
  250. reviewsTemplate.push(`TO: No Reviews https://turkopticon.ucsd.edu/${hit.requester_id}`)
  251. }
  252.  
  253. if (requesterReview.turkopticon2 !== undefined) {
  254. const to2 = requesterReview.turkopticon2
  255. const to2Recent = to2.recent
  256.  
  257. reviewsTemplate.push([
  258. `**[TO2](https://turkopticon.info/requesters/${hit.requester_id}):**`,
  259. `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
  260. `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
  261. `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
  262. `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
  263. `[Rej: ${to2Recent.rejected[0]}]`,
  264. `[ToS: ${to2Recent.tos[0]}]`,
  265. `[Brk: ${to2Recent.broken[0]}]`,
  266. ``
  267. ].join(` `))
  268. } else if (turkopticon2 === true) {
  269. reviewsTemplate.push(`TO2: No Reviews https://turkopticon.info/requesters/${hit.rid}`)
  270. }
  271.  
  272. const exportTemplate = [
  273. `> **Title:** [${hit.title}](https://worker.mturk.com/projects/${hit.hit_set_id}/tasks) | [PANDA](https://worker.mturk.com/projects/${hit.hit_set_id}/tasks/accept_random)`,
  274. `**Requester:** [${hit.requester_name}](https://worker.mturk.com/requesters${hit.requester_id}/projects) [${hit.requester_id}] ([Contact](https://worker.mturk.com/contact?requesterId=${hit.requester_id}))`,
  275. reviewsTemplate.join(` \n`),
  276. `**Reward:** ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
  277. `**Duration:** ${hit.assignment_duration_in_seconds.toTimeString()}`,
  278. `**Available:** ${hit.assignable_hits_count}`,
  279. `**Description:** ${hit.description}`,
  280. `**Qualifications:** ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  281. ]
  282. .filter((item) => item !== undefined).join(` \n`)
  283.  
  284. GM_setClipboard(exportTemplate)
  285.  
  286. const notification = new window.Notification(`Markdown HIT Export has been copied to your clipboard.`)
  287. setTimeout(notification.close.bind(notification), 10000)
  288. }
  289.  
  290. async function getRequesterReview (id) {
  291. return new Promise(async (resolve) => {
  292. const getReview = (stringSite, stringURL) => {
  293. return new Promise(async (resolve) => {
  294. try {
  295. const response = await window.fetch(stringURL)
  296.  
  297. if (response.status === 200) {
  298. const json = await response.json()
  299. resolve([stringSite, json.data ? Object.assign(...json.data.map((item) => ({ [item.id]: item.attributes.aggregates }))) : json])
  300. } else {
  301. resolve()
  302. }
  303. } catch (error) {
  304. resolve()
  305. }
  306. })
  307. }
  308.  
  309. const promises = []
  310.  
  311. if (turkerview === true) {
  312. promises.push(getReview(`turkerview`, `https://turkerview.com/api/v1/requesters/?ids=${id}`))
  313. }
  314. if (turkopticon === true) {
  315. promises.push(getReview(`turkopticon`, `https://turkopticon.ucsd.edu/api/multi-attrs.php?ids=${id}`))
  316. }
  317. if (turkopticon2 === true) {
  318. promises.push(getReview(`turkopticon2`, `https://api.turkopticon.info/requesters?rids=${id}&fields[requesters]=aggregates`))
  319. }
  320.  
  321. const getReviewAll = await Promise.all(promises)
  322.  
  323. const objectReview = {}
  324.  
  325. for (const item of getReviewAll) {
  326. if (item && item.length > 0) {
  327. const site = item[0]
  328. const reviews = item[1]
  329.  
  330. for (const key in reviews) {
  331. objectReview[site] = reviews[key]
  332. }
  333. }
  334. }
  335. resolve(objectReview)
  336. })
  337. }
  338.  
  339. (function () {
  340. const react = document.querySelector(`div[data-react-class="require('reactComponents/hitSetTable/HitSetTable')['default']"]`) ||
  341. document.querySelector(`div[data-react-class="require('reactComponents/taskQueueTable/TaskQueueTable')['default']"]`)
  342.  
  343. if (react) {
  344. const hitExportButton = (text, callback) => {
  345. const div = document.createElement(`div`)
  346. div.className = `col-xs-6`
  347.  
  348. const button = document.createElement(`button`)
  349. button.className = `btn btn-primary btn-hit-export`
  350. button.textContent = text
  351. button.style.width = `100%`
  352. button.addEventListener(`click`, callback)
  353. div.appendChild(button)
  354.  
  355. return div
  356. }
  357.  
  358. const modal = document.createElement(`div`)
  359. modal.className = `modal`
  360. modal.id = `hitExportModal`
  361. document.body.appendChild(modal)
  362.  
  363. const modalDialog = document.createElement(`div`)
  364. modalDialog.className = `modal-dialog`
  365. modal.appendChild(modalDialog)
  366.  
  367. const modalContent = document.createElement(`div`)
  368. modalContent.className = `modal-content`
  369. modalDialog.appendChild(modalContent)
  370.  
  371. const modalHeader = document.createElement(`div`)
  372. modalHeader.className = `modal-header`
  373. modalContent.appendChild(modalHeader)
  374.  
  375. // modal close here
  376.  
  377. const modalTitle = document.createElement(`h2`)
  378. modalTitle.className = `modal-title`
  379. modalTitle.textContent = `HIT Export`
  380. modalHeader.appendChild(modalTitle)
  381.  
  382. const modalBody = document.createElement(`div`)
  383. modalBody.className = `modal-body`
  384. modalContent.appendChild(modalBody)
  385.  
  386. const modalBodyRow1 = document.createElement(`div`)
  387. modalBodyRow1.className = `row`
  388. modalBody.appendChild(modalBodyRow1)
  389. modalBodyRow1.appendChild(hitExportButton(`Short`, short))
  390. modalBodyRow1.appendChild(hitExportButton(`Plain`, plain))
  391.  
  392. const modalBodyRow2 = document.createElement(`div`)
  393. modalBodyRow2.className = `row`
  394. modalBody.appendChild(modalBodyRow2)
  395. modalBodyRow2.appendChild(hitExportButton(`BBCode`, bbcode))
  396. modalBodyRow2.appendChild(hitExportButton(`Markdown`, markdown))
  397.  
  398. const style = document.createElement(`style`)
  399. style.innerHTML = `.modal-backdrop.in { z-index: 1049; }`
  400. document.head.appendChild(style)
  401.  
  402. const json = JSON.parse(react.dataset.reactProps).bodyData
  403. const hitRows = react.getElementsByClassName(`table-row`)
  404.  
  405. for (let i = 0; i < hitRows.length; i++) {
  406. const hit = json[i].project ? json[i].project : json[i]
  407. const project = hitRows[i].getElementsByClassName(`project-name-column`)[0]
  408.  
  409. const button = document.createElement(`button`)
  410. button.className = `btn btn-primary btn-sm`
  411. button.textContent = `Export`
  412. button.style.marginRight = `5px`
  413. project.prepend(button)
  414.  
  415. if (hitExports === `all`) {
  416. button.dataset.toggle = `modal`
  417. button.dataset.target = `#hitExportModal`
  418. button.addEventListener(`click`, (event) => {
  419. event.target.closest(`.desktop-row`).click()
  420.  
  421. for (const element of document.getElementsByClassName(`btn-hit-export`)) {
  422. element.dataset.hit = JSON.stringify(hit)
  423. }
  424. })
  425. } else {
  426. button.addEventListener(`click`, (event) => {
  427. event.target.closest(`.desktop-row`).click()
  428.  
  429. if (hitExports === `short`) {
  430. short(event, hit)
  431. } else if (hitExports === `plain`) {
  432. plain(event, hit)
  433. } else if (hitExports === `bbcode`) {
  434. bbcode(event, hit)
  435. } else if (hitExports === `markdown`) {
  436. markdown(event, hit)
  437. }
  438. })
  439. }
  440. }
  441. }
  442. })()
  443.  
  444. Object.assign(Number.prototype, {
  445. toMoneyString () {
  446. return `${this.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`
  447. },
  448. toTimeString () {
  449. let day
  450. let hour
  451. let minute
  452. let seconds = this
  453. minute = Math.floor(seconds / 60)
  454. seconds = seconds % 60
  455. hour = Math.floor(minute / 60)
  456. minute = minute % 60
  457. day = Math.floor(hour / 24)
  458. hour = hour % 24
  459.  
  460. let string = ``
  461.  
  462. if (day > 0) {
  463. string += `${day} day${day > 1 ? `s` : ``} `
  464. }
  465. if (hour > 0) {
  466. string += `${hour} hour${hour > 1 ? `s` : ``} `
  467. }
  468. if (minute > 0) {
  469. string += `${minute} day${minute > 1 ? `s` : ``}`
  470. }
  471. return string.trim()
  472. }
  473. })