Wanikani Forums: POLL helper

Adds an easy way to create new POLLs

// ==UserScript==
// @name         Wanikani Forums: POLL helper
// @namespace    http://tampermonkey.net/
// @version      0.3.2
// @description  Adds an easy way to create new POLLs
// @author       latepotato
// @include      https://community.wanikani.com/*
// @grant        GM.xmlHttpRequest
// ==/UserScript==
// This is mostly a modified Version of Kumirei's Bottled WaniMekani script. A lot of the credit goes to them! This script is supposed to be
// an easy way to create new POLLs without needing to use the newly changed UI

;(function () {
    let rng_timestamp
    // Wait until the save function is defined
    const i = setInterval(tryInject, 100)

    // Inject if the save function is defined
    function tryInject() {
        const old_save = unsafeWindow.require('discourse/controllers/composer').default.prototype.save
        if (old_save) {
            clearInterval(i)
            inject(old_save)
        }
    }

    // Wrap the save function with our own function
    function inject(old_save) {
        const new_save = async function (t) {
            const composer = document.querySelector('textarea.d-editor-input') // Reply box
            composer.value += await commune(composer) // Modify message
            composer.value = await delete_commands(composer.value) // Deletes the lines with !poll commands
            composer.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })) // Let Discourse know
            old_save.call(this, t) // Call regular save function
        }
        unsafeWindow.require('discourse/controllers/composer').default.prototype.save = new_save // Inject
    }

    // Grabs the text then returns the POLL
    async function commune(composer) {
        // Get draft text, without quotes
        const text = composer.value.replace(/\[quote((?!\[\/quote\]).)*\[\/quote\]/gis, '')
        // Don't do anything if results are already present
        if (text.match(/(<!-- START ANSWERS -->|<wmki>)/i)) return ''
        // Get responses
        const responses = await get_responses(text)

        // If no commands were found, don't modify the post
        if (responses === '') return ''
        // If commands were found, append a reply
        return (
            '\n\n<!-- START ANSWERS -->\n\n' +
            `${responses}\n\n` +
            '<!-- END ANSWERS -->\n'
            // </p> and </blockquote> omitted because the Discourse parser wants to put them in a code block
        )
    }

    // Create responses to the commands
    async function get_responses(text) {
        // Get stored data
        const cache = get_local()
        if (cache.off && !text.match(/!poll\s/i)) return ''
        // Extract the commands
        // Each command is formatted as [whole line, !poll, word1, word2, ...]
        let regx = new RegExp('!poll[^\n]+', 'gi')
        let commands = text.match(regx)?.map((c) => [c, ...c.replace(/\s+/g, ' ').split(' ')]) || []
        // Process commands
        let results = []
        for (let i=0; i<commands.length; i++) {
            let command = commands[i]
            let listing = ''
            let word = command[2].toLowerCase()
            switch (word) {
                case '!help':
                case '!h':
                case '-help':
                case '-h': listing = quote(poll_help())
                           break
                case '!l':
                case '!link': listing = quote(`You can get the latest version of POLL helper by clicking [here]\(https://greasyfork.org/en/scripts/424969-wanikani-forums-poll-helper\)`)
                              break
                case '!trilla': listing = `https://youtu.be/Qw5Vqg7Hq60?t=15\n`
                                break;
                case 'help': listing = quote(`You have entered [i]help[/i] as a POLL option. If you want to find out how to use the POLL `
                                       + `helper, try out [i]!poll -help[/i] or [i]!poll !help[/i] instead\n\n`)
                default: listing += poll(command[0],i)
            }
            //listing = poll(command[0], i)
            if (listing) {
                results.push(listing)
            }
        }
        set_local(cache)
        return results.join('\n\n')
    }

    // Deletes all lines that start with a !poll command from the text by first splitting the String into lines and filtering out command lines
    async function delete_commands(text) {
        return text.split('\n')
            .filter(function(line) {
                return line.substring(0,6).toLowerCase() != '!poll '
            }).join('\n')
    }

    // Provides info on how to poll
    function poll_help() {
        const config_list = [
            `title="<phrase>": Puts a title on your poll`,
            `multi / number: Make the poll multiple choice or number type poll. Omit for single choice`,
            `onvote / onclose: Decide when to show results, either after voting or after the poll closes. Omit to always show`,
            `min<number>: The minimum number of options to choose in a !multi, or the lowest number in a !number poll. Omit for min 1`,
            `max<number>: Same as min, but default is the number of poll options you specified`,
            `step<number>: The step between numbers in a number poll. Omit for step 1`,
            `pie: Make the chart a pie chart. Omit for bar chart`,
            `private: Don't show who voted. Omit for public votes`,
            `close<number>: Close the poll after a number of hours. Omit to never close`,
            `c: Adds a final POLL option that contains Coelacanth`
        ]
        const response =
            `Hi, thank you for using POLL helper!\n\n` +
            `With this tool, you're able to easily create POLLs without the need for clicking through menues.\n\n` +
            `To create a POLL, simply type \`!poll\` followed by the voting options. ` +
            `If an option contains multiple words, make sure to put quotation marks at the beginning and the end of the option.\n\n` +
            `Using the following commands prefixed by ! will change the configuration of the POLL:\n` +
            `\`\`\`http\n${config_list.join('\n')}\n\`\`\`\n` +
            `A sample command might look like \`!poll !multi "Let's start" "POLLing!" !c\`\n\n` +
            `If you write a command in a new line, it will automatically get deleted before posting.\n\n` +
            `By the way, if you're interested but don't have the POLLhelper script yet, check it out [here]\(https://greasyfork.org/en/scripts/424969-wanikani-forums-poll-helper\)!`
        return response
    }

    // Creates a poll from nothing
    // id is needed, so multiple POLLs can be created at the same time
    function poll(line, id) {
        // Remove !poll command
        line = line.replace(/!poll\s+/i, '')
        // Find optional configs
        const config = {
            title: line.match(/!title=["“„«]([^"””»\n]+)["””»]/i)?.[1] || '',
            type: (line.match(/!(multi|number)/i)?.[1] || 'regular').replace(/multi/, 'multiple'),
            result: (line.match(/!(onvote|onclose)/i)?.[1] || 'always').replace(/on/, 'on_'),
            min: line.match(/!min(\d+)/i)?.[1] || 1,
            step: line.match(/!step(\d+)/i)?.[1] || 1,
            chart: !!line.match(/!pie/i) ? 'pie' : 'bar',
            public: !line.match(/!private/i),
            hours: line.match(/!close(\d+)/i)?.[1] || 0,
            coelacanth: line.match(/!c(?!lose)/i) ? 1 : 0
        }
        if (config.close) config.close = new Date(Date.now() + Number(config.hours) * 60 * 60 * 1000).toISOString()
        // Find poll options
        const options_line = line.replace(/!\w+(=["“„«]([^"””»\n]+)["””»])?/gi, '') // Remove configs
        const options =
            options_line
                .match(/(["“„«][^"””»\n]+["””»])|(\S+)/g)
                ?.map((o) => `* ${o.replace(/["“”„”«»]/g, '')}`)
                ?.slice(0, 20) || [] // Match options, max 20
        config.max = line.match(/!max(\d+)/i)?.[1] || options.length + config.coelacanth || 10

        // Build poll
        return (
            `[poll name=MekaniPOLL-${Date.now()}-`+ id + ` type=${config.type} results=${config.result} ` +
            `min=${config.min} max=${config.max} step=${config.step} chartType=${config.chart} ` +
            `public=${config.public} ` +
            (config.close ? `close=${config.close}` : '') +
            `]\n` +
            (config.title ? `# ${config.title}\n` : '') +
            (config.type == 'number' ? '' : options.join('\n')) +
            (options.length < 20 && config.coelacanth==1 ? `\n* Coelacanth` : '') +
            `\n[/poll]`
        )
    }

    // Puts text in a quote by POLL helper
    function quote(text) {
        return `[quote=\"POLLhelper\"]\n${text}\n[/quote]\n`
    }

    // Fetch local storage cache
    function get_local() {
        return JSON.parse(localStorage.getItem('WMKI') || '{ "reminders": [] }')
    }

    // Saves to local storage
    function set_local(cache) {
        localStorage.setItem('WMKI', JSON.stringify(cache))
    }


})()