From da5525a5429fe04ecd11350b07958bf2a8c5e72c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 14 Oct 2024 13:09:30 +1300 Subject: [PATCH] Make invite interaction async Fix potential lag issues --- src/discord/interactions/invite.js | 84 ++++++++++++------------- src/discord/interactions/invite.test.js | 61 +++++++++++------- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index b35f44f..0afc6d8 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -1,8 +1,9 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const Ty = require("../../types") const assert = require("assert/strict") +const {InteractionMethods} = require("snowtransfer") +const {id: botID} = require("../../../addbot") const {discord, sync, db, select} = require("../../passthrough") /** @type {import("../../d2m/actions/create-room")} */ @@ -15,21 +16,21 @@ const api = sync.require("../../matrix/api") /** * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction * @param {{api: typeof api}} di - * @returns {Promise} + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data, channel, guild_id}, {api}) { +async function* _interact({data, channel, guild_id}, {api}) { // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options const input = options?.[0]?.value || "" const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] - if (!mxid) return { + if (!mxid) return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} const guild = discord.guilds.get(guild_id) assert(guild) @@ -37,15 +38,22 @@ async function _interact({data, channel, guild_id}, {api}) { // Ensure guild and room are bridged db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id) const existing = createRoom.existsOrAutocreatable(channel, guild_id) - if (existing === 0) return { + if (existing === 0) return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} assert(existing) // can't be null or undefined as we just inserted the guild_active row + yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} + const spaceID = await createSpace.ensureSpace(guild) const roomID = await createRoom.ensureRoom(channel.id) @@ -55,24 +63,17 @@ async function _interact({data, channel, guild_id}, {api}) { spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) } catch (e) {} if (spaceMember && spaceMember.membership === "invite") { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, + }} } // Invite Matrix user if not in space if (!spaceMember || spaceMember.membership !== "join") { await api.inviteToRoom(spaceID, mxid) - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `You invited \`${mxid}\` to the server.` - } - } + return yield {editOriginalInteractionResponse: { + content: `You invited \`${mxid}\` to the server.` + }} } // The Matrix user *is* in the space, maybe we want to invite them to this channel? @@ -81,32 +82,24 @@ async function _interact({data, channel, guild_id}, {api}) { roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) } catch (e) {} if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, - flags: DiscordTypes.MessageFlags.Ephemeral, + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, + components: [{ + type: DiscordTypes.ComponentType.ActionRow, components: [{ - type: DiscordTypes.ComponentType.ActionRow, - components: [{ - type: DiscordTypes.ComponentType.Button, - custom_id: "invite_channel", - style: DiscordTypes.ButtonStyle.Primary, - label: "Sure", - }] + type: DiscordTypes.ComponentType.Button, + custom_id: "invite_channel", + style: DiscordTypes.ButtonStyle.Primary, + label: "Sure", }] - } - } + }] + }} } // The Matrix user *is* in the space and in the channel. - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` is already in this server and this channel.`, - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` is already in this server and this channel.`, + }} } /** @@ -133,7 +126,14 @@ async function _interactButton({channel, message}, {api}) { /** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} 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) { + // TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all. + 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) + } + } } /** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ diff --git a/src/discord/interactions/invite.test.js b/src/discord/interactions/invite.test.js index 431ff60..1890b76 100644 --- a/src/discord/interactions/invite.test.js +++ b/src/discord/interactions/invite.test.js @@ -4,19 +4,32 @@ const {db, discord} = require("../../passthrough") const {MatrixServerError} = require("../../matrix/mreq") const {_interact, _interactButton} = require("./invite") +/** + * @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("invite: checks for missing matrix ID", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [] }, channel: discord.channels.get("0"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") }) test("invite: checks for invalid matrix ID", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -26,13 +39,13 @@ test("invite: checks for invalid matrix ID", async t => { }, channel: discord.channels.get("0"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") }) test("invite: checks if channel exists or is autocreatable", async t => { db.prepare("UPDATE guild_active SET autocreate = 0").run() - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -42,14 +55,14 @@ test("invite: checks if channel exists or is autocreatable", async t => { }, channel: discord.channels.get("498323546729086986"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.") db.prepare("UPDATE guild_active SET autocreate = 1").run() }) test("invite: checks if user is already invited to space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -72,14 +85,14 @@ test("invite: checks if user is already invited to space", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.") t.equal(called, 1) }) test("invite: invites if user is not in space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -104,14 +117,14 @@ test("invite: invites if user is not in space", async t => { t.equal(mxid, "@cadence:cadence.moe") } } - }) - t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the server.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "You invited `@cadence:cadence.moe` to the server.") t.equal(called, 2) }) test("invite: prompts to invite to room (if never joined)", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -137,14 +150,14 @@ test("invite: prompts to invite to room (if never joined)", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") t.equal(called, 2) }) test("invite: prompts to invite to room (if left)", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -173,8 +186,8 @@ test("invite: prompts to invite to room (if left)", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") t.equal(called, 2) }) @@ -200,7 +213,7 @@ test("invite button: invites to room when button clicked", async t => { test("invite: no-op if in room and space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -222,7 +235,7 @@ test("invite: no-op if in room and space", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server and this channel.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server and this channel.") t.equal(called, 2) })