From 9f9d1f615edf7cd636fa48b15c57f41733a67268 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 23:35:09 +1300 Subject: [PATCH] Test coverage for all interactions --- src/discord/interactions/invite.js | 2 +- src/discord/interactions/matrix-info.js | 2 +- src/discord/interactions/permissions.js | 72 +++++-- src/discord/interactions/permissions.test.js | 199 +++++++++++++++++++ src/discord/interactions/privacy.js | 1 - src/discord/interactions/reactions.js | 2 +- src/discord/interactions/reactions.test.js | 4 - test/test.js | 1 + 8 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 src/discord/interactions/permissions.test.js diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index bacf465a..b35f44f3 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -3,7 +3,7 @@ const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") const assert = require("assert/strict") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, db, select} = require("../../passthrough") /** @type {import("../../d2m/actions/create-room")} */ const createRoom = sync.require("../../d2m/actions/create-room") diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index f7bc31a6..b7551f10 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") diff --git a/src/discord/interactions/permissions.js b/src/discord/interactions/permissions.js index e010b1b8..fea9ce0c 100644 --- a/src/discord/interactions/permissions.js +++ b/src/discord/interactions/permissions.js @@ -2,34 +2,41 @@ const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, select, from} = require("../../passthrough") const assert = require("assert/strict") +const {id: botID} = require("../../../addbot") +const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction - * @returns {Promise} + * @param {{api: typeof api}} di + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data, channel, guild_id}) { - const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() - assert(row) +async function* _interact({data, guild_id}, {api}) { + // Get message info + const row = from("event_message") + .join("message_channel", "message_id") + .select("event_id", "source", "channel_id") + .where({message_id: data.target_id}) + .get() // Can't operate on Discord users - if (row.source === 1) { // discord - return { + if (!row || row.source === 1) { // not bridged or sent by a discord user + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This command is only meaningful for Matrix users.`, + content: `The permissions command can only be used on Matrix users.`, flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } // Get the message sender, the person that will be inspected/edited const eventID = row.event_id - const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + const roomID = select("channel_room", "room_id", {channel_id: row.channel_id}).pluck().get() assert(roomID) const event = await api.getEvent(roomID, eventID) const sender = event.sender @@ -45,16 +52,16 @@ async function _interact({data, channel, guild_id}) { // Administrators equal to the bot cannot be demoted if (userPower >= 100) { - return { + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${sender}\` has administrator permissions. This cannot be edited.`, flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } - return { + yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `Showing permissions for \`${sender}\`. Click to edit.`, @@ -82,13 +89,15 @@ async function _interact({data, channel, guild_id}) { } ] } - } + }} } /** * @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction + * @param {{api: typeof api}} di + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function interactEdit({data, id, token, guild_id, message}) { +async function* _interactEdit({data, guild_id, message}, {api}) { // Get the person that will be inspected/edited const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) @@ -96,13 +105,13 @@ async function interactEdit({data, id, token, guild_id, message}) { const permission = data.values[0] const power = permission === "moderator" ? 50 : 0 - await discord.snow.interaction.createInteractionResponse(id, token, { + yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.UpdateMessage, data: { content: `Updating \`${mxid}\` to **${permission}**, please wait...`, components: [] } - }) + }} // Get the space, where the power levels will be inspected/edited const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() @@ -112,17 +121,40 @@ async function interactEdit({data, id, token, guild_id, message}) { await api.setUserPowerCascade(spaceID, mxid, power) // ACK - await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, { + yield {editOriginalInteractionResponse: { content: `Updated \`${mxid}\` to **${permission}**.`, components: [] - }) + }} } + +/* c8 ignore start */ + /** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) + 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.APIMessageComponentSelectMenuInteraction} interaction */ +async function interactEdit(interaction) { + for await (const response of _interactEdit(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) + } + } } module.exports.interact = interact module.exports.interactEdit = interactEdit module.exports._interact = _interact +module.exports._interactEdit = _interactEdit diff --git a/src/discord/interactions/permissions.test.js b/src/discord/interactions/permissions.test.js new file mode 100644 index 00000000..64cda809 --- /dev/null +++ b/src/discord/interactions/permissions.test.js @@ -0,0 +1,199 @@ +const {test} = require("supertape") +const DiscordTypes = require("discord-api-types/v10") +const {select, db} = require("../../passthrough") +const {_interact, _interactEdit} = require("./permissions") + +/** + * @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("permissions: checks if message is bridged", async t => { + const msgs = await fromAsync(_interact({ + data: { + target_id: "0" + }, + guild_id: "0" + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "The permissions command can only be used on Matrix users.") +}) + +test("permissions: checks if message is sent by a matrix user", async t => { + const msgs = await fromAsync(_interact({ + data: { + target_id: "1126786462646550579" + }, + guild_id: "112760669178241024" + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "The permissions command can only be used on Matrix users.") +}) + +test("permissions: reports permissions of selected matrix user (implicit default)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {} + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.") + t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[0], {label: "Default", value: "default", default: true}) + t.equal(called, 2) +}) + +test("permissions: reports permissions of selected matrix user (moderator)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 50 + } + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.") + t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[1], {label: "Moderator", value: "moderator", default: true}) + t.equal(called, 2) +}) + +test("permissions: reports permissions of selected matrix user (admin)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 100 + } + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "`@cadence:cadence.moe` has administrator permissions. This cannot be edited.") + t.notOk(msgs[0].createInteractionResponse.data.components) + t.equal(called, 2) +}) + +test("permissions: can update user to moderator", async t => { + let called = 0 + const msgs = await fromAsync(_interactEdit({ + data: { + target_id: "1128118177155526666", + values: ["moderator"] + }, + message: { + content: "Showing permissions for `@cadence:cadence.moe`. Click to edit." + }, + guild_id: "112760669178241024" + }, { + api: { + async setUserPowerCascade(roomID, mxid, power) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(mxid, "@cadence:cadence.moe") + t.equal(power, 50) + } + } + })) + t.equal(msgs.length, 2) + t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **moderator**, please wait...") + t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **moderator**.") + t.equal(called, 1) +}) + +test("permissions: can update user to default", async t => { + let called = 0 + const msgs = await fromAsync(_interactEdit({ + data: { + target_id: "1128118177155526666", + values: ["default"] + }, + message: { + content: "Showing permissions for `@cadence:cadence.moe`. Click to edit." + }, + guild_id: "112760669178241024" + }, { + api: { + async setUserPowerCascade(roomID, mxid, power) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(mxid, "@cadence:cadence.moe") + t.equal(power, 0) + } + } + })) + t.equal(msgs.length, 2) + t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **default**, please wait...") + t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **default**.") + t.equal(called, 1) +}) diff --git a/src/discord/interactions/privacy.js b/src/discord/interactions/privacy.js index 8227b5a8..841167e0 100644 --- a/src/discord/interactions/privacy.js +++ b/src/discord/interactions/privacy.js @@ -16,7 +16,6 @@ const createSpace = sync.require("../../d2m/actions/create-space") async function* _interact({data, guild_id}, {createSpace}) { // Check guild is bridged const current = select("guild_space", "privacy_level", {guild_id}).pluck().get() - InteractionMethods.prototype.createInteractionResponse if (current == null) { return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 0a6d5a6e..1a5a9cac 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, select, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js index c6de0543..6e29f576 100644 --- a/src/discord/interactions/reactions.test.js +++ b/src/discord/interactions/reactions.test.js @@ -1,8 +1,4 @@ const {test} = require("supertape") -const data = require("../../../test/data") -const DiscordTypes = require("discord-api-types/v10") -const {db, discord} = require("../../passthrough") -const {MatrixServerError} = require("../../matrix/mreq") const {_interact} = require("./reactions") test("reactions: checks if message is bridged", async t => { diff --git a/test/test.js b/test/test.js index c6ab1bbb..ee638341 100644 --- a/test/test.js +++ b/test/test.js @@ -140,6 +140,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") require("../src/discord/interactions/matrix-info.test") + require("../src/discord/interactions/permissions.test") require("../src/discord/interactions/privacy.test") require("../src/discord/interactions/reactions.test") })()