Compare commits
2 commits
52d9c6fea8
...
b463e1173b
| Author | SHA1 | Date | |
|---|---|---|---|
| b463e1173b | |||
| 564d564490 |
5 changed files with 142 additions and 36 deletions
|
|
@ -9,22 +9,15 @@ const {discord, sync, db, select, from} = passthrough
|
|||
const {reg} = require("../../matrix/read-registration")
|
||||
/** @type {import("./poll-vote")} */
|
||||
const vote = sync.require("../actions/poll-vote")
|
||||
/** @type {import("../../m2d/converters/poll-components")} */
|
||||
const pollComponents = sync.require("../../m2d/converters/poll-components")
|
||||
|
||||
// This handles, in the following order:
|
||||
// * verifying Matrix-side votes are accurate for a poll originating on Discord, sending missed votes to Matrix if necessary
|
||||
// * sending a message to Discord if a vote in that poll has been cast on Matrix
|
||||
// This does *not* handle bridging of poll closures on Discord to Matrix; that takes place in converters/message-to-event.js.
|
||||
/** @type {import("../../discord/interactions/poll-responses")} */
|
||||
const pollResponses = sync.require("../../discord/interactions/poll-responses")
|
||||
|
||||
/**
|
||||
* @param {number} percent
|
||||
* @file This handles, in the following order:
|
||||
* * verifying Matrix-side votes are accurate for a poll originating on Discord, sending missed votes to Matrix if necessary
|
||||
* * sending a message to Discord if a vote in that poll has been cast on Matrix
|
||||
* This does *not* handle bridging of poll closures on Discord to Matrix; that takes place in converters/message-to-event.js.
|
||||
*/
|
||||
function barChart(percent) {
|
||||
const width = 12
|
||||
const bars = Math.floor(percent*width)
|
||||
return "█".repeat(bars) + "▒".repeat(width-bars)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} channelID
|
||||
|
|
@ -114,26 +107,14 @@ async function endPoll(closeMessage) {
|
|||
}))
|
||||
}
|
||||
|
||||
/** @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(pollMessageID)
|
||||
const combinedVotes = pollResults.reduce((a, c) => a + c.count, 0)
|
||||
const totalVoters = db.prepare("SELECT count(DISTINCT discord_or_matrix_user_id) as count FROM poll_vote WHERE message_id = ?").pluck().get(pollMessageID)
|
||||
const {combinedVotes, messageString} = pollResponses.getCombinedResults(pollMessageID, true)
|
||||
|
||||
if (combinedVotes !== totalVotes) { // This means some votes were cast on Matrix!
|
||||
// Now that we've corrected the vote totals, we can get the results again and post them to Discord!
|
||||
const topAnswers = pollResults.toSorted((a, b) => b.count - a.count)
|
||||
let messageString = ""
|
||||
for (const option of pollResults) {
|
||||
const medal = pollComponents.getMedal(topAnswers, option.count)
|
||||
const countString = `${String(option.count).padStart(String(topAnswers[0].count).length)}`
|
||||
const votesString = option.count === 1 ? "vote " : "votes"
|
||||
const label = medal === "🥇" ? `**${option.option_text}**` : option.option_text
|
||||
messageString += `\`\u200b${countString} ${votesString}\u200b\` ${barChart(option.count/totalVoters)} ${label} ${medal}\n`
|
||||
}
|
||||
if (combinedVotes !== totalVotes) { // This means some votes were cast on Matrix. Now that we've corrected the vote totals, we can get the results again and post them to Discord.
|
||||
return {
|
||||
username: "Total results including Matrix votes",
|
||||
avatar_url: `${reg.ooye.bridge_origin}/discord/poll-star-avatar.png`,
|
||||
content: messageString
|
||||
content: messageString,
|
||||
flags: DiscordTypes.MessageFlags.SuppressEmbeds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@ function eventCanBeEdited(ev) {
|
|||
if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") {
|
||||
return false
|
||||
}
|
||||
// Discord does not allow stickers to be edited. Poll closures are sent as "edits", but not in a way we care about.
|
||||
if (ev.old.event_type === "m.sticker" || ev.old.event_type === "org.matrix.msc3381.poll.start") {
|
||||
// Discord does not allow stickers to be edited.
|
||||
if (ev.old.event_type === "m.sticker") {
|
||||
return false
|
||||
}
|
||||
// Discord does not allow the data of polls to be edited, they may only be responded to.
|
||||
if (ev.old.event_type === "org.matrix.msc3381.poll.start" || ev.old.event_type === "org.matrix.msc3381.poll.end") {
|
||||
return false
|
||||
}
|
||||
// Anything else is fair game.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ const mxUtils = sync.require("../../matrix/utils")
|
|||
const dUtils = sync.require("../../discord/utils")
|
||||
/** @type {import("./find-mentions")} */
|
||||
const findMentions = sync.require("./find-mentions")
|
||||
/** @type {import("../../discord/interactions/poll-responses")} */
|
||||
const pollResponses = sync.require("../../discord/interactions/poll-responses")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
|
||||
/**
|
||||
|
|
@ -269,7 +271,21 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}
|
||||
|
||||
if (message.type === DiscordTypes.MessageType.PollResult) {
|
||||
const event_id = select("event_message", "event_id", {message_id: message.message_reference?.message_id}).pluck().get()
|
||||
const pollMessageID = message.message_reference?.message_id
|
||||
if (!pollMessageID) return []
|
||||
const event_id = select("event_message", "event_id", {message_id: pollMessageID}).pluck().get()
|
||||
const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||
const pollQuestionText = select("poll", "question_text", {message_id: pollMessageID}).pluck().get()
|
||||
if (!event_id || !roomID || !pollQuestionText) return [] // drop it if the corresponding poll start was not bridged
|
||||
|
||||
const rep = new mxUtils.MatrixStringBuilder()
|
||||
rep.addLine(`The poll ${pollQuestionText} has closed.`, tag`The poll <a href="https://matrix.to/#/${roomID}/${event_id}">${pollQuestionText}</a> has closed.`)
|
||||
|
||||
const {messageString} = pollResponses.getCombinedResults(pollMessageID, true) // poll results have already been double-checked before this point, so these totals will be accurate
|
||||
rep.addLine(markdown.toHTML(messageString, {discordOnly: true, escapeHTML: false}), markdown.toHTML(messageString, {}))
|
||||
|
||||
const {body, formatted_body} = rep.get()
|
||||
|
||||
return [{
|
||||
$type: "org.matrix.msc3381.poll.end",
|
||||
"m.relates_to": {
|
||||
|
|
@ -277,7 +293,11 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
event_id
|
||||
},
|
||||
"org.matrix.msc3381.poll.end": {},
|
||||
body: "This poll has ended.",
|
||||
"org.matrix.msc1767.text": body,
|
||||
"org.matrix.msc1767.html": formatted_body,
|
||||
body: body,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: formatted_body,
|
||||
msgtype: "m.text"
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
94
src/discord/interactions/poll-responses.js
Normal file
94
src/discord/interactions/poll-responses.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
const {id: botID} = require("../../../addbot")
|
||||
const {InteractionMethods} = require("snowtransfer")
|
||||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../../m2d/converters/poll-components")} */
|
||||
const pollComponents = sync.require("../../m2d/converters/poll-components")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
|
||||
/**
|
||||
* @param {number} percentc
|
||||
*/
|
||||
function barChart(percent) {
|
||||
const width = 12
|
||||
const bars = Math.floor(percent*width)
|
||||
return "█".repeat(bars) + "▒".repeat(width-bars)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pollMessageID
|
||||
* @param {boolean} isClosed
|
||||
*/
|
||||
function getCombinedResults(pollMessageID, isClosed) {
|
||||
/** @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(pollMessageID)
|
||||
const combinedVotes = pollResults.reduce((a, c) => a + c.count, 0)
|
||||
const totalVoters = db.prepare("SELECT count(DISTINCT discord_or_matrix_user_id) as count FROM poll_vote WHERE message_id = ?").pluck().get(pollMessageID)
|
||||
const topAnswers = pollResults.toSorted((a, b) => b.count - a.count)
|
||||
|
||||
let messageString = ""
|
||||
for (const option of pollResults) {
|
||||
const medal = isClosed ? pollComponents.getMedal(topAnswers, option.count) : ""
|
||||
const countString = `${String(option.count).padStart(String(topAnswers[0].count).length)}`
|
||||
const votesString = option.count === 1 ? "vote " : "votes"
|
||||
const label = medal === "🥇" ? `**${option.option_text}**` : option.option_text
|
||||
messageString += `\`\u200b${countString} ${votesString}\u200b\` ${barChart(option.count/totalVoters)} ${label} ${medal}\n`
|
||||
}
|
||||
|
||||
return {messageString, combinedVotes, totalVoters}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction
|
||||
* @param {{api: typeof api}} di
|
||||
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
||||
*/
|
||||
async function* _interact({data}, {api}) {
|
||||
const row = select("poll", "is_closed", {message_id: data.target_id}).get()
|
||||
|
||||
if (!row) {
|
||||
return yield {createInteractionResponse: {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "This poll hasn't been bridged to Matrix.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
const {messageString} = getCombinedResults(data.target_id, !!row.is_closed)
|
||||
|
||||
return yield {createInteractionResponse: {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
embeds: [{
|
||||
author: {
|
||||
name: "Current results including Matrix votes",
|
||||
icon_url: `${reg.ooye.bridge_origin}/discord/poll-star-avatar.png`
|
||||
},
|
||||
description: messageString
|
||||
}],
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
/* c8 ignore start */
|
||||
|
||||
/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */
|
||||
async function interact(interaction) {
|
||||
for await (const response of _interact(interaction, {api})) {
|
||||
if (response.createInteractionResponse) {
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.interact = interact
|
||||
module.exports._interact = _interact
|
||||
module.exports.getCombinedResults = getCombinedResults
|
||||
|
|
@ -10,6 +10,7 @@ const permissions = sync.require("./interactions/permissions.js")
|
|||
const reactions = sync.require("./interactions/reactions.js")
|
||||
const privacy = sync.require("./interactions/privacy.js")
|
||||
const poll = sync.require("./interactions/poll.js")
|
||||
const pollResponses = sync.require("./interactions/poll-responses.js")
|
||||
const ping = sync.require("./interactions/ping.js")
|
||||
|
||||
// User must have EVERY permission in default_member_permissions to be able to use the command
|
||||
|
|
@ -24,7 +25,7 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
|||
type: DiscordTypes.ApplicationCommandType.Message,
|
||||
default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles)
|
||||
}, {
|
||||
name: "Reactions",
|
||||
name: "Responses",
|
||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||
type: DiscordTypes.ApplicationCommandType.Message
|
||||
}, {
|
||||
|
|
@ -107,8 +108,14 @@ async function dispatchInteraction(interaction) {
|
|||
await permissions.interact(interaction)
|
||||
} else if (interactionId === "permissions_edit") {
|
||||
await permissions.interactEdit(interaction)
|
||||
} else if (interactionId === "Reactions") {
|
||||
await reactions.interact(interaction)
|
||||
} else if (interactionId === "Responses") {
|
||||
/** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
|
||||
const messageInteraction = interaction
|
||||
if (messageInteraction.data.resolved.messages[messageInteraction.data.target_id]?.poll) {
|
||||
await pollResponses.interact(messageInteraction)
|
||||
} else {
|
||||
await reactions.interact(messageInteraction)
|
||||
}
|
||||
} else if (interactionId === "ping") {
|
||||
await ping.interact(interaction)
|
||||
} else if (interactionId === "privacy") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue