initial polls support (not exactly working)
This commit is contained in:
committed by
Cadence Ember
parent
2496f4c3b0
commit
e565342ac8
@@ -22,8 +22,8 @@ const editMessage = sync.require("../../d2m/actions/edit-message")
|
||||
const emojiSheet = sync.require("../actions/emoji-sheet")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
||||
* @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
||||
* @param {{poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | stream.Readable})[]}} message
|
||||
* @returns {Promise<{poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]}>}
|
||||
*/
|
||||
async function resolvePendingFiles(message) {
|
||||
if (!message.pendingFiles) return message
|
||||
@@ -59,7 +59,7 @@ async function resolvePendingFiles(message) {
|
||||
return newMessage
|
||||
}
|
||||
|
||||
/** @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker} event */
|
||||
/** @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start} event */
|
||||
async function sendEvent(event) {
|
||||
const row = from("channel_room").where({room_id: event.room_id}).select("channel_id", "thread_parent").get()
|
||||
if (!row) return [] // allow the bot to exist in unbridged rooms, just don't do anything with it
|
||||
@@ -133,6 +133,12 @@ async function sendEvent(event) {
|
||||
}, guild, null)
|
||||
)
|
||||
}
|
||||
|
||||
if (message.poll){ // Need to store answer mapping in the database.
|
||||
for (let i=0; i<message.poll.answers.length; i++){
|
||||
db.prepare("INSERT INTO poll_option (message_id, matrix_option, discord_option) VALUES (?, ?, ?)").run(messageResponse.id, message.poll.answers[i].matrix_option, messageResponse.poll.answers[i].answer_id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const user of ensureJoined) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @ts-check
|
||||
|
||||
const Ty = require("../../types")
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {Readable} = require("stream")
|
||||
const assert = require("assert").strict
|
||||
const crypto = require("crypto")
|
||||
const passthrough = require("../../passthrough")
|
||||
const {sync, discord, db, select} = passthrough
|
||||
|
||||
/** @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Response} event */
|
||||
async function updateVote(event) {
|
||||
|
||||
const messageID = select("event_message", "message_id", {event_id: event.content["m.relates_to"].event_id, event_type: "org.matrix.msc3381.poll.start"}).pluck().get()
|
||||
if (!messageID) return // Nothing can be done if the parent message was never bridged.
|
||||
|
||||
db.prepare("DELETE FROM poll_vote WHERE discord_or_matrix_user_id = ? AND message_id = ?").run(event.sender, messageID) // Clear all the existing votes, since this overwrites. Technically we could check and only overwrite the changes, but the complexity isn't worth it.
|
||||
|
||||
event.content["org.matrix.msc3381.poll.response"].answers.map(answer=>{
|
||||
db.prepare("INSERT OR IGNORE INTO poll_vote (discord_or_matrix_user_id, message_id, vote) VALUES (?, ?, ?)").run(event.sender, messageID, answer)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.updateVote = updateVote
|
||||
@@ -517,7 +517,7 @@ async function getL1L2ReplyLine(called = false) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start} event
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {DiscordTypes.APIGuildTextChannel} channel
|
||||
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, mxcDownloader: (mxc: string) => Promise<Buffer | undefined>}} di simple-as-nails dependency injection for the matrix API
|
||||
@@ -544,13 +544,15 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
displayNameRunoff = ""
|
||||
}
|
||||
|
||||
let content = event.content.body // ultimate fallback
|
||||
let content = event.content["body"] || "" // ultimate fallback
|
||||
/** @type {{id: string, filename: string}[]} */
|
||||
const attachments = []
|
||||
/** @type {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */
|
||||
const pendingFiles = []
|
||||
/** @type {DiscordTypes.APIUser[]} */
|
||||
const ensureJoined = []
|
||||
/** @type {Ty.SendingPoll} */
|
||||
let poll = null
|
||||
|
||||
// Convert content depending on what the message is
|
||||
// Handle images first - might need to handle their `body`/`formatted_body` as well, which will fall through to the text processor
|
||||
@@ -628,6 +630,24 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
}
|
||||
attachments.push({id: "0", filename})
|
||||
pendingFiles.push({name: filename, mxc: event.content.url})
|
||||
|
||||
} else if (event.type === "org.matrix.msc3381.poll.start") {
|
||||
content = ""
|
||||
const pollContent = event.content["org.matrix.msc3381.poll.start"] // just for convenience
|
||||
let allowMultiselect = (pollContent.max_selections != 1)
|
||||
let answers = pollContent.answers.map(answer=>{
|
||||
return {poll_media: {text: answer["org.matrix.msc1767.text"]}, matrix_option: answer["id"]}
|
||||
})
|
||||
poll = {
|
||||
question: {
|
||||
text: event.content["org.matrix.msc3381.poll.start"].question["org.matrix.msc1767.text"]
|
||||
},
|
||||
answers: answers,
|
||||
duration: 768, // Maximum duration (32 days). Matrix doesn't allow automatically-expiring polls, so this is the only thing that makes sense to send.
|
||||
allow_multiselect: allowMultiselect,
|
||||
layout_type: 1
|
||||
}
|
||||
|
||||
} else {
|
||||
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
|
||||
// this event ---is an edit of--> original event ---is a reply to--> past event
|
||||
@@ -828,7 +848,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
|
||||
);
|
||||
const root = doc.getElementById("turndown-root");
|
||||
async function forEachNode(node) {
|
||||
async function forEachNode(event, node) {
|
||||
for (; node; node = node.nextSibling) {
|
||||
// Check written mentions
|
||||
if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) {
|
||||
@@ -876,10 +896,10 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
node.setAttribute("data-suppress", "")
|
||||
}
|
||||
}
|
||||
await forEachNode(node.firstChild)
|
||||
await forEachNode(event, node.firstChild)
|
||||
}
|
||||
}
|
||||
await forEachNode(root)
|
||||
await forEachNode(event, root)
|
||||
|
||||
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
|
||||
// First we need to determine which emojis are at the end.
|
||||
@@ -960,7 +980,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
|
||||
// Split into 2000 character chunks
|
||||
const chunks = chunk(content, 2000)
|
||||
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||
/** @type {({poll?: Ty.SendingPoll} & DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||
const messages = chunks.map(content => ({
|
||||
content,
|
||||
allowed_mentions: {
|
||||
@@ -983,6 +1003,15 @@ async function eventToMessage(event, guild, channel, di) {
|
||||
messages[0].pendingFiles = pendingFiles
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
if (!messages.length) messages.push({
|
||||
content: " ", // stopgap, remove when library updates
|
||||
username: displayNameShortened,
|
||||
avatar_url: avatarURL
|
||||
})
|
||||
messages[0].poll = poll
|
||||
}
|
||||
|
||||
const messagesToEdit = []
|
||||
const messagesToSend = []
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
|
||||
@@ -18,6 +18,8 @@ const addReaction = sync.require("./actions/add-reaction")
|
||||
const redact = sync.require("./actions/redact")
|
||||
/** @type {import("./actions/update-pins")}) */
|
||||
const updatePins = sync.require("./actions/update-pins")
|
||||
/** @type {import("./actions/vote")}) */
|
||||
const vote = sync.require("./actions/vote")
|
||||
/** @type {import("../matrix/matrix-command-handler")} */
|
||||
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
|
||||
/** @type {import("../matrix/utils")} */
|
||||
@@ -218,6 +220,25 @@ async event => {
|
||||
await api.ackEvent(event)
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.start", guard("org.matrix.msc3381.poll.start",
|
||||
/**
|
||||
* @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start} event it is a org.matrix.msc3381.poll.start because that's what this listener is filtering for
|
||||
*/
|
||||
async event => {
|
||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||
const messageResponses = await sendEvent.sendEvent(event)
|
||||
await api.ackEvent(event)
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.response", guard("org.matrix.msc3381.poll.response",
|
||||
/**
|
||||
* @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Response} event it is a org.matrix.msc3381.poll.response because that's what this listener is filtering for
|
||||
*/
|
||||
async event => {
|
||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||
await vote.updateVote(event) // Matrix votes can't be bridged, so all we do is store it in the database.
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction",
|
||||
/**
|
||||
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event it is a m.reaction because that's what this listener is filtering for
|
||||
|
||||
Reference in New Issue
Block a user