diff --git a/package-lock.json b/package-lock.json index ed438d0..eb07b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", diff --git a/package.json b/package.json index 73fd43d..9dfd2a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "description": "A bridge between Matrix and Discord", "main": "index.js", "repository": { diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 5b3b4f3..8550d43 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -60,7 +60,8 @@ async function sendMessage(message, channel, guild, row) { const detailedResultsMessage = await pollEnd.endPoll(message) if (detailedResultsMessage) { const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get() - const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent) + const channelID = threadParent ? threadParent : message.channel_id + const threadID = threadParent ? message.channel_id : undefined sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID) } } diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js index 42e3a35..218f046 100644 --- a/src/d2m/actions/speedbump.js +++ b/src/d2m/actions/speedbump.js @@ -1,5 +1,6 @@ // @ts-check +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const {discord, select, db} = passthrough @@ -69,18 +70,12 @@ async function doSpeedbump(messageID) { * Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted. * @param {string} channelID * @param {string} messageID - * @param {string} [userID] if provided, only slow down the message when the user has used PK before * @returns whether it was deleted, and data about the channel's (not thread's) speedbump */ -async function maybeDoSpeedbump(channelID, messageID, userID) { - let row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get() - if (row?.thread_parent) row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread - if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump - if (userID) { - if (row.speedbump_webhook_id === userID) return {affected: false, row} // shortcut - const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get() - if (!userHasProxy) return {affected: false, row} // user has not used PK before, no speedbump - } +async function maybeDoSpeedbump(channelID, messageID) { + let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get() + if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread + if (!row?.speedbump_webhook_id) return {affected: false, row: null} // not affected, no speedbump const affected = await doSpeedbump(messageID) return {affected, row} // maybe affected, and there is a speedbump } diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index aeda572..7229d3d 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -265,9 +265,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) { * @param {any} newEvents merge into events * @param {any} events will be modified * @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc - * @param {boolean} [forceMerge] if true, must merge event, will error if it had to append */ -function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) { +function mergeTextEvents(newEvents, events, forceSameMsgtype) { let prev = events.at(-1) for (const ne of newEvents) { const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype) @@ -279,8 +278,6 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false rep.addLine(ne.body, ne.formatted_body) prev.body = rep.body prev.formatted_body = rep.formattedBody - } else if (forceMerge) { - throw new Error("Unable to merge events") } else { events.push(ne) } @@ -557,7 +554,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?):([^:>]+):([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] @@ -970,8 +967,7 @@ async function messageToEvent(message, guild, options = {}, di) { // May only be a section accessory or in an action row (up to 5) if (component.style === DiscordTypes.ButtonStyle.Link) { assert(component.label) // required for Discord to validate link buttons - const link = await transformContentMessageLinks(component.url) - stack.msb.add(`[${component.label} ${link}] `, tag`${component.label} `) + stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) } } @@ -984,19 +980,7 @@ async function messageToEvent(message, guild, options = {}, di) { const {body, formatted_body} = stack.msb.get() if (body.trim().length) { - // Create new message if Components V2 (cannot have regular content) - if ((message.flags ?? 0) & DiscordTypes.MessageFlags.IsComponentsV2) { - await addTextEvent(body, formatted_body, "m.text") - } - // Add to existing message if legacy components https://docs.discord.com/developers/components/reference#legacy-message-component-behavior - else { - mergeTextEvents([{ - msgtype: "m.text", - body, - format: "org.matrix.custom.html", - formatted_body - }], events, false, true) - } + await addTextEvent(body, formatted_body, "m.text") } } diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 1ef83c3..137b63b 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -1,7 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../matrix/utils.test") test("message2event components: pk question mark output", async t => { const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {}) @@ -78,24 +77,3 @@ test("message2event components: pk question mark output", async t => { msgtype: "m.text", }]) }) - -test("message2event components: pk ping message legacy components", async t => { - const events = await messageToEvent(data.message_with_components.pk_ping_components_v1, data.guild.general, {}, { - api: { - async getJoinedMembers() { - return {joined: {}} - }, - getEffectivePower: mockGetEffectivePower() - } - }) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "โญ cadence used `/๐Ÿ”” Ping author`" - + "\nPsst, **Red** (@cadence.worm:), you have been pinged by @cadence.worm:." - + "\n[Jump https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe] ", - format: "org.matrix.custom.html", - formatted_body: "
โญ cadence used /๐Ÿ”” Ping author
Psst, Red (@cadence.worm), you have been pinged by @cadence.worm.
Jump ", - "m.mentions": {} - }]) -}) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 03ca72b..8101a03 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -313,7 +313,7 @@ module.exports = { if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable - const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id) + const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id) if (affected) return // @ts-ignore @@ -335,11 +335,13 @@ module.exports = { if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only! // Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from. - const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id) + const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return - // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (!await retrigger.waitForMessage(data.id)) return + if (!row) { + // Check that the sending-to room exists, and deal with Eventual Consistency(TM) + if (!await retrigger.waitForMessage(data.id)) return + } /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index 66012b4..e3d58c4 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -91,32 +91,40 @@ function registerInteractions() { async function dispatchInteraction(interaction) { const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"] try { - if (interactionId === "Matrix info") { - await matrixInfo.interact(interaction) - } else if (interactionId === "invite") { - await invite.interact(interaction) - } else if (interactionId === "invite_channel") { - await invite.interactButton(interaction) - } else if (interactionId === "Permissions") { - await permissions.interact(interaction) - } else if (interactionId === "permissions_edit") { - await permissions.interactEdit(interaction) - } else if (interactionId === "Responses") { - /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore - const messageInteraction = interaction - if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) { - await pollResponses.interact(messageInteraction) + if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) { + // All we get is custom_id, don't know which context the button was clicked in. + // So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_. + if (interaction.data.custom_id.startsWith("POLL_")) { + await poll.interact(interaction) } else { - await reactions.interact(messageInteraction) + throw new Error(`Unknown message component ${interaction.data.custom_id}`) } - } else if (interactionId === "ping") { - await ping.interact(interaction) - } else if (interactionId === "privacy") { - await privacy.interact(interaction) - } else if (interactionId.startsWith("POLL_")) { - await poll.interact(interaction) } else { - throw new Error(`Unknown interaction ${interactionId}`) + if (interactionId === "Matrix info") { + await matrixInfo.interact(interaction) + } else if (interactionId === "invite") { + await invite.interact(interaction) + } else if (interactionId === "invite_channel") { + await invite.interactButton(interaction) + } else if (interactionId === "Permissions") { + await permissions.interact(interaction) + } else if (interactionId === "permissions_edit") { + await permissions.interactEdit(interaction) + } else if (interactionId === "Responses") { + /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore + const messageInteraction = interaction + if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) { + await pollResponses.interact(messageInteraction) + } else { + await reactions.interact(messageInteraction) + } + } else if (interactionId === "ping") { + await ping.interact(interaction) + } else if (interactionId === "privacy") { + await privacy.interact(interaction) + } else { + throw new Error(`Unknown interaction ${interactionId}`) + } } } catch (e) { let stackLines = null diff --git a/src/discord/utils.js b/src/discord/utils.js index 6fb0b43..0d400f1 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -182,18 +182,6 @@ function filterTo(xs, fn) { return filtered } -/** - * The parameters correspond to the columns of the channel_room table. - * @param {string} rowChannelID thread ID, OR channel ID if there is no thread - * @param {string | null | undefined} rowThreadParent channel ID if there is a thread - */ -function swapThreadID(rowChannelID, rowThreadParent) { - return { - channelID: rowThreadParent ? rowThreadParent : rowChannelID, - threadID: rowThreadParent ? rowChannelID : undefined - } -} - const supportedPlaintextPreviewExtensions = new Set([ "4d", "abnf", @@ -594,5 +582,4 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact module.exports.getPublicUrlForCdn = getPublicUrlForCdn module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage module.exports.filterTo = filterTo -module.exports.swapThreadID = swapThreadID module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions diff --git a/src/m2d/actions/vote.js b/src/m2d/actions/vote.js index 84f8cc7..926b957 100644 --- a/src/m2d/actions/vote.js +++ b/src/m2d/actions/vote.js @@ -1,12 +1,18 @@ // @ts-check const Ty = require("../../types") +const DiscordTypes = require("discord-api-types/v10") +const {Readable} = require("stream") const assert = require("assert").strict +const crypto = require("crypto") const passthrough = require("../../passthrough") -const {sync, db, select} = passthrough +const {sync, discord, db, select} = passthrough -/** @type {import("../../discord/utils")} */ -const dUtils = sync.require("../../discord/utils") +const {reg} = require("../../matrix/read-registration") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** @type {import("../converters/poll-components")} */ const pollComponents = sync.require("../converters/poll-components") /** @type {import("./channel-webhook")} */ @@ -27,11 +33,10 @@ async function updateVote(event) { // If poll was started on Matrix, the Discord version is using components, so we can update that to the current status if (messageRow.source === 0) { - const row = select("channel_room", ["channel_id", "thread_parent"], {room_id: event.room_id}).get() - assert(row) - const {channelID, threadID} = dUtils.swapThreadID(row.channel_id, row.thread_parent) - await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID), threadID) + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + assert(channelID) + await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID)) } } -module.exports.updateVote = updateVote +module.exports.updateVote = updateVote \ No newline at end of file diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 7411a1e..9791ae3 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -122,7 +122,7 @@ block body #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible .s-popover--arrow.s-popover--arrow__tc +add-roles-menu(guild, guild_id) - p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. + p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. h3.mt32.fs-category Features .s-card.d-grid.px0.g16 @@ -191,14 +191,14 @@ block body label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id) +discord(channel, true, "Announcement") else - .s-empty-state.p8 No Discord channels available. + .s-empty-state.p8 All Discord channels are linked. .fl-grow1.s-btn-group.fd-column.w30 each room in unlinkedRooms input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id) label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id) +matrix(room, true) else - .s-empty-state.p8 No Matrix rooms available. + .s-empty-state.p8 All Matrix rooms are linked. input(type="hidden" name="guild_id" value=guild_id) div button.s-btn.s-btn__icon.s-btn__filled#link-button diff --git a/test/data.js b/test/data.js index eab9a63..f3092bc 100644 --- a/test/data.js +++ b/test/data.js @@ -5473,189 +5473,6 @@ module.exports = { content: '-# Original Message ID: 1466556003645657118 ยท ' } ] - }, - pk_ping_components_v1: { - type: 23, - content: "Psst, **Red** (<@772659086046658620>), you have been pinged by <@772659086046658620>.", - mentions: [ - { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - } - ], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2026-03-25T07:07:02.626000+00:00", - edited_timestamp: null, - flags: 0, - components: [ - { - type: 1, - id: 1, - components: [ - { - type: 2, - id: 2, - style: 5, - label: "Jump", - url: "https://discord.com/channels/1160893336324931584/1160894080998461480/1440549403667468320" - } - ] - } - ], - id: "1486260105908457653", - channel_id: "1160894080998461480", - author: { - id: "466378653216014359", - username: "PluralKit", - avatar: "b78ef67a081737a830b60aa47d9ebcd9", - discriminator: "4020", - public_flags: 65536, - flags: 65536, - bot: true, - banner: null, - accent_color: null, - global_name: null, - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: null, - primary_guild: null - }, - pinned: false, - mention_everyone: false, - tts: false, - application_id: "466378653216014359", - interaction: { - id: "1486260103928614932", - type: 2, - name: "๐Ÿ”” Ping author", - user: { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - } - }, - webhook_id: "466378653216014359", - message_reference: { - type: 0, - channel_id: "1160894080998461480", - message_id: "1440549403667468320", - guild_id: "1160893336324931584" - }, - interaction_metadata: { - id: "1486260103928614932", - type: 2, - user: { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - }, - authorizing_integration_owners: { "0": "1160893336324931584" }, - name: "๐Ÿ”” Ping author", - command_type: 3, - target_message_id: "1440549403667468320" - }, - referenced_message: { - type: 0, - content: "test", - mentions: [], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2025-11-19T03:49:01.948000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - id: "1440549403667468320", - channel_id: "1160894080998461480", - author: { - id: "1195662438662680720", - username: "special name", - avatar: "a82347890f2739e5880cd82b8c1a708e", - discriminator: "0000", - public_flags: 0, - flags: 0, - bot: true, - global_name: null, - clan: null, - primary_guild: null - }, - pinned: false, - mention_everyone: false, - tts: false, - application_id: "466378653216014359", - webhook_id: "1195662438662680720" - } } }, message_update: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1662320..8dd71cd 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -95,8 +95,7 @@ WITH a (message_id, channel_id) AS (VALUES ('1381212840957972480', '112760669178241024'), ('1401760355339862066', '112760669178241024'), ('1439351590262800565', '1438284564815548418'), -('1404133238414376971', '112760669178241024'), -('1440549403667468320', '1160894080998461480')) +('1404133238414376971', '112760669178241024')) SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id; INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES @@ -144,8 +143,7 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0), ('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0), ('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1), -('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1), -('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1); +('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),