diff --git a/src/d2m/actions/poll-end.js b/src/d2m/actions/poll-end.js index b0d29b9..936dedf 100644 --- a/src/d2m/actions/poll-end.js +++ b/src/d2m/actions/poll-end.js @@ -9,15 +9,22 @@ 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("../../discord/interactions/poll-responses")} */ -const pollResponses = sync.require("../../discord/interactions/poll-responses") +/** @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. /** - * @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. + * @param {number} percent */ +function barChart(percent) { + const width = 12 + const bars = Math.floor(percent*width) + return "█".repeat(bars) + "▒".repeat(width-bars) +} /** * @param {string} channelID @@ -107,14 +114,26 @@ async function endPoll(closeMessage) { })) } - const {combinedVotes, messageString} = pollResponses.getCombinedResults(pollMessageID, true) + /** @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) - 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. + 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` + } return { username: "Total results including Matrix votes", avatar_url: `${reg.ooye.bridge_origin}/discord/poll-star-avatar.png`, - content: messageString, - flags: DiscordTypes.MessageFlags.SuppressEmbeds + content: messageString } } } diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index b73d6e0..82b9417 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -16,12 +16,8 @@ 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. - 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") { + // 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") { return false } // Anything else is fair game. diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 2f12958..9236c89 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -20,8 +20,6 @@ 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") /** @@ -271,21 +269,7 @@ async function messageToEvent(message, guild, options = {}, di) { } if (message.type === DiscordTypes.MessageType.PollResult) { - 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 ${pollQuestionText} 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() - + const event_id = select("event_message", "event_id", {message_id: message.message_reference?.message_id}).pluck().get() return [{ $type: "org.matrix.msc3381.poll.end", "m.relates_to": { @@ -293,11 +277,7 @@ async function messageToEvent(message, guild, options = {}, di) { event_id }, "org.matrix.msc3381.poll.end": {}, - "org.matrix.msc1767.text": body, - "org.matrix.msc1767.html": formatted_body, - body: body, - format: "org.matrix.custom.html", - formatted_body: formatted_body, + body: "This poll has ended.", msgtype: "m.text" }] } diff --git a/src/discord/interactions/poll-responses.js b/src/discord/interactions/poll-responses.js deleted file mode 100644 index 86277c2..0000000 --- a/src/discord/interactions/poll-responses.js +++ /dev/null @@ -1,94 +0,0 @@ -// @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[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 \ No newline at end of file diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index a909393..b37f28e 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -10,7 +10,6 @@ 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 @@ -25,7 +24,7 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ type: DiscordTypes.ApplicationCommandType.Message, default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles) }, { - name: "Responses", + name: "Reactions", contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message }, { @@ -108,14 +107,8 @@ async function dispatchInteraction(interaction) { await permissions.interact(interaction) } else if (interactionId === "permissions_edit") { await permissions.interactEdit(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 === "Reactions") { + await reactions.interact(interaction) } else if (interactionId === "ping") { await ping.interact(interaction) } else if (interactionId === "privacy") {