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
@@ -21,7 +21,7 @@ async function addReaction(data) {
|
||||
const user = data.member?.user
|
||||
assert.ok(user && user.username)
|
||||
|
||||
const parentID = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get()
|
||||
const parentID = select("event_message", "event_id", {message_id: data.message_id}, "ORDER BY reaction_part").pluck().get()
|
||||
if (!parentID) return // Nothing can be done if the parent message was never bridged.
|
||||
assert.equal(typeof parentID, "string")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const {isDeepStrictEqual} = require("util")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {discord, sync, db, select, from} = passthrough
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("./register-user")} */
|
||||
@@ -14,6 +15,8 @@ const registerUser = sync.require("./register-user")
|
||||
const createRoom = sync.require("../actions/create-room")
|
||||
/** @type {import("./add-or-remove-vote.js")} */
|
||||
const vote = sync.require("../actions/add-or-remove-vote")
|
||||
/** @type {import("../../m2d/converters/poll-components")} */
|
||||
const pollComponents = sync.require("../../m2d/converters/poll-components")
|
||||
/** @type {import("../../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
||||
|
||||
@@ -26,8 +29,9 @@ const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
||||
* @param {number} percent
|
||||
*/
|
||||
function barChart(percent){
|
||||
let bars = Math.floor(percent*10)
|
||||
return "█".repeat(bars) + "▒".repeat(10-bars)
|
||||
const width = 12
|
||||
const bars = Math.floor(percent*width)
|
||||
return "█".repeat(bars) + "▒".repeat(width-bars)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +77,7 @@ async function closePoll(closeMessage, guild){
|
||||
// If the closure came from Discord, we want to fetch all the votes there again and bridge over any that got lost to Matrix before posting the results.
|
||||
// Database reads are cheap, and API calls are expensive, so we will only query Discord when the totals don't match.
|
||||
|
||||
const totalVotes = pollCloseObject.fields.find(element => element.name === "total_votes").value // We could do [2], but best not to rely on the ordering staying consistent.
|
||||
const totalVotes = +pollCloseObject.fields.find(element => element.name === "total_votes").value // We could do [2], but best not to rely on the ordering staying consistent.
|
||||
|
||||
const databaseVotes = select("poll_vote", ["discord_or_matrix_user_id", "matrix_option"], {message_id: pollMessageID}, " AND discord_or_matrix_user_id NOT LIKE '@%'").all()
|
||||
|
||||
@@ -120,26 +124,27 @@ async function closePoll(closeMessage, guild){
|
||||
}))
|
||||
}
|
||||
|
||||
/** @type {{discord_option: string, option_text: string, count: number}[]} */
|
||||
const pollResults = db.prepare("SELECT discord_option, option_text, count(*) as count FROM poll_option INNER JOIN poll_vote USING (message_id, matrix_option) GROUP BY discord_option").all()
|
||||
/** @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!
|
||||
const message = await discord.snow.channel.getChannelMessage(closeMessage.channel_id, pollMessageID)
|
||||
assert(message?.poll?.answers)
|
||||
// Now that we've corrected the vote totals, we can get the results again and post them to Discord!
|
||||
const topAnswers = pollResults.toSorted()
|
||||
const unique = topAnswers.length > 1 && topAnswers[0].count === topAnswers[1].count
|
||||
|
||||
let messageString = "📶 Results including Matrix votes\n"
|
||||
for (const result of pollResults) {
|
||||
if (result === topAnswers[0] && unique) {
|
||||
messageString = messageString + `${barChart(result.count/combinedVotes)} **${result.option_text}** (**${result.count}**)\n`
|
||||
} else {
|
||||
messageString = messageString + `${barChart(result.count/combinedVotes)} ${result.option_text} (${result.count})\n`
|
||||
}
|
||||
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
|
||||
}
|
||||
await channelWebhook.sendMessageWithWebhook(closeMessage.channel_id, {content: messageString}, closeMessage.thread_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ async function removeSomeReactions(data) {
|
||||
if (!row) return
|
||||
|
||||
const eventReactedTo = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
.where({message_id: data.message_id, reaction_part: 0}).select("event_id", "room_id").get()
|
||||
.where({message_id: data.message_id}).and("ORDER BY reaction_part").select("event_id", "room_id").get()
|
||||
if (!eventReactedTo) return
|
||||
|
||||
// Due to server restrictions, all relations (i.e. reactions) have to be in the same room as the original event.
|
||||
|
||||
@@ -4,7 +4,7 @@ const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const { discord, sync, db, select } = passthrough
|
||||
const { discord, sync, db, select, from} = passthrough
|
||||
/** @type {import("../converters/message-to-event")} */
|
||||
const messageToEvent = sync.require("../converters/message-to-event")
|
||||
/** @type {import("../../matrix/api")} */
|
||||
@@ -21,6 +21,8 @@ const createRoom = sync.require("../actions/create-room")
|
||||
const closePoll = sync.require("../actions/close-poll")
|
||||
/** @type {import("../../discord/utils")} */
|
||||
const dUtils = sync.require("../../discord/utils")
|
||||
/** @type {import("../../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
@@ -33,10 +35,6 @@ async function sendMessage(message, channel, guild, row) {
|
||||
const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: roomID}).pluck().get()
|
||||
assert(historicalRoomIndex)
|
||||
|
||||
if (message.type === 46) { // This is a poll_result. We might need to send a message to Discord (if there were any Matrix-side votes), regardless of if this message was sent by the bridge or not.
|
||||
await closePoll.closePoll(message, guild)
|
||||
}
|
||||
|
||||
let senderMxid = null
|
||||
if (dUtils.isWebhookMessage(message)) {
|
||||
const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guild.id}).pluck().get() ?? 0
|
||||
@@ -104,6 +102,20 @@ async function sendMessage(message, channel, guild, row) {
|
||||
})()
|
||||
}
|
||||
|
||||
if (message.type === DiscordTypes.MessageType.PollResult) { // We might need to send a message to Discord (if there were any Matrix-side votes).
|
||||
const detailedResultsMessage = await closePoll.closePoll(message, guild)
|
||||
if (detailedResultsMessage) {
|
||||
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
|
||||
const channelID = threadParent ? threadParent : message.channel_id
|
||||
const threadID = threadParent ? message.channel_id : undefined
|
||||
const sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
|
||||
db.transaction(() => {
|
||||
db.prepare("UPDATE event_message SET reaction_part = 1 WHERE event_id = ?").run(eventID)
|
||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, sentResultsMessage.id, 1, 0) // part = 1, reaction_part = 0
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
eventIDs.push(eventID)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user