From ddb211f8f3aae75f85d3b5a7fae5218c1d8adaf5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:17:34 +1200 Subject: [PATCH 01/10] Make permissions command apply recursively --- d2m/discord-client.js | 3 ++ discord/interactions/permissions.js | 53 ++++++++++++++++++++--------- discord/register-interactions.js | 29 +++++++--------- matrix/api.js | 16 +++++++++ start.js | 4 --- 5 files changed, 69 insertions(+), 36 deletions(-) diff --git a/d2m/discord-client.js b/d2m/discord-client.js index 80dcbcf..ace8481 100644 --- a/d2m/discord-client.js +++ b/d2m/discord-client.js @@ -57,6 +57,9 @@ class DiscordClient { addEventLogger("error", "Error") addEventLogger("disconnected", "Disconnected") addEventLogger("ready", "Ready") + this.snow.requestHandler.on("requestError", (requestID, error) => { + console.error("request error:", error) + }) } } diff --git a/discord/interactions/permissions.js b/discord/interactions/permissions.js index d30f632..82c3d3c 100644 --- a/discord/interactions/permissions.js +++ b/discord/interactions/permissions.js @@ -5,23 +5,27 @@ const Ty = require("../../types") const {discord, sync, db, select, from} = require("../../passthrough") const assert = require("assert/strict") + /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -async function interact({data, channel, id, token, guild_id}) { +/** + * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction + * @returns {Promise} + */ +async function _interact({data, channel, guild_id}) { const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() assert(row) // Can't operate on Discord users if (row.source === 1) { // discord - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `This command is only meaningful for Matrix users.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } // Get the message sender, the person that will be inspected/edited @@ -42,16 +46,16 @@ async function interact({data, channel, id, token, guild_id}) { // Administrators equal to the bot cannot be demoted if (userPower >= 100) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${sender}\` has administrator permissions. This cannot be edited.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } - await discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `Showing permissions for \`${sender}\`. Click to edit.`, @@ -79,30 +83,47 @@ async function interact({data, channel, id, token, guild_id}) { } ] } - }) + } } -/** @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction */ -async function interactEdit({data, channel, id, token, guild_id, message}) { +/** + * @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction + */ +async function interactEdit({data, id, token, guild_id, message}) { // Get the person that will be inspected/edited const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) + const permission = data.values[0] + const power = permission === "moderator" ? 50 : 0 + + await discord.snow.interaction.createInteractionResponse(id, token, { + 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() assert(spaceID) // Do it - const permission = data.values[0] - const power = permission === "moderator" ? 50 : 0 - await api.setUserPower(spaceID, mxid, power) - // TODO: Cascade permissions through room hierarchy (make a helper for this already, geez...) + await api.setUserPowerCascade(spaceID, mxid, power) // ACK - await discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.DeferredMessageUpdate + await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, { + content: `Updated \`${mxid}\` to **${permission}**.`, + components: [] }) } +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) +} + module.exports.interact = interact module.exports.interactEdit = interactEdit +module.exports._interact = _interact diff --git a/discord/register-interactions.js b/discord/register-interactions.js index 79bcb14..a6e4332 100644 --- a/discord/register-interactions.js +++ b/discord/register-interactions.js @@ -48,23 +48,23 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ }]) async function dispatchInteraction(interaction) { - const id = interaction.data.custom_id || interaction.data.name + const interactionId = interaction.data.custom_id || interaction.data.name try { console.log(interaction) - if (id === "Matrix info") { + if (interactionId === "Matrix info") { await matrixInfo.interact(interaction) - } else if (id === "invite") { + } else if (interactionId === "invite") { await invite.interact(interaction) - } else if (id === "invite_channel") { + } else if (interactionId === "invite_channel") { await invite.interactButton(interaction) - } else if (id === "Permissions") { + } else if (interactionId === "Permissions") { await permissions.interact(interaction) - } else if (id === "permissions_edit") { + } else if (interactionId === "permissions_edit") { await permissions.interactEdit(interaction) - } else if (id === "bridge") { + } else if (interactionId === "bridge") { await bridge.interact(interaction) } else { - throw new Error(`Unknown interaction ${id}`) + throw new Error(`Unknown interaction ${interactionId}`) } } catch (e) { let stackLines = null @@ -75,14 +75,11 @@ async function dispatchInteraction(interaction) { stackLines = stackLines.slice(0, cloudstormLine - 2) } } - discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `Interaction failed: **${id}**` - + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` - + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, - flags: DiscordTypes.MessageFlags.Ephemeral - } + await discord.snow.interaction.createFollowupMessage(id, interaction.token, { + content: `Interaction failed: **${interactionId}**` + + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` + + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, + flags: DiscordTypes.MessageFlags.Ephemeral }) } } diff --git a/matrix/api.js b/matrix/api.js index 7d8ea9f..e94a1a5 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -260,6 +260,21 @@ async function setUserPower(roomID, mxid, power) { return powerLevels } +/** + * Set a user's power level for a whole room hierarchy. + * @param {string} roomID + * @param {string} mxid + * @param {number} power + */ +async function setUserPowerCascade(roomID, mxid, power) { + assert(roomID[0] === "!") + assert(mxid[0] === "@") + const rooms = await getFullHierarchy(roomID) + for (const room of rooms) { + await setUserPower(room.room_id, mxid, power) + } +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -281,3 +296,4 @@ module.exports.sendTyping = sendTyping module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.setUserPower = setUserPower +module.exports.setUserPowerCascade = setUserPowerCascade diff --git a/start.js b/start.js index 63f5c57..e218819 100644 --- a/start.js +++ b/start.js @@ -27,10 +27,6 @@ passthrough.select = orm.select sync.require("./m2d/event-dispatcher") -discord.snow.requestHandler.on("requestError", data => { - console.error("request error", data) -}) - ;(async () => { await migrate.migrate(db) await discord.cloud.connect() From 78a17b2de9e80ceb567c518e648972ef630914d9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:17:54 +1200 Subject: [PATCH 02/10] Update formatting of matrix info command --- discord/interactions/matrix-info.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/discord/interactions/matrix-info.js b/discord/interactions/matrix-info.js index 9f0e9e1..fac3804 100644 --- a/discord/interactions/matrix-info.js +++ b/discord/interactions/matrix-info.js @@ -7,7 +7,8 @@ const {discord, sync, db, select, from} = require("../../passthrough") const api = sync.require("../../matrix/api") /** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -async function interact({id, token, data}) { +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact({id, token, guild_id, channel, data}) { const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") .select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get() @@ -21,12 +22,15 @@ async function interact({id, token, data}) { }) } + const idInfo = `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\`` + if (message.source === 1) { // from Discord + const userID = data.resolved.messages[data.target_id].author.id return discord.snow.interaction.createInteractionResponse(id, token, { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This message was bridged to [${message.nick || message.name}]() on Matrix.` - + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord to [${message.nick || message.name}]() on Matrix.` + + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } }) @@ -37,9 +41,8 @@ async function interact({id, token, data}) { return discord.snow.interaction.createInteractionResponse(id, token, { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This message was bridged from [${message.nick || message.name}]() on Matrix.` - + `\nIt was originally sent by [${event.sender}]().` - + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + content: `Bridged [${event.sender}]()'s message in [${message.nick || message.name}]() on Matrix to https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord.` + + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } }) From 4b7593d630e30ef5f1dfb2f3307461aac8d1ef61 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:18:06 +1200 Subject: [PATCH 03/10] Make invite command more testable --- discord/interactions/invite.js | 54 ++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/discord/interactions/invite.js b/discord/interactions/invite.js index 0590be8..689ea1a 100644 --- a/discord/interactions/invite.js +++ b/discord/interactions/invite.js @@ -7,31 +7,34 @@ const {discord, sync, db, select, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ -async function interact({id, token, data, channel, member, guild_id}) { +/** + * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction + * @returns {Promise} + */ +async function _interact({data, channel, guild_id}) { // Check guild is bridged const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() - if (!spaceID || !roomID) return discord.snow.interaction.createInteractionResponse(id, token, { + if (!spaceID || !roomID) return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This server isn't bridged to Matrix, so you can't invite Matrix users.", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } // 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 discord.snow.interaction.createInteractionResponse(id, token, { + if (!mxid) return { 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 } - }) + } // Check for existing invite to the space let spaceMember @@ -39,24 +42,24 @@ async function interact({id, token, data, channel, member, guild_id}) { spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) } catch (e) {} if (spaceMember && spaceMember.membership === "invite") { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } // Invite Matrix user if not in space if (!spaceMember || spaceMember.membership !== "join") { await api.inviteToRoom(spaceID, mxid) - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `You invited \`${mxid}\` to the server.` } - }) + } } // The Matrix user *is* in the space, maybe we want to invite them to this channel? @@ -65,7 +68,7 @@ async function interact({id, token, data, channel, member, guild_id}) { roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) } catch (e) {} if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, @@ -80,34 +83,49 @@ async function interact({id, token, data, channel, member, guild_id}) { }] }] } - }) + } } // The Matrix user *is* in the space and in the channel. - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` is already in this server and this channel.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } -/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ -async function interactButton({id, token, data, channel, member, guild_id, message}) { +/** + * @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction + * @returns {Promise} + */ +async function _interactButton({channel, message}) { const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() await api.inviteToRoom(roomID, mxid) - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.UpdateMessage, data: { content: `You invited \`${mxid}\` to the channel.`, flags: DiscordTypes.MessageFlags.Ephemeral, components: [] } - }) + } +} + +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) +} + +/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ +async function interactButton(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction)) } module.exports.interact = interact module.exports.interactButton = interactButton +module.exports._interact = _interact +module.exports._interactButton = _interactButton From 42bfd034cf4b1bc9d97924c247d47976dc801d4d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:50:48 +1200 Subject: [PATCH 04/10] Bridge command author metadata to Matrix --- d2m/converters/message-to-event.js | 5 +++++ discord/interactions/bridge.js | 2 +- discord/utils.js | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 1e77d9d..d4b580f 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -229,6 +229,11 @@ async function messageToEvent(message, guild, options = {}, di) { }] } + if (message.type === DiscordTypes.MessageType.ChatInputCommand && message.interaction_metadata && "name" in message.interaction_metadata) { + // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top. + message.content = `> ↪️ <@${message.interaction_metadata.user.id}> used \`/${message.interaction_metadata.name}\`\n${message.content}` + } + /** @type {{room?: boolean, user_ids?: string[]}} We should consider the following scenarios for mentions: diff --git a/discord/interactions/bridge.js b/discord/interactions/bridge.js index ee33bfd..b2d1ac0 100644 --- a/discord/interactions/bridge.js +++ b/discord/interactions/bridge.js @@ -39,7 +39,7 @@ async function getCachedHierarchy(spaceID) { /** @type {{name: string, value: string}[]} */ const childRooms = [] for (const room of result) { - if (room.name) { + if (room.name && !room.name.match(/^\[[⛓️🔊]\]/) && room.room_type !== "m.space") { childRooms.push({name: room.name, value: room.room_id}) reverseCache.set(room.room_id, spaceID) } diff --git a/discord/utils.js b/discord/utils.js index 57e563f..865b2e3 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -97,8 +97,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) { * @param {DiscordTypes.APIMessage} message */ function isWebhookMessage(message) { - const isInteractionResponse = message.type === 20 - return message.webhook_id && !isInteractionResponse + return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand } /** From a6c961984d39b207a04c7065b46b3dd088be931e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:31:57 +1200 Subject: [PATCH 05/10] An emoji can be a single character --- d2m/converters/message-to-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index d4b580f..e6044b6 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -368,7 +368,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter. // So we scan the message ahead of time for all its emojis and ensure they are in the DB. - const emojiMatches = [...content.matchAll(/<(a?):([^:>]{2,64}):([0-9]+)>/g)] + const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)] await Promise.all(emojiMatches.map(match => { const id = match[3] const name = match[2] From 818311bcb4cbca013e66c71c5c0cb7b410b994b0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:35:53 +1200 Subject: [PATCH 06/10] Use kstate $url feature for channel icons --- d2m/actions/create-room.js | 3 +-- d2m/actions/create-space.js | 1 - d2m/actions/create-space.test.js | 1 - test/data.js | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 322f8be..2dd62e0 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -120,8 +120,7 @@ async function channelToKState(channel, guild) { if (customAvatar) { avatarEventContent.url = customAvatar } else if (guild.icon) { - avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API + avatarEventContent.url = {$url: file.guildIcon(guild)} } let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index b15cba6..a9b8448 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -65,7 +65,6 @@ async function guildToKState(guild, privacyLevel) { "m.room.name/": {name: guild.name}, "m.room.avatar/": { $if: guild.icon, - discord_path: file.guildIcon(guild), url: {$url: file.guildIcon(guild)} }, "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, diff --git a/d2m/actions/create-space.test.js b/d2m/actions/create-space.test.js index b1c1f06..c4111db 100644 --- a/d2m/actions/create-space.test.js +++ b/d2m/actions/create-space.test.js @@ -14,7 +14,6 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))), { "m.room.avatar/": { - discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" }, "m.room.guest_access/": { diff --git a/test/data.js b/test/data.js index 771c183..456033a 100644 --- a/test/data.js +++ b/test/data.js @@ -38,8 +38,7 @@ module.exports = { }] }, "m.room.avatar/": { - discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", - url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"} }, "m.room.power_levels/": { events: { From 607fd3808a8c1ebec7d77e17c7d086582a2d8b73 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:47:47 +1200 Subject: [PATCH 07/10] Fix bigint/number type in orm WHERE --- db/orm-defs.d.ts | 1 + db/orm.js | 4 ++-- m2d/actions/redact.js | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index e481f95..7484d76 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -114,3 +114,4 @@ export type AllKeys = U extends any ? keyof U : never export type PickTypeOf> = T extends { [k in K]?: any } ? T[K] : never export type Merge = {[x in AllKeys]: PickTypeOf} export type Nullable = {[k in keyof T]: T[k] | null} +export type Numberish = {[k in keyof T]: T[k] extends number ? (number | bigint) : T[k]} diff --git a/db/orm.js b/db/orm.js index 09e4bc7..601a7a0 100644 --- a/db/orm.js +++ b/db/orm.js @@ -8,7 +8,7 @@ const U = require("./orm-defs") * @template {keyof U.Models[Table]} Col * @param {Table} table * @param {Col[] | Col} cols - * @param {Partial} where + * @param {Partial>} where * @param {string} [e] */ function select(table, cols, where = {}, e = "") { @@ -108,7 +108,7 @@ class From { } /** - * @param {Partial} conditions + * @param {Partial>} conditions */ where(conditions) { const wheres = Object.entries(conditions).map(([col, value]) => { diff --git a/m2d/actions/redact.js b/m2d/actions/redact.js index 7569df4..ffbb261 100644 --- a/m2d/actions/redact.js +++ b/m2d/actions/redact.js @@ -25,7 +25,6 @@ async function deleteMessage(event) { */ async function removeReaction(event) { const hash = utils.getEventIDHash(event.redacts) - // TODO: this works but fix the type const row = from("reaction").join("message_channel", "message_id").select("channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get() if (!row) return await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji) From 71c553a9cf5ae8ffca22263c62fb7805fd4fcb8a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:05:40 +1200 Subject: [PATCH 08/10] Test cases for bridging author command metadata --- .../message-to-event.embeds.test.js | 35 +++++++++++++++++++ d2m/converters/message-to-event.js | 11 ++++-- test/ooye-test-data.sql | 3 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index 61a0822..05e3b5d 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -6,6 +6,13 @@ const Ty = require("../../types") test("message2event embeds: nothing but a field", async t => { const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {}) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", "m.mentions": {}, msgtype: "m.notice", @@ -143,6 +150,13 @@ test("message2event embeds: crazy html is all escaped", async t => { test("message2event embeds: title without url", async t => { const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", @@ -155,6 +169,13 @@ test("message2event embeds: title without url", async t => { test("message2event embeds: url without title", async t => { const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| I condone pirating music!", @@ -167,6 +188,13 @@ test("message2event embeds: url without title", async t => { test("message2event embeds: author without url", async t => { const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| ## Amanda\n| \n| I condone pirating music!", @@ -179,6 +207,13 @@ test("message2event embeds: author without url", async t => { test("message2event embeds: author url without name", async t => { const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| I condone pirating music!", diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index e6044b6..b86293e 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -32,7 +32,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) { /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() - const username = message.mentions.find(ment => ment.id === node.id)?.username || node.id + const interaction = message.interaction_metadata || message.interaction + const username = message.mentions.find(ment => ment.id === node.id)?.username + || (interaction?.user.id === node.id ? interaction.user.username : null) + || node.id if (mxid && useHTML) { return `@${username}` } else { @@ -229,9 +232,11 @@ async function messageToEvent(message, guild, options = {}, di) { }] } - if (message.type === DiscordTypes.MessageType.ChatInputCommand && message.interaction_metadata && "name" in message.interaction_metadata) { + const interaction = message.interaction_metadata || message.interaction + if (message.type === DiscordTypes.MessageType.ChatInputCommand && interaction && "name" in interaction) { // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top. - message.content = `> ↪️ <@${message.interaction_metadata.user.id}> used \`/${message.interaction_metadata.name}\`\n${message.content}` + if (message.content) message.content = `\n${message.content}` + message.content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${message.content}` } /** diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1fb9e24..370c6aa 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -23,7 +23,8 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES ('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), ('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), ('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'), -('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'); +('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'), +('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe'); INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); From 2c27879afb2a308439c66404a0a638215c9493b1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:09:27 +1200 Subject: [PATCH 09/10] Add another async/await, just to be safe --- d2m/discord-packets.js | 2 +- d2m/event-dispatcher.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 0981827..ed47fae 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -181,7 +181,7 @@ const utils = { } catch (e) { // Let OOYE try to handle errors too - eventDispatcher.onError(client, e, message) + await eventDispatcher.onError(client, e, message) } } } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 7f27b77..57cb72c 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -50,7 +50,7 @@ module.exports = { * @param {Error} e * @param {import("cloudstorm").IGatewayMessage} gatewayMessage */ - onError(client, e, gatewayMessage) { + async onError(client, e, gatewayMessage) { console.error("hit event-dispatcher's error handler with this exception:") console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later console.error(`while handling this ${gatewayMessage.t} gateway event:`) @@ -83,7 +83,7 @@ module.exports = { builder.addLine(`Error trace:\n${stackLines.join("\n")}`, `
Error trace
${stackLines.join("\n")}
`) } builder.addLine("", `
Original payload
${util.inspect(gatewayMessage.d, false, 4, false)}
`) - api.sendEvent(roomID, "m.room.message", { + await api.sendEvent(roomID, "m.room.message", { ...builder.get(), "moe.cadence.ooye.error": { source: "discord", From 5d91f999f2144a263101ef2e824b8375f27992bf Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:53:27 +1200 Subject: [PATCH 10/10] Make power migration testable --- matrix/power.js | 21 +++++++++++++-------- start.js | 3 ++- test/ooye-test-data.sql | 28 +++++++++++++++------------- test/test.js | 2 ++ 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/matrix/power.js b/matrix/power.js index 5dac550..cd2b8cb 100644 --- a/matrix/power.js +++ b/matrix/power.js @@ -1,7 +1,6 @@ // @ts-check const {db, from} = require("../passthrough") -const api = require("./api") const reg = require("./read-registration") const ks = require("./kstate") const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") @@ -11,13 +10,16 @@ for (const mxid of reg.ooye.invite) { db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") } -// Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. -const rows = from("member_cache").join("member_power", "mxid") - .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") - .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") - .all() +/** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */ +function _getAffectedRooms() { + return from("member_cache").join("member_power", "mxid") + .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") + .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") + .all() +} -;(async () => { +async function applyPower() { + const rows = _getAffectedRooms() for (const row of rows) { const kstate = await roomToKState(row.room_id) const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}}) @@ -26,4 +28,7 @@ const rows = from("member_cache").join("member_power", "mxid") // but we update it here anyway since the homeserver does not always deliver the event round-trip. db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid) } -})() +} + +module.exports._getAffectedRooms = _getAffectedRooms +module.exports.applyPower = applyPower diff --git a/start.js b/start.js index e218819..1ece1dd 100644 --- a/start.js +++ b/start.js @@ -25,13 +25,14 @@ const orm = sync.require("./db/orm") passthrough.from = orm.from passthrough.select = orm.select +const power = require("./matrix/power.js") sync.require("./m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) await discord.cloud.connect() console.log("Discord gateway started") - require("./matrix/power.js") + await power.applyPower() require("./stdin") })() diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 370c6aa..4666b4d 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -126,19 +126,21 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'), ('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'); -INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES -('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), -('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'), -('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL), -('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko'), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP'), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), -('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL); +INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), +('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc', 0), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL, 0), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL, 0), +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0), +('!BpMdOUkWWhFxmTrENV:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 100); INSERT INTO member_power (mxid, room_id, power_level) VALUES ('@test_auto_invite:example.org', '*', 100); diff --git a/test/test.js b/test/test.js index b5977f1..796ff68 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" +reg.ooye.invite = [] const sync = new HeatSync({watchFS: false}) @@ -116,6 +117,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/file.test") + require("../matrix/power.test") require("../matrix/read-registration.test") require("../matrix/txnid.test") require("../d2m/actions/create-room.test")