Greasy Fork is available in English.

[MTurk Worker] HIT Exporter

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

// ==UserScript==
// @name         [MTurk Worker] HIT Exporter
// @namespace    https://github.com/Kadauchi
// @version      1.0.3
// @description  Allows you to export HITs as formatted text with short, plain, bbcode or markdown styling.
// @author       Kadauchi
// @icon         http://i.imgur.com/oGRQwPN.png
// @include      https://worker.mturk.com/*
// @grant        GM_setClipboard
// ==/UserScript==

/* globals GM_setClipboard */

const hitExports = `all` // Valid options are: `all`, `short`, `plain`, `bbcode` or `markdown`
const turkerview = true // Use turkerview in HIT exports
const turkopticon = true // Use turkopticon in HIT exports
const turkopticon2 = true // Use turkopticon2 in HIT exports

async function short (event, object) {
  window.alert(`Short exports are not supported yet`)
}

async function plain (event, object) {
  const hit = object || JSON.parse(event.target.dataset.hit)
  const requesterReview = await getRequesterReview(hit.requester_id)
  const reviewsTemplate = []

  if (requesterReview.turkerview !== undefined) {
    const tv = requesterReview.turkerview
    const tvRatings = tv.ratings

    reviewsTemplate.push([
      `TV:`,
      `[Hrly: $${tvRatings.hourly}]`,
      `[Pay: ${tvRatings.pay}]`,
      `[Fast: ${tvRatings.fast}]`,
      `[Comm: ${tvRatings.comm}]`,
      `[Rej: ${tv.rejections}]`,
      `[ToS: ${tv.tos}]`,
      `[Blk: ${tv.blocks}]`,
      `• https://turkerview.com/requesters/${hit.requester_id}`

    ].join(` `))
  } else if (turkerview === true) {
    reviewsTemplate.push(`TV: No Reviews • https://turkerview.com/requesters/${hit.requester_id}`)
  }

  if (requesterReview.turkopticon !== undefined) {
    const to = requesterReview.turkopticon
    const toAttrs = to.attrs

    reviewsTemplate.push([
      `TO:`,
      `[Pay: ${toAttrs.pay}]`,
      `[Fast: ${toAttrs.fast}]`,
      `[Comm: ${toAttrs.comm}]`,
      `[Fair: ${toAttrs.fair}]`,
      `[Reviews: ${to.reviews}]`,
      `[ToS: ${to.tos_flags}]`,
      `• https://turkopticon.ucsd.edu/${hit.requester_id}`
    ].join(` `))
  } else if (turkopticon === true) {
    reviewsTemplate.push(`TO: No Reviews • https://turkopticon.ucsd.edu/${hit.requester_id}`)
  }

  if (requesterReview.turkopticon2 !== undefined) {
    const to2 = requesterReview.turkopticon2
    const to2Recent = to2.recent

    reviewsTemplate.push([
      `TO2:`,
      `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
      `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
      `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
      `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
      `[Rej: ${to2Recent.rejected[0]}]`,
      `[ToS: ${to2Recent.tos[0]}]`,
      `[Brk: ${to2Recent.broken[0]}]`,
      `https://turkopticon.info/requesters/${hit.requester_id}`
    ].join(` `))
  } else if (turkopticon2 === true) {
    reviewsTemplate.push(`TO2: No Reviews • https://turkopticon.info/requesters/${hit.rid}`)
  }

  const exportTemplate = [
    `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`,
    `Requester: ${hit.requester_name} • https://worker.mturk.com/requesters${hit.requester_id}/projects`,
    reviewsTemplate.join(`\n`),
    `Reward: ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
    `Duration: ${hit.assignment_duration_in_seconds.toTimeString()}`,
    `Available: ${hit.assignable_hits_count}`,
    `Description: ${hit.description}`,
    `Requirements: ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  ].filter((item) => item !== undefined).join(`\n`)

  GM_setClipboard(exportTemplate)

  const notification = new window.Notification(`Plain HIT Export has been copied to your clipboard.`)
  setTimeout(notification.close.bind(notification), 10000)
}

async function bbcode (event, object) {
  const hit = object || JSON.parse(event.target.dataset.hit)
  const requesterReview = await getRequesterReview(hit.requester_id)
  const reviewsTemplate = []

  const ratingColor = (rating) => {
    if (rating > 3.99) {
      return `[color=#00cc00]${rating}[/color]`
    } else if (rating > 2.99) {
      return `[color=#cccc00]${rating}[/color]`
    } else if (rating > 1.99) {
      return `[color=#cc6600]${rating}[/color]`
    } else if (rating > 0.00) {
      return `[color=#cc0000]${rating}[/color]`
    }
    return rating
  }

  const percentColor = (rating) => {
    if (rating[1] > 0) {
      const percent = Math.round(rating[0] / rating[1] * 100)

      if (percent > 79) {
        return `[color=#00cc00]${percent}%[/color] of ${rating[1]}`
      } else if (percent > 59) {
        return `[color=#cccc00]${percent}%[/color] of ${rating[1]}`
      } else if (percent > 39) {
        return `[color=#cc6600]${percent}%[/color] of ${rating[1]}`
      }
      return `[color=#cc0000]${percent}%[/color] of ${rating[1]}`
    }
    return `---`
  }

  const goodBadColor = (rating) => {
    return `[color=${rating === 0 ? `#00cc00` : `#cc0000`}]${rating}[/color]`
  }

  if (requesterReview.turkerview !== undefined) {
    const tv = requesterReview.turkerview

    reviewsTemplate.push([
      `[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:`,
      `[Hrly: $${tv.ratings.hourly}]`,
      `[Pay: ${ratingColor(tv.ratings.pay)}]`,
      `[Fast: ${ratingColor(tv.ratings.fast)}]`,
      `[Comm: ${ratingColor(tv.ratings.comm)}]`,
      `[Rej: ${goodBadColor(tv.rejections)}]`,
      `[ToS: ${goodBadColor(tv.tos)}]`,
      `[Blk: ${goodBadColor(tv.blocks)}][/b]`
    ].join(` `))
  } else if (turkerview === true) {
    reviewsTemplate.push(`[b][url=https://turkerview.com/requesters/${hit.requester_id}]TV[/url]:[/b] No Reviews`)
  }

  if (requesterReview.turkopticon !== undefined) {
    const to = requesterReview.turkopticon
    const toAttrs = to.attrs

    if (toAttrs) {
      reviewsTemplate.push([
        `[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:`,
        `[Pay: ${ratingColor(toAttrs.pay)}]`,
        `[Fast: ${ratingColor(toAttrs.fast)}]`,
        `[Comm: ${ratingColor(toAttrs.comm)}]`,
        `[Fair: ${ratingColor(toAttrs.fair)}]`,
        `[Reviews: ${to.reviews}]`,
        `[ToS: ${goodBadColor(to.tos_flags)}][/b]`
      ].join(` `))
    } else {
      reviewsTemplate.push(`[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:[/b] No Reviews`)
    }
  } else if (turkopticon === true) {
    reviewsTemplate.push(`[b][url=https://turkopticon.ucsd.edu/${hit.requester_id}]TO[/url]:[/b] No Reviews`)
  }

  if (requesterReview.turkopticon2 !== undefined) {
    const to2 = requesterReview.turkopticon2
    const to2Recent = to2.recent

    reviewsTemplate.push([
      `[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:`,
      `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
      `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
      `[Res: ${percentColor(to2Recent.comm)}]`,
      `[Rec: ${percentColor(to2Recent.recommend)}]`,
      `[Rej: ${goodBadColor(to2Recent.rejected[0])}]`,
      `[ToS: ${goodBadColor(to2Recent.tos[0])}]`,
      `[Brk: ${goodBadColor(to2Recent.broken[0])}][/b]`
    ].join(` `))
  } else if (turkopticon2 === true) {
    reviewsTemplate.push(`[b][url=https://turkopticon.info/requesters/${hit.requester_id}]TO2[/url]:[/b] No Reviews`)
  }

  const exportTemplate = [
    `[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]`,
    `[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])`,
    reviewsTemplate.join(`\n`),
    `[b]Reward:[/b] ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
    `[b]Duration:[/b] ${hit.assignment_duration_in_seconds.toTimeString()}`,
    `[b]Available:[/b] ${hit.assignable_hits_count}`,
    `[b]Description:[/b] ${hit.description}`,
    `[b]Requirements:[/b] ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  ].filter((item) => item !== undefined).join(`\n`)

  GM_setClipboard(`[table][tr][td]${exportTemplate}[/td][/tr][/table]`)

  const notification = new window.Notification(`BBCode HIT Export has been copied to your clipboard.`)
  setTimeout(notification.close.bind(notification), 10000)
}

async function markdown (event, object) {
  const hit = object || JSON.parse(event.target.dataset.hit)
  const requesterReview = await getRequesterReview(hit.requester_id)
  const reviewsTemplate = []

  if (requesterReview.turkerview !== undefined) {
    const tv = requesterReview.turkerview
    const tvRatings = tv.ratings

    reviewsTemplate.push([
      `**[TV](https://turkerview.com/requesters/${hit.requester_id}):**`,
      `[Hrly: $${tvRatings.hourly}]`,
      `[Pay: ${tvRatings.pay}]`,
      `[Fast: ${tvRatings.fast}]`,
      `[Comm: ${tvRatings.comm}]`,
      `[Rej: ${tv.rejections}]`,
      `[ToS: ${tv.tos}]`,
      `[Blk: ${tv.blocks}]`
    ].join(` `))
  } else if (turkerview === true) {
    reviewsTemplate.push(`TV: No Reviews • https://turkerview.com/requesters/${hit.requester_id}`)
  }

  if (requesterReview.turkopticon !== undefined) {
    const to = requesterReview.turkopticon
    const toAttrs = to.attrs

    reviewsTemplate.push([
      `**[TO](https://turkopticon.ucsd.edu/${hit.requester_id}):**`,
      `[Pay: ${toAttrs.pay}]`,
      `[Fast: ${toAttrs.fast}]`,
      `[Comm: ${toAttrs.comm}]`,
      `[Fair: ${toAttrs.fair}]`,
      `[Reviews: ${to.reviews}]`,
      `[ToS: ${to.tos_flags}]`
    ].join(` `))
  } else if (turkopticon === true) {
    reviewsTemplate.push(`TO: No Reviews • https://turkopticon.ucsd.edu/${hit.requester_id}`)
  }

  if (requesterReview.turkopticon2 !== undefined) {
    const to2 = requesterReview.turkopticon2
    const to2Recent = to2.recent

    reviewsTemplate.push([
      `**[TO2](https://turkopticon.info/requesters/${hit.requester_id}):**`,
      `[Hrly: ${to2Recent.reward[1] > 0 ? `${(to2Recent.reward[0] / to2Recent.reward[1] * 3600).toMoneyString()}` : `---`}]`,
      `[Pen: ${to2Recent.pending > 0 ? `${(to2Recent.pending / 86400).toFixed(2)} days` : `---`}]`,
      `[Res: ${to2Recent.comm[1] > 0 ? `${Math.round(to2Recent.comm[0] / to2Recent.comm[1] * 100)}% of ${to2Recent.comm[1]}` : `---`}]`,
      `[Rec: ${to2Recent.recommend[1] > 0 ? `${Math.round(to2Recent.recommend[0] / to2Recent.recommend[1] * 100)}% of ${to2Recent.recommend[1]}` : `---`}]`,
      `[Rej: ${to2Recent.rejected[0]}]`,
      `[ToS: ${to2Recent.tos[0]}]`,
      `[Brk: ${to2Recent.broken[0]}]`,
      ``
    ].join(` `))
  } else if (turkopticon2 === true) {
    reviewsTemplate.push(`TO2: No Reviews • https://turkopticon.info/requesters/${hit.rid}`)
  }

  const exportTemplate = [
    `> **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)`,
    `**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}))`,
    reviewsTemplate.join(`  \n`),
    `**Reward:** ${hit.monetary_reward.amount_in_dollars.toMoneyString()}`,
    `**Duration:** ${hit.assignment_duration_in_seconds.toTimeString()}`,
    `**Available:** ${hit.assignable_hits_count}`,
    `**Description:** ${hit.description}`,
    `**Requirements:** ${hit.project_requirements.map(o => `${o.qualification_type.name} ${o.comparator} ${o.qualification_values.map(v => v).join(`, `)}`.trim()).join(`; `)}`
  ]
    .filter((item) => item !== undefined).join(`  \n`)

  GM_setClipboard(exportTemplate)

  const notification = new window.Notification(`Markdown HIT Export has been copied to your clipboard.`)
  setTimeout(notification.close.bind(notification), 10000)
}

async function getRequesterReview (id) {
  return new Promise(async (resolve) => {
    const getReview = (stringSite, stringURL) => {
      return new Promise(async (resolve) => {
        try {
          const response = await window.fetch(stringURL)

          if (response.status === 200) {
            const json = await response.json()
            resolve([stringSite, json.data ? Object.assign(...json.data.map((item) => ({ [item.id]: item.attributes.aggregates }))) : json])
          } else {
            resolve()
          }
        } catch (error) {
          resolve()
        }
      })
    }

    const promises = []

    if (turkerview === true) {
      promises.push(getReview(`turkerview`, `https://turkerview.com/api/v1/requesters/?ids=${id}`))
    }
    if (turkopticon === true) {
      promises.push(getReview(`turkopticon`, `https://turkopticon.ucsd.edu/api/multi-attrs.php?ids=${id}`))
    }
    if (turkopticon2 === true) {
      promises.push(getReview(`turkopticon2`, `https://api.turkopticon.info/requesters?rids=${id}&fields[requesters]=aggregates`))
    }

    const getReviewAll = await Promise.all(promises)

    const objectReview = {}

    for (const item of getReviewAll) {
      if (item && item.length > 0) {
        const site = item[0]
        const reviews = item[1]

        for (const key in reviews) {
          objectReview[site] = reviews[key]
        }
      }
    }
    resolve(objectReview)
  })
}

(function () {
  const react = document.querySelector(`div[data-react-class="require('reactComponents/hitSetTable/HitSetTable')['default']"]`) ||
          document.querySelector(`div[data-react-class="require('reactComponents/taskQueueTable/TaskQueueTable')['default']"]`)

  if (react) {
    const hitExportButton = (text, callback) => {
      const div = document.createElement(`div`)
      div.className = `col-xs-6`

      const button = document.createElement(`button`)
      button.className = `btn btn-primary btn-hit-export`
      button.textContent = text
      button.style.width = `100%`
      button.addEventListener(`click`, callback)
      div.appendChild(button)

      return div
    }

    const modal = document.createElement(`div`)
    modal.className = `modal`
    modal.id = `hitExportModal`
    document.body.appendChild(modal)

    const modalDialog = document.createElement(`div`)
    modalDialog.className = `modal-dialog`
    modal.appendChild(modalDialog)

    const modalContent = document.createElement(`div`)
    modalContent.className = `modal-content`
    modalDialog.appendChild(modalContent)

    const modalHeader = document.createElement(`div`)
    modalHeader.className = `modal-header`
    modalContent.appendChild(modalHeader)

        // modal close here

    const modalTitle = document.createElement(`h2`)
    modalTitle.className = `modal-title`
    modalTitle.textContent = `HIT Export`
    modalHeader.appendChild(modalTitle)

    const modalBody = document.createElement(`div`)
    modalBody.className = `modal-body`
    modalContent.appendChild(modalBody)

    const modalBodyRow1 = document.createElement(`div`)
    modalBodyRow1.className = `row`
    modalBody.appendChild(modalBodyRow1)
    modalBodyRow1.appendChild(hitExportButton(`Short`, short))
    modalBodyRow1.appendChild(hitExportButton(`Plain`, plain))

    const modalBodyRow2 = document.createElement(`div`)
    modalBodyRow2.className = `row`
    modalBody.appendChild(modalBodyRow2)
    modalBodyRow2.appendChild(hitExportButton(`BBCode`, bbcode))
    modalBodyRow2.appendChild(hitExportButton(`Markdown`, markdown))

    const style = document.createElement(`style`)
    style.innerHTML = `.modal-backdrop.in { z-index: 1049; }`
    document.head.appendChild(style)

    const json = JSON.parse(react.dataset.reactProps).bodyData
    const hitRows = react.getElementsByClassName(`table-row`)

    for (let i = 0; i < hitRows.length; i++) {
      const hit = json[i].project ? json[i].project : json[i]
      const project = hitRows[i].getElementsByClassName(`project-name-column`)[0]

      const button = document.createElement(`button`)
      button.className = `btn btn-primary btn-sm`
      button.textContent = `Export`
      button.style.marginRight = `5px`
      project.prepend(button)

      if (hitExports === `all`) {
        button.dataset.toggle = `modal`
        button.dataset.target = `#hitExportModal`
        button.addEventListener(`click`, (event) => {
          event.target.closest(`.desktop-row`).click()

          for (const element of document.getElementsByClassName(`btn-hit-export`)) {
            element.dataset.hit = JSON.stringify(hit)
          }
        })
      } else {
        button.addEventListener(`click`, (event) => {
          event.target.closest(`.desktop-row`).click()

          if (hitExports === `short`) {
            short(event, hit)
          } else if (hitExports === `plain`) {
            plain(event, hit)
          } else if (hitExports === `bbcode`) {
            bbcode(event, hit)
          } else if (hitExports === `markdown`) {
            markdown(event, hit)
          }
        })
      }
    }
  }
})()

Object.assign(Number.prototype, {
  toMoneyString () {
    return `$${this.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`
  },
  toTimeString () {
    let day
    let hour
    let minute
    let seconds = this
    minute = Math.floor(seconds / 60)
    seconds = seconds % 60
    hour = Math.floor(minute / 60)
    minute = minute % 60
    day = Math.floor(hour / 24)
    hour = hour % 24

    let string = ``

    if (day > 0) {
      string += `${day} day${day > 1 ? `s` : ``} `
    }
    if (hour > 0) {
      string += `${hour} hour${hour > 1 ? `s` : ``} `
    }
    if (minute > 0) {
      string += `${minute} day${minute > 1 ? `s` : ``}`
    }
    return string.trim()
  }
})