Add full support for polls, both m2d and d2m.
Mostly works, but a few edge-cases still need to be worked out. Co-authored-by: Cadence Ember <cadence@disroot.org>
This commit is contained in:
committed by
Cadence Ember
co-authored by
Cadence Ember
parent
afca4de6b6
commit
90606d9176
@@ -1,6 +1,28 @@
|
||||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {sync, db, discord, select, from} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../actions/setup-emojis")} */
|
||||
const setupEmojis = sync.require("../actions/setup-emojis")
|
||||
|
||||
/**
|
||||
* @param {{count: number}[]} topAnswers
|
||||
* @param {number} count
|
||||
* @returns {string}
|
||||
*/
|
||||
function getMedal(topAnswers, count) {
|
||||
const winningOrTied = count && topAnswers[0].count === count
|
||||
const secondOrTied = !winningOrTied && count && topAnswers[1]?.count === count && topAnswers.slice(-1)[0].count !== count
|
||||
const thirdOrTied = !winningOrTied && !secondOrTied && count && topAnswers[2]?.count === count && topAnswers.slice(-1)[0].count !== count
|
||||
const medal =
|
||||
( winningOrTied ? "🥇"
|
||||
: secondOrTied ? "🥈"
|
||||
: thirdOrTied ? "🥉"
|
||||
: "")
|
||||
return medal
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isClosed
|
||||
@@ -11,20 +33,20 @@ function optionsToComponents(isClosed, pollOptions) {
|
||||
const topAnswers = pollOptions.toSorted((a, b) => b.count - a.count)
|
||||
/** @type {DiscordTypes.APIMessageTopLevelComponent[]} */
|
||||
return pollOptions.map(option => {
|
||||
const winningOrTied = option.count && topAnswers[0].count === option.count
|
||||
const medal = getMedal(topAnswers, option.count)
|
||||
return {
|
||||
type: DiscordTypes.ComponentType.Container,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: option.option_text
|
||||
content: medal && isClosed ? `${medal} ${option.option_text}` : option.option_text
|
||||
}],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: winningOrTied ? DiscordTypes.ButtonStyle.Success : DiscordTypes.ButtonStyle.Secondary,
|
||||
style: medal === "🥇" && isClosed ? DiscordTypes.ButtonStyle.Success : DiscordTypes.ButtonStyle.Secondary,
|
||||
label: option.count.toString(),
|
||||
custom_id: option.matrix_option,
|
||||
custom_id: `POLL_OPTION#${option.matrix_option}`,
|
||||
disabled: isClosed
|
||||
}
|
||||
}]
|
||||
@@ -32,6 +54,34 @@ function optionsToComponents(isClosed, pollOptions) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} maxSelections
|
||||
* @param {number} optionCount
|
||||
*/
|
||||
function getMultiSelectString(maxSelections, optionCount) {
|
||||
if (maxSelections === 1) {
|
||||
return "Select one answer"
|
||||
} else if (maxSelections >= optionCount) {
|
||||
return "Select one or more answers"
|
||||
} else {
|
||||
return `Select up to ${maxSelections} answers`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} maxSelections
|
||||
* @param {number} optionCount
|
||||
*/
|
||||
function getMultiSelectClosedString(maxSelections, optionCount) {
|
||||
if (maxSelections === 1) {
|
||||
return "Single choice"
|
||||
} else if (maxSelections >= optionCount) {
|
||||
return "Multiple choice"
|
||||
} else {
|
||||
return `Multiple choice (up to ${maxSelections})`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isClosed
|
||||
* @param {number} maxSelections
|
||||
@@ -40,39 +90,31 @@ function optionsToComponents(isClosed, pollOptions) {
|
||||
* @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody}
|
||||
*/
|
||||
function getPollComponents(isClosed, maxSelections, questionText, pollOptions) {
|
||||
/** @type {DiscordTypes.APIMessageTopLevelComponent[]} array because it can move around */
|
||||
const multiSelectInfoComponent = [{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: isClosed ? `-# ${getMultiSelectClosedString(maxSelections, pollOptions.length)}` : `-# ${getMultiSelectString(maxSelections, pollOptions.length)}`
|
||||
}]
|
||||
/** @type {DiscordTypes.APIMessageTopLevelComponent} */
|
||||
let headingComponent
|
||||
if (isClosed) {
|
||||
const multiSelectString =
|
||||
( maxSelections === 1 ? "-# ~~Select one answer~~"
|
||||
: maxSelections >= pollOptions.length ? "-# ~~Select one or more answers~~"
|
||||
: `-# ~~Select up to ${maxSelections} answers~~`)
|
||||
headingComponent = { // This one is for the poll heading.
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: `## ${questionText}`
|
||||
},
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: multiSelectString
|
||||
}
|
||||
],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: DiscordTypes.ButtonStyle.Secondary,
|
||||
custom_id: "vote",
|
||||
label: "Voting closed!",
|
||||
custom_id: "POLL_VOTE",
|
||||
label: "Voting closed",
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const multiSelectString =
|
||||
( maxSelections === 1 ? "-# Select one answer"
|
||||
: maxSelections >= pollOptions.length ? "-# Select one or more answers"
|
||||
: `-# Select up to ${maxSelections} answers`)
|
||||
} else {
|
||||
headingComponent = { // This one is for the poll heading.
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [
|
||||
@@ -80,15 +122,13 @@ function getPollComponents(isClosed, maxSelections, questionText, pollOptions) {
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: `## ${questionText}`
|
||||
},
|
||||
{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: multiSelectString
|
||||
}
|
||||
// @ts-ignore
|
||||
multiSelectInfoComponent.pop()
|
||||
],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: DiscordTypes.ButtonStyle.Primary,
|
||||
custom_id: "vote",
|
||||
custom_id: "POLL_VOTE",
|
||||
label: "Vote!"
|
||||
}
|
||||
}
|
||||
@@ -96,8 +136,92 @@ function getPollComponents(isClosed, maxSelections, questionText, pollOptions) {
|
||||
const optionComponents = optionsToComponents(isClosed, pollOptions)
|
||||
return {
|
||||
flags: DiscordTypes.MessageFlags.IsComponentsV2,
|
||||
components: [headingComponent, ...optionComponents]
|
||||
components: [headingComponent, ...optionComponents, ...multiSelectInfoComponent]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getPollComponents = getPollComponents
|
||||
/** @param {string} messageID */
|
||||
function getPollComponentsFromDatabase(messageID) {
|
||||
const pollRow = select("poll", ["max_selections", "is_closed", "question_text"], {message_id: messageID}).get()
|
||||
assert(pollRow)
|
||||
/** @type {{matrix_option: string, option_text: string, count: number}[]} */
|
||||
const pollResults = db.prepare("SELECT matrix_option, option_text, seq, count(discord_or_matrix_user_id) as count FROM poll_option LEFT JOIN poll_vote USING (message_id, matrix_option) WHERE message_id = ? GROUP BY matrix_option ORDER BY seq").all(messageID)
|
||||
return getPollComponents(!!pollRow.is_closed, pollRow.max_selections, pollRow.question_text, pollResults)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} channelID
|
||||
* @param {string} messageID
|
||||
* @param {string} questionText
|
||||
* @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
|
||||
* @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody}
|
||||
*/
|
||||
function getPollEndMessage(channelID, messageID, questionText, pollOptions) {
|
||||
const topAnswers = pollOptions.toSorted((a, b) => b.count - a.count)
|
||||
const totalVotes = pollOptions.reduce((a, c) => a + c.count, 0)
|
||||
const tied = topAnswers[0].count === topAnswers[1].count
|
||||
const titleString = `-# The poll **${questionText}** has closed.`
|
||||
let winnerString = ""
|
||||
let resultsString = ""
|
||||
if (totalVotes == 0) {
|
||||
winnerString = "There was no winner"
|
||||
} else if (tied) {
|
||||
winnerString = "It's a draw!"
|
||||
resultsString = `${Math.round((topAnswers[0].count/totalVotes)*100)}%`
|
||||
} else {
|
||||
const pollWin = select("auto_emoji", ["name", "emoji_id"], {name: "poll_win"}).get()
|
||||
winnerString = `${topAnswers[0].option_text} <:${pollWin?.name}:${pollWin?.emoji_id}>`
|
||||
resultsString = `Winning answer • ${Math.round((topAnswers[0].count/totalVotes)*100)}%`
|
||||
}
|
||||
// @ts-ignore
|
||||
const guildID = discord.channels.get(channelID).guild_id
|
||||
let mainContent = `**${winnerString}**`
|
||||
if (resultsString) {
|
||||
mainContent += `\n-# ${resultsString}`
|
||||
}
|
||||
return {
|
||||
flags: DiscordTypes.MessageFlags.IsComponentsV2,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: titleString
|
||||
}, {
|
||||
type: DiscordTypes.ComponentType.Container,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.Section,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.TextDisplay,
|
||||
content: `**${winnerString}**\n-# ${resultsString}`
|
||||
}],
|
||||
accessory: {
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
style: DiscordTypes.ButtonStyle.Link,
|
||||
url: `https://discord.com/channels/${guildID}/${channelID}/${messageID}`,
|
||||
label: "View Poll"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} channelID
|
||||
* @param {string} messageID
|
||||
*/
|
||||
async function getPollEndMessageFromDatabase(channelID, messageID) {
|
||||
const pollWin = select("auto_emoji", ["name", "emoji_id"], {name: "poll_win"}).get()
|
||||
if (!pollWin) {
|
||||
await setupEmojis.setupEmojis()
|
||||
}
|
||||
|
||||
const pollRow = select("poll", ["max_selections", "question_text"], {message_id: messageID}).get()
|
||||
assert(pollRow)
|
||||
/** @type {{matrix_option: string, option_text: string, count: number}[]} */
|
||||
const pollResults = db.prepare("SELECT matrix_option, option_text, seq, count(discord_or_matrix_user_id) as count FROM poll_option LEFT JOIN poll_vote USING (message_id, matrix_option) WHERE message_id = ? GROUP BY matrix_option ORDER BY seq").all(messageID)
|
||||
return getPollEndMessage(channelID, messageID, pollRow.question_text, pollResults)
|
||||
}
|
||||
|
||||
module.exports.getMultiSelectString = getMultiSelectString
|
||||
module.exports.getPollComponents = getPollComponents
|
||||
module.exports.getPollComponentsFromDatabase = getPollComponentsFromDatabase
|
||||
module.exports.getPollEndMessageFromDatabase = getPollEndMessageFromDatabase
|
||||
module.exports.getMedal = getMedal
|
||||
|
||||
Reference in New Issue
Block a user