diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index 95fc0aa..d991f08 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -23,16 +23,7 @@ async function removeSomeReactions(data) { const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get() if (!eventIDForMessage) return - /** @type {Ty.Event.Outer[]} */ - let reactions = [] - /** @type {string | undefined} */ - let nextBatch = undefined - do { - /** @type {Ty.Pagination>} */ - const res = await api.getRelations(roomID, eventIDForMessage, {from: nextBatch}, "m.annotation") - reactions = reactions.concat(res.chunk) - nextBatch = res.next_batch - } while (nextBatch) + const reactions = await api.getFullRelations(roomID, eventIDForMessage, "m.annotation") // Run the proper strategy and any strategy-specific database changes const removals = await diff --git a/discord/interactions/reactions.js b/discord/interactions/reactions.js new file mode 100644 index 0000000..67f3a68 --- /dev/null +++ b/discord/interactions/reactions.js @@ -0,0 +1,58 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db, select, from} = require("../../passthrough") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../../m2d/converters/utils")} */ +const utils = sync.require("../../m2d/converters/utils") + +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact({id, token, data}) { + 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 discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This message hasn't been bridged to Matrix.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") + + /** @type {Map} */ + const inverted = new Map() + for (const reaction of reactions) { + if (utils.eventSenderIsFromDiscord(reaction.sender)) continue + const key = reaction.content["m.relates_to"].key + const displayname = select("member_cache", "displayname", {mxid: reaction.sender, room_id: row.room_id}).pluck().get() || reaction.sender + if (!inverted.has(key)) inverted.set(key, []) + // @ts-ignore + inverted.get(key).push(displayname) + } + + if (inverted.size === 0) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Nobody from Matrix reacted to this message.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) +} + +module.exports.interact = interact diff --git a/discord/register-interactions.js b/discord/register-interactions.js index 553c6db..026ac7a 100644 --- a/discord/register-interactions.js +++ b/discord/register-interactions.js @@ -8,6 +8,7 @@ const matrixInfo = sync.require("./interactions/matrix-info.js") const invite = sync.require("./interactions/invite.js") const permissions = sync.require("./interactions/permissions.js") const bridge = sync.require("./interactions/bridge.js") +const reactions = sync.require("./interactions/reactions.js") discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Matrix info", @@ -18,6 +19,10 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message, default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles) +}, { + name: "Reactions", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.Message }, { name: "invite", contexts: [DiscordTypes.InteractionContextType.Guild], @@ -63,6 +68,8 @@ async function dispatchInteraction(interaction) { await permissions.interactEdit(interaction) } else if (interactionId === "bridge") { await bridge.interact(interaction) + } else if (interactionId === "Reactions") { + await reactions.interact(interaction) } else { throw new Error(`Unknown interaction ${interactionId}`) } diff --git a/matrix/api.js b/matrix/api.js index e94a1a5..0d61207 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -169,6 +169,26 @@ function getRelations(roomID, eventID, pagination, relType) { return mreq.mreq("GET", path) } +/** + * Like `getRelations` but collects and filters all pages for you. + * @param {string} roomID + * @param {string} eventID + * @param {string?} [relType] type of relations to filter, e.g. "m.annotation" for reactions + */ +async function getFullRelations(roomID, eventID, relType) { + /** @type {Ty.Event.Outer[]} */ + let reactions = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.Pagination>} */ + const res = await getRelations(roomID, eventID, {from: nextBatch}, relType) + reactions = reactions.concat(res.chunk) + nextBatch = res.next_batch + } while (nextBatch) + return reactions +} + /** * @param {string} roomID * @param {string} type @@ -289,6 +309,7 @@ module.exports.getJoinedMembers = getJoinedMembers module.exports.getHierarchy = getHierarchy module.exports.getFullHierarchy = getFullHierarchy module.exports.getRelations = getRelations +module.exports.getFullRelations = getFullRelations module.exports.sendState = sendState module.exports.sendEvent = sendEvent module.exports.redactEvent = redactEvent