From 1e4952f1b871adaa7ba722b383bf1576d4794b76 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 14:31:32 +1300 Subject: [PATCH 1/2] Add anti-timeout system to reactions interaction --- src/discord/interactions/reactions.js | 45 +++++++++++++--------- src/discord/interactions/reactions.test.js | 34 +++++++++++----- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 1a5a9ca..13dbd69 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -2,6 +2,8 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, select, from} = require("../../passthrough") +const {id: botID} = require("../../../addbot") +const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") @@ -11,21 +13,28 @@ const utils = sync.require("../../m2d/converters/utils") /** * @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction * @param {{api: typeof api}} di - * @returns {Promise} + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data}, {api}) { +async function* _interact({data}, {api}) { const row = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") .select("event_id", "room_id").where({message_id: data.target_id}).get() if (!row) { - return { + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This message hasn't been bridged to Matrix.", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } + yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} + const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") /** @type {Map} */ @@ -40,29 +49,27 @@ async function _interact({data}, {api}) { } if (inverted.size === 0) { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "Nobody from Matrix reacted to this message.", - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: "Nobody from Matrix reacted to this message.", + }} } - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: [...inverted.entries()].map(([key, value]) => `${key} ā®ž ${value.join(" ⬩ ")}`).join("\n"), - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: [...inverted.entries()].map(([key, value]) => `${key} ā®ž ${value.join(" ⬩ ")}`).join("\n"), + }} } /* c8 ignore start */ /** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) + for await (const response of _interact(interaction, {api})) { + if (response.createInteractionResponse) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } } module.exports.interact = interact diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js index 6e29f57..50ddeca 100644 --- a/src/discord/interactions/reactions.test.js +++ b/src/discord/interactions/reactions.test.js @@ -1,17 +1,31 @@ const {test} = require("supertape") const {_interact} = require("./reactions") +/** + * @template T + * @param {AsyncIterable} ai + * @returns {Promise} + */ +async function fromAsync(ai) { + const result = [] + for await (const value of ai) { + result.push(value) + } + return result +} + test("reactions: checks if message is bridged", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "0" } - }, {}) - t.equal(msg.data.content, "This message hasn't been bridged to Matrix.") + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "This message hasn't been bridged to Matrix.") }) test("reactions: different response if nobody reacted", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "1126786462646550579" } @@ -23,13 +37,14 @@ test("reactions: different response if nobody reacted", async t => { return [] } } - }) - t.equal(msg.data.content, "Nobody from Matrix reacted to this message.") + })) + t.equal(msgs.length, 2) + t.equal(msgs[1].editOriginalInteractionResponse.content, "Nobody from Matrix reacted to this message.") }) test("reactions: shows reactions if there are some, ignoring discord users", async t => { let called = 1 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "1126786462646550579" } @@ -73,9 +88,10 @@ test("reactions: shows reactions if there are some, ignoring discord users", asy }] } } - }) + })) + t.equal(msgs.length, 2) t.equal( - msg.data.content, + msgs[1].editOriginalInteractionResponse.content, "🐈 ā®ž cadence [they] ⬩ @rnl:cadence.moe" + "\nšŸˆā€ā¬› ā®ž cadence [they]" ) From 6bb31deeaf55fa01a74eb0bef44cbd17892d6677 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 16 Jan 2025 08:40:26 +1300 Subject: [PATCH 2/2] Ignore missed messages if channel was just added --- src/d2m/event-dispatcher.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 943e40e..a80c451 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -103,13 +103,19 @@ module.exports = { async checkMissedMessages(client, guild) { if (guild.unavailable) return const bridgedChannels = select("channel_room", "channel_id").pluck().all() - const prepared = select("event_message", "event_id", {}, "WHERE message_id = ?").pluck() + const preparedExists = db.prepare("SELECT channel_id FROM message_channel WHERE channel_id = ? LIMIT 1") + const preparedGet = select("event_message", "event_id", {}, "WHERE message_id = ?").pluck() for (const channel of guild.channels.concat(guild.threads)) { if (!bridgedChannels.includes(channel.id)) continue if (!("last_message_id" in channel) || !channel.last_message_id) continue - const latestWasBridged = prepared.get(channel.last_message_id) + + // Skip if channel is already up-to-date + const latestWasBridged = preparedGet.get(channel.last_message_id) if (latestWasBridged) continue + // Skip if channel was just added to the bridge (there's no place to resume from if it's brand new) + if (!preparedExists.get(channel.id)) continue + // Permissions check const member = guild.members.find(m => m.user?.id === client.user.id) if (!member) return @@ -131,7 +137,7 @@ module.exports = { } } let latestBridgedMessageIndex = messages.findIndex(m => { - return prepared.get(m.id) + return preparedGet.get(m.id) }) // console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`) if (latestBridgedMessageIndex === -1) latestBridgedMessageIndex = 1 // rather than crawling the ENTIRE channel history, let's just bridge the most recent 1 message to make it up to date.