From e6c3013993e8e365549b1db9a249ed5b544b1e9a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Mar 2026 20:23:43 +1300 Subject: [PATCH 01/34] Make default permission setting functional --- src/d2m/actions/create-room.js | 2 +- src/d2m/actions/register-user.js | 2 +- src/discord/utils.js | 12 ++++++++++- src/m2d/converters/event-to-message.js | 4 ++-- src/matrix/matrix-command-handler.js | 29 +++++++++++++++++++------- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 651eaf4..0f2f903 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -122,7 +122,7 @@ async function channelToKState(channel, guild, di) { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } - const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages) const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index 1bdd6e3..c837ccb 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -154,7 +154,7 @@ function memberToPowerLevel(user, member, guild, channel) { if (!member) return 0 const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, user.id, channel.permission_overwrites) - const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) /* * PL 100 = Administrator = People who can brick the room. RATIONALE: * - Administrator. diff --git a/src/discord/utils.js b/src/discord/utils.js index a51b155..2431246 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -5,7 +5,7 @@ const assert = require("assert").strict const {reg} = require("../matrix/read-registration") -const {db} = require("../passthrough") +const {db, select} = require("../passthrough") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null @@ -58,6 +58,15 @@ function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrite return allowed } +/** + * @param {{id: string, roles: DiscordTypes.APIGuild["roles"]}} guild + * @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channel] + */ +function getDefaultPermissions(guild, channel) { + const defaultRoles = select("role_default", "role_id", {guild_id: guild.id}).pluck().all() + return getPermissions(guild.id, defaultRoles, guild.roles, undefined, channel) +} + /** * Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`. * It is designed like this to avoid developer error with bit manipulations. @@ -174,6 +183,7 @@ function filterTo(xs, fn) { } module.exports.getPermissions = getPermissions +module.exports.getDefaultPermissions = getDefaultPermissions module.exports.hasPermission = hasPermission module.exports.hasSomePermissions = hasSomePermissions module.exports.hasAllPermissions = hasAllPermissions diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 81ad48c..458924d 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -898,7 +898,7 @@ async function eventToMessage(event, guild, channel, di) { let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<" if (!shouldSuppress && guild?.roles) { // Suppress if regular users don't have permission - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel?.permission_overwrites) const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) shouldSuppress = !canEmbedLinks } @@ -961,7 +961,7 @@ async function eventToMessage(event, guild, channel, di) { // Suppress if regular users don't have permission if (!shouldSuppress && guild?.roles) { - const permissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const permissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) shouldSuppress = !canEmbedLinks } diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index d568f7b..b38b4b1 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -105,7 +105,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] let matrixOnlyReason = null const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality." // Check if we can/should upload to Discord, for various causes @@ -115,7 +116,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) const slots = getSlotCount(guild.premium_tier) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (guild.emojis.length >= slots) { matrixOnlyReason = "CAPACITY" } else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...) @@ -240,7 +241,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] if (!guildID) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -251,7 +253,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -270,7 +272,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] if (!guildID) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -281,7 +284,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -290,7 +293,19 @@ const commands = [{ }) } - const invite = await discord.snow.channel.createChannelInvite(channelID) + try { + var invite = await discord.snow.channel.createChannelInvite(channelID) + } catch (e) { + if (e.message === `{"message": "Missing Permissions", "code": 50013}`) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "I don't have permission to create invites to the Discord channel/server." + }) + } else { + throw e + } + } const validHours = Math.ceil(invite.max_age / (60 * 60)) const validUses = ( invite.max_uses === 0 ? "unlimited uses" From 3365023fe39eeda639b06d24ccb67b3b55cef734 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 01:21:38 +1300 Subject: [PATCH 02/34] Sync default roles changes immediately --- src/d2m/discord-packets.js | 2 +- src/web/routes/guild-settings.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 8cf2fde..b1e381e 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -26,6 +26,7 @@ const utils = { client.user = message.d.user client.application = message.d.application console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + interactions.registerInteractions() } else if (message.t === "GUILD_CREATE") { message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web @@ -47,7 +48,6 @@ const utils = { if (listen === "full") { try { - interactions.registerInteractions() await eventDispatcher.checkMissedExpressions(message.d) await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index ae52825..8119f93 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -131,6 +131,9 @@ as.router.post("/api/default-roles", defineEventHandler(async event => { db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID) } + const createSpace = getCreateSpace(event) + await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request + if (getRequestHeader(event, "HX-Request")) { return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID}) } else { From 6716b432bae356f60731df554dfae01e55705a9d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 01:33:29 +1300 Subject: [PATCH 03/34] Wait for response before next click (don't queue) --- src/web/pug/guild.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 74b476a..6dd8601 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -108,7 +108,7 @@ block body h3.mt32.fs-category Default roles .s-card - form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles + form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles input(type="hidden" name="guild_id" value=guild_id) .d-flex.fw-wrap.g4 .s-tag.s-tag__md.fs-body1.s-tag__required @everyone From d3afa728ed0b5ac76ed2946f2519c9b0734eeb7f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 20:53:41 +1300 Subject: [PATCH 04/34] Fix m->d posting embeds even when setting is off --- src/d2m/converters/edit-to-changes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index 4f743eb..61f3290 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -151,9 +151,11 @@ async function editToChanges(message, guild, api) { const messageReallyOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 2 * 60 * 1000 // older than 2 minutes ago // Don't post new generated embeds for messages if the setting was disabled. const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 + // Bots may rely on embeds to send new content, so the rules may be more lax for them. + const botEmbedsApproved = message.author?.bot && !originallyFromMatrix if (messageReallyOld) { eventsToSend = [] // Only allow edits to change and delete, but not send new. - } else if ((messageQuiteOld || !embedsEnabled) && !message.author?.bot) { + } else if ((messageQuiteOld || !embedsEnabled) && !botEmbedsApproved) { eventsToSend = eventsToSend.filter(e => e.msgtype !== "m.notice") // Only send events that aren't embeds. } From f1b111a8a4b871ed15f5293b15bce9f33c9ae4c7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 17 Mar 2026 12:35:42 +1300 Subject: [PATCH 05/34] Refuse to operate on encrypted rooms - Refuse to link to encrypted rooms - Do not show encrypted rooms as link candidates (if server supports) - Reject invites to encrypted rooms with message - Unbridge and leave room if it becomes encrypted --- src/d2m/actions/create-room.js | 5 ++-- src/m2d/event-dispatcher.js | 15 ++++++++++ src/matrix/api.js | 11 +++++--- src/types.d.ts | 9 +++++- src/web/pug/guild.pug | 5 ++++ src/web/routes/guild.js | 3 +- src/web/routes/link.js | 6 ++++ src/web/routes/link.test.js | 50 +++++++++++++++++++++++++++++++--- 8 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 0f2f903..31d3022 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -442,8 +442,9 @@ function syncRoom(channelID) { /** * @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional) * @param {string} guildID + * @param {string} messageBeforeLeave */ -async function unbridgeChannel(channel, guildID) { +async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room was removed from the bridge.") { const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() assert.ok(roomID) const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get() @@ -493,7 +494,7 @@ async function unbridgeChannel(channel, guildID) { // send a notification in the room await api.sendEvent(roomID, "m.room.message", { msgtype: "m.notice", - body: "⚠️ This room was removed from the bridge." + body: `⚠️ ${messageBeforeLeave}` }) // if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 70e293b..085c69c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -413,6 +413,7 @@ async event => { console.error(e) return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`) } + if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.") if (!inviteRoomState?.name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite.`) await api.joinRoom(event.room_id) db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar) @@ -483,6 +484,20 @@ async event => { await roomUpgrade.onTombstone(event, api) })) +sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + // Dramatically unbridge rooms if they become encrypted + if (event.state_key !== "") return + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + if (!channelID) return + const channel = discord.channels.get(channelID) + if (!channel) return + await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.") +})) + module.exports.stringifyErrorStack = stringifyErrorStack module.exports.sendError = sendError module.exports.printError = printError diff --git a/src/matrix/api.js b/src/matrix/api.js index 87bbf0c..f24f4d9 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -172,7 +172,7 @@ function getStateEventOuter(roomID, type, key) { /** * @param {string} roomID * @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event] - * @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?}>} + * @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?, encryption: string?}>} */ async function getInviteState(roomID, event) { function getFromInviteRoomState(strippedState, nskey, key) { @@ -191,7 +191,8 @@ async function getInviteState(roomID, event) { name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"), topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"), avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"), - type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type") + type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"), + encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm") } } @@ -227,7 +228,8 @@ async function getInviteState(roomID, event) { name: getFromInviteRoomState(strippedState, "m.room.name", "name"), topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"), avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"), - type: getFromInviteRoomState(strippedState, "m.room.create", "type") + type: getFromInviteRoomState(strippedState, "m.room.create", "type"), + encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm") } } } catch (e) {} @@ -240,7 +242,8 @@ async function getInviteState(roomID, event) { name: room.name ?? null, topic: room.topic ?? null, avatar: room.avatar_url ?? null, - type: room.room_type ?? null + type: room.room_type ?? null, + encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null } } diff --git a/src/types.d.ts b/src/types.d.ts index a85907d..be037ca 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -157,7 +157,7 @@ export namespace Event { type: string state_key: string sender: string - content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias + content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias | Event.M_Room_Encryption } export type M_Room_Create = { @@ -390,6 +390,12 @@ export namespace Event { body: string replacement_room: string } + + export type M_Room_Encryption = { + algorithm: string + rotation_period_ms?: number + rotation_period_msgs?: number + } } export namespace R { @@ -437,6 +443,7 @@ export namespace R { num_joined_members: number room_id: string room_type?: string + encryption?: string } export type ResolvedRoom = { diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 6dd8601..9791ae3 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -249,6 +249,11 @@ block body ul.my8.ml24 each row in removedLinkedRooms li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name + h3.mt24 Unavailable rooms: Encryption not supported + .s-card.p0 + ul.my8.ml24 + each row in removedEncryptedRooms + li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name h3.mt24 Unavailable rooms: Wrong type .s-card.p0 ul.my8.ml24 diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index a5508c4..70092d5 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -123,13 +123,14 @@ function getChannelRoomsLinks(guild, rooms, roles) { let unlinkedRooms = [...rooms] let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id)) let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type) + let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"]) // https://discord.com/developers/docs/topics/threads#active-archived-threads // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(πŸ”’)?⛓️\]/)) return { linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms, - removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms + removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms } } diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 43995fc..772a19c 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -204,6 +204,12 @@ as.router.post("/api/link", defineEventHandler(async event => { throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`}) } + // Check room is not encrypted + const encryption = await api.getStateEvent(parsedBody.matrix, "m.room.encryption", "").catch(() => null) + if (encryption) { + throw createError({status: 400, message: "Bad Request", data: "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room."}) + } + // Check bridge has PL 100 const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api) if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"}) diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index 70299d5..0182093 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -435,6 +435,47 @@ test("web link room: check that bridge can join room (uses via for join attempt) t.equal(called, 2) }) +test("web link room: check that room is not encrypted", async t => { +let called = 0 + const [error] = await tryToCatch(() => router.test("post", "/api/link", { + sessionData: { + managedGuilds: ["665289423482519565"] + }, + body: { + discord: "665310973967597573", + matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe", + guild_id: "665289423482519565" + }, + api: { + async joinRoom(roomID) { + called++ + return roomID + }, + async *generateFullHierarchy(spaceID) { + called++ + t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe") + yield { + room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe", + children_state: [], + guest_can_join: false, + num_joined_members: 2 + } + /* c8 ignore next */ + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe") + if (type === "m.room.encryption" && key === "") { + return {algorithm: "m.megolm.v1.aes-sha2"} + } + throw new Error("Unknown state event") + } + } + })) + t.equal(error.data, "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room.") + t.equal(called, 3) +}) + test("web link room: check that bridge has PL 100 in target room", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { @@ -465,9 +506,10 @@ test("web link room: check that bridge has PL 100 in target room", async t => { async getStateEvent(roomID, type, key) { called++ t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe") - t.equal(type, "m.room.power_levels") - t.equal(key, "") - return {users_default: 50} + if (type === "m.room.power_levels" && key === "") { + return {users_default: 50} + } + throw new Error("Unknown state event") }, async getStateEventOuter(roomID, type, key) { called++ @@ -489,7 +531,7 @@ test("web link room: check that bridge has PL 100 in target room", async t => { } })) t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room") - t.equal(called, 4) + t.equal(called, 5) }) test("web link room: successfully calls createRoom", async t => { From 711e024caaf3739b39bc1b91040b3db6371a6071 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 17 Mar 2026 14:02:11 +1300 Subject: [PATCH 06/34] Update dependencies --- docs/developer-orientation.md | 15 +- package-lock.json | 631 ++++++++++++++++++++-------------- 2 files changed, 371 insertions(+), 275 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index e17f263..bff64c2 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,15 +89,14 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin # Dependency justification -Total transitive production dependencies: 121 +Total transitive production dependencies: 144 ### πŸ¦• -* (31) better-sqlite3: SQLite is the best database, and this is the best library for it. -* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.) -* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. -* (9) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, self-service, and more. -* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. +* (35) better-sqlite3: SQLite is the best database, and this is the best library for it. +* (29) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. It has libvips prebuilts for each platform. +* (26) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.) +* (9) h3: Web server. OOYE needs this for the web UI, appservice listener, authmedia proxy, and more. ### πŸͺ± @@ -116,12 +115,12 @@ Total transitive production dependencies: 121 * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. * (0) discord-api-types: Bitfields needed at runtime and types needed for development. * (0) domino: DOM implementation that's already pulled in by turndown. -* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. +* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. * (0) get-relative-path: Looks fine. No dependencies. * (1) heatsync: Module hot-reloader that I trust. * (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used. -* (0) mime-type: File extension to mime type mapping that's already pulled in by stream-mime-type. +* (1) mime-types: List of mime type mappings. Needed to serve static files. * (0) prettier-bytes: It does what I want and has no dependencies. * (0) snowtransfer: Discord API library with bring-your-own-caching that I trust. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. diff --git a/package-lock.json b/package-lock.json index 7b27322..10b4668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,50 +54,6 @@ "node": ">=22" } }, - "../extended-errors/enhance-errors": { - "version": "1.0.0", - "extraneous": true, - "license": "UNLICENSED", - "dependencies": { - "ts-expose-internals": "^5.6.3", - "ts-patch": "^3.3.0", - "typescript": "^5.9.3" - }, - "devDependencies": { - "@types/node": "^22.19.1", - "ts-node": "^10.9.2" - } - }, - "../nodejs-stream-type": { - "name": "@cloudrac3r/stream-type", - "version": "1.0.0", - "extraneous": true, - "license": "AGPL-3.0-only", - "devDependencies": { - "@cloudrac3r/tap-dot": "^2.0.3", - "@types/node": "^22.19.15", - "c8": "^11.0.0", - "cross-env": "^10.1.0", - "supertape": "^12.10.4", - "try-to-catch": "^4.0.5" - }, - "engines": { - "node": ">=22.6.0" - } - }, - "../tap-dot": { - "name": "@cloudrac3r/tap-dot", - "version": "2.0.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@cloudrac3r/tap-out": "^3.2.3", - "ansi-colors": "^4.1.3" - }, - "bin": { - "tap-dot": "bin/dot" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -117,12 +73,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -132,9 +88,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -149,6 +105,7 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -184,12 +141,14 @@ "node_modules/@cloudrac3r/giframe": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/giframe/-/giframe-0.4.3.tgz", - "integrity": "sha512-LKuRfdHrhvgPP0heYdlVRecswk/kYaC3fI+X+GQmnkJE36uN1E2dg5l5QdLoukliH7g8S2hgDYk0jsR7sJf8Dg==" + "integrity": "sha512-LKuRfdHrhvgPP0heYdlVRecswk/kYaC3fI+X+GQmnkJE36uN1E2dg5l5QdLoukliH7g8S2hgDYk0jsR7sJf8Dg==", + "license": "MIT" }, "node_modules/@cloudrac3r/html-template-tag": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@cloudrac3r/html-template-tag/-/html-template-tag-5.0.1.tgz", "integrity": "sha512-aH+ZdWJf53E63bVb2FiSnpM81qtF2ZNVbrXjrHcfnofyV/GTYJjZHnmPYC2FgXxJ+I8+bZP3DiwYzj7zXYoekw==", + "license": "MIT", "dependencies": { "html-es6cape": "^2.0.0" } @@ -220,6 +179,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/pngjs/-/pngjs-7.0.3.tgz", "integrity": "sha512-Aghuja9XAIqBPmY2jk8dKZSyK90gImxA4hJeEYYAWkZO34bf+zliUAvGBygoBZA0EgXSmfxewVchL+9y3w+rDw==", + "license": "MIT", "engines": { "node": ">=14.19.0" } @@ -228,6 +188,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug/-/pug-4.0.4.tgz", "integrity": "sha512-RZhxM/WfSHT0n39URlwDdugBfGfwEWmr+w+mCyiT9jaiqCjeZPpXkps/cWLA1XRLo7fzq0+9THtGzVKXS487/A==", + "license": "MIT", "dependencies": { "@cloudrac3r/pug-code-gen": "3.0.5", "@cloudrac3r/pug-lexer": "5.0.3", @@ -242,6 +203,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-code-gen/-/pug-code-gen-3.0.5.tgz", "integrity": "sha512-dKKpy3i9YlVa3lBgu5Jds513c7AtzmmsR2/lGhY2NOODSpIiTcbWLw1obA9YEmmH1tAJny+J6ePYN1N1RgjjQA==", + "license": "MIT", "dependencies": { "constantinople": "^4.0.1", "doctypes": "^1.1.0", @@ -257,6 +219,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-lexer/-/pug-lexer-5.0.3.tgz", "integrity": "sha512-ym4g4q+l9IC2H1wXCDnF79AQZ48xtxO675JOT316e17W2wHWtgRccXpT6DkBAaRDZycmkGzSxID1S15T2lZj+g==", + "license": "MIT", "dependencies": { "character-parser": "^4.0.0", "is-expression": "^4.0.0", @@ -307,14 +270,15 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz", "integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==", + "license": "MIT", "dependencies": { "domino": "^2.1.6" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, "dependencies": { @@ -324,12 +288,13 @@ "node_modules/@hotwired/stimulus": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", - "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==" + "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==", + "license": "MIT" }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" @@ -802,9 +767,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { @@ -835,10 +800,11 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -865,6 +831,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -889,6 +856,7 @@ "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-2.0.0.tgz", "integrity": "sha512-/tl1XiBog6XMb1T9kalFerYU86sYsl6EtrlvGI5RVtlHOQdEEJAIPRxmX4vnKG3uoY5aVEkJOWzbPM5tsncmFQ==", "dev": true, + "license": "MIT", "dependencies": { "fastest-levenshtein": "^1.0.12", "just-kebab-case": "^4.2.0" @@ -898,9 +866,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.47", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", - "integrity": "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -976,19 +944,6 @@ "node": ">=22" } }, - "node_modules/@supertape/formatter-progress-bar/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@supertape/formatter-short": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-3.0.0.tgz", @@ -1027,19 +982,6 @@ "node": ">=22" } }, - "node_modules/@supertape/formatter-time/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@supertape/operator-stub": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-4.0.0.tgz", @@ -1054,10 +996,11 @@ } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.19.15", @@ -1070,9 +1013,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1082,6 +1025,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1093,6 +1037,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1107,15 +1052,13 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1126,19 +1069,22 @@ "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", "dev": true, + "license": "MIT", "dependencies": { "printable-characters": "^1.0.42" } }, "node_modules/assert-never": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", - "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT" }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", "dependencies": { "@babel/types": "^7.9.6" }, @@ -1153,13 +1099,13 @@ "license": "MIT" }, "node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/base64-js": { @@ -1179,12 +1125,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "12.6.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", - "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1199,6 +1146,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" } @@ -1207,13 +1155,27 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/bl/node_modules/buffer": { + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -1231,24 +1193,12 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, - "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/c8": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", @@ -1284,16 +1234,13 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1302,17 +1249,20 @@ "node_modules/character-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-4.0.0.tgz", - "integrity": "sha512-jWburCrDpd+aPopB7esjh/gLyZoHZS4C2xwwJlkTPyhhJdXG+FCG0P4qCOInvOd9yhiuAEJYlZsUMQ0JSK4ykw==" + "integrity": "sha512-jWburCrDpd+aPopB7esjh/gLyZoHZS4C2xwwJlkTPyhhJdXG+FCG0P4qCOInvOd9yhiuAEJYlZsUMQ0JSK4ykw==", + "license": "MIT" }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" }, "node_modules/chunk-text": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/chunk-text/-/chunk-text-2.0.1.tgz", "integrity": "sha512-ER6TSpe2DT4wjOVOKJ3FFAYv7wE77HA/Ztz88Peiv3lq/2oVMsItYJJsVVI0xNZM8cdImOOTNqlw+LQz7gYdJg==", + "license": "MIT", "dependencies": { "runes": "^0.4.3" }, @@ -1322,9 +1272,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -1355,6 +1305,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1382,6 +1333,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1393,22 +1345,25 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.6.0", "@babel/types": "^7.6.1" } }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, "node_modules/cookie-es": { "version": "1.2.2", @@ -1421,6 +1376,7 @@ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -1439,6 +1395,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1467,12 +1424,14 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -1487,6 +1446,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -1494,7 +1454,8 @@ "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" }, "node_modules/destr": { "version": "2.0.5", @@ -1512,9 +1473,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.41", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.41.tgz", - "integrity": "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ==", + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1523,23 +1484,27 @@ "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" }, "node_modules/domino": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", - "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", + "license": "BSD-2-Clause" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -1548,6 +1513,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -1560,6 +1526,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz", "integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1568,10 +1535,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1580,6 +1548,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" } @@ -1589,6 +1558,7 @@ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } @@ -1596,13 +1566,15 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1641,7 +1613,8 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" }, "node_modules/fullstore": { "version": "4.0.0", @@ -1658,6 +1631,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1667,6 +1641,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1674,13 +1649,15 @@ "node_modules/get-relative-path": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-relative-path/-/get-relative-path-1.0.2.tgz", - "integrity": "sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==" + "integrity": "sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==", + "license": "MIT" }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", "dev": true, + "license": "Unlicense", "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -1689,7 +1666,8 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" }, "node_modules/glob": { "version": "13.0.6", @@ -1710,9 +1688,9 @@ } }, "node_modules/h3": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", - "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", + "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -1731,15 +1709,17 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1762,13 +1742,15 @@ "node_modules/html-es6cape": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-es6cape/-/html-es6cape-2.0.2.tgz", - "integrity": "sha512-utzhH8rq2VABdW1LsPdv5tmxeMNOtP83If0jKCa79xPBgLWfcMvdf9K+EZoxJ5P7KioCxTs6WBnSDWLQHJ2lWA==" + "integrity": "sha512-utzhH8rq2VABdW1LsPdv5tmxeMNOtP83If0jKCa79xPBgLWfcMvdf9K+EZoxJ5P7KioCxTs6WBnSDWLQHJ2lWA==", + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/htmx.org": { "version": "2.0.8", @@ -1793,17 +1775,20 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" }, "node_modules/iron-webcrypto": { "version": "1.2.1", @@ -1815,12 +1800,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1830,6 +1819,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", "dependencies": { "acorn": "^7.1.1", "object-assign": "^4.1.1" @@ -1840,6 +1830,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1848,13 +1839,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -1864,6 +1857,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -1874,10 +1868,11 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -1887,30 +1882,64 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.4.4.tgz", - "integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==", + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", "dev": true, "license": "MIT" }, @@ -1918,7 +1947,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/just-snake-case": { "version": "3.2.0", @@ -1932,6 +1962,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -1943,9 +1974,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -1956,6 +1987,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1970,6 +2002,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1990,6 +2023,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2017,6 +2051,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2034,17 +2069,20 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" }, "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" }, "node_modules/node-abi": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", - "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -2062,6 +2100,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2070,6 +2109,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -2079,6 +2119,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2094,6 +2135,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -2109,6 +2151,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2118,6 +2161,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2126,7 +2170,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.2", @@ -2146,16 +2191,18 @@ } }, "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -2173,12 +2220,13 @@ "node_modules/prettier-bytes": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", - "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==" + "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==", + "license": "ISC" }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2190,29 +2238,18 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/pug-attrs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", "dependencies": { "constantinople": "^4.0.1", "js-stringify": "^1.0.2", @@ -2222,12 +2259,14 @@ "node_modules/pug-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" }, "node_modules/pug-linker": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0", "pug-walk": "^2.0.0" @@ -2237,6 +2276,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", "dependencies": { "object-assign": "^4.1.1", "pug-walk": "^2.0.0" @@ -2246,6 +2286,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0", "token-stream": "1.0.0" @@ -2254,12 +2295,14 @@ "node_modules/pug-runtime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" }, "node_modules/pug-strip-comments": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0" } @@ -2267,12 +2310,14 @@ "node_modules/pug-walk": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2288,6 +2333,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -2331,23 +2377,28 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2356,6 +2407,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/runes/-/runes-0.4.3.tgz", "integrity": "sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -2377,12 +2429,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2440,6 +2493,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2452,6 +2506,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2461,6 +2516,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2485,7 +2541,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/simple-get": { "version": "4.0.1", @@ -2505,6 +2562,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -2537,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2552,10 +2611,11 @@ } }, "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", "dev": true, + "license": "Unlicense", "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -2565,6 +2625,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -2574,6 +2635,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2599,14 +2661,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/supertape": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.7.0.tgz", - "integrity": "sha512-5PXh6HsfEJKkC0SMhPNkH35o8Okj8xlVvoju9R0aCohzsK+GEufeYZ1IPhRBRQ2DBLXdMZHVF6N/4pAefxNuAA==", + "version": "12.10.5", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.10.5.tgz", + "integrity": "sha512-1Px+6mhFaqcht3p4tkf3o4G8lbBazvx4pgFngm4vGwWipYm3fykm6SJ4ThXobiaNsptz53CDWA2q4B/2KtmA4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2658,6 +2721,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2670,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2693,6 +2758,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -2729,12 +2795,13 @@ "node_modules/token-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" }, "node_modules/try-catch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-4.0.7.tgz", - "integrity": "sha512-gkBWUxbiN4T4PsO8KhoQYWzUPN6e0/h12H9H3YhcfPbwaN8b84fy8cFqL4rWTiPh7qHPFaEfklr6OkVxYRW0Gg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-4.0.9.tgz", + "integrity": "sha512-tEWGmsfqZ9NBzvDOGbACxu+AaXajM6+RtmIM6wCIkFD6lCa3/UvjNuWjCRoOjn8qTKuZlQmrMh8vSTBMQcceew==", "dev": true, "license": "MIT", "engines": { @@ -2761,6 +2828,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -2796,17 +2864,19 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" @@ -2816,6 +2886,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2825,6 +2896,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2839,6 +2911,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.9.6", "@babel/types": "^7.9.6", @@ -2854,6 +2927,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2866,27 +2940,47 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", - "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==" + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -2896,6 +2990,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2914,6 +3009,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -2923,6 +3019,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, From 5b04b5d71231b89f2320f97f05fc0968ca28ba29 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:33:50 +1300 Subject: [PATCH 07/34] Reformat /plu/ral emulated replies --- src/d2m/converters/message-to-event.js | 27 ++++++++++++++++--------- src/db/migrations/0035-role-default.sql | 2 +- src/discord/utils.js | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index adc56e6..3f598f2 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -35,10 +35,10 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() - const interaction = message.interaction_metadata || message.interaction + const interactionMetadata = message.interaction_metadata const username = message.mentions?.find(ment => ment.id === node.id)?.username || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username - || (interaction?.user.id === node.id ? interaction.user.username : null) + || (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null) || (message.author?.id === node.id ? message.author.username : null) || "unknown-user" if (mxid && useHTML) { @@ -357,9 +357,8 @@ async function messageToEvent(message, guild, options = {}, di) { }] } - const interaction = message.interaction_metadata || message.interaction - const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction - const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) + let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction + let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) /** @type {{room?: boolean, user_ids?: string[]}} @@ -400,6 +399,16 @@ async function messageToEvent(message, guild, options = {}, di) { } else if (message.referenced_message) { repliedToUnknownEvent = true } + } else if (message.type === DiscordTypes.MessageType.ContextMenuCommand && message.interaction && message.message_reference?.message_id) { + // It could be a /plu/ral emulated reply + if (message.interaction.name.startsWith("Reply ") && message.content.startsWith("-# [β†ͺ](")) { + const row = await getHistoricalEventRow(message.message_reference?.message_id) + if (row && "event_id" in row) { + repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id}) + message.content = message.content.replace(/^.*\n/, "") + isInteraction = false // declutter + } + } } else if (dUtils.isWebhookMessage(message) && message.embeds[0]?.author?.name?.endsWith("↩️")) { // It could be a PluralKit emulated reply, let's see if it has a message link const isEmulatedReplyToText = message.embeds[0].description?.startsWith("**[Reply to:]") @@ -685,8 +694,8 @@ async function messageToEvent(message, guild, options = {}, di) { } } - if (isInteraction && !isThinkingInteraction && events.length === 0) { - const formattedInteraction = getFormattedInteraction(interaction, false) + if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) { + const formattedInteraction = getFormattedInteraction(message.interaction, false) body = `${formattedInteraction.body}\n${body}` html = `${formattedInteraction.html}${html}` } @@ -782,8 +791,8 @@ async function messageToEvent(message, guild, options = {}, di) { events.push(...forwardedEvents) } - if (isThinkingInteraction) { - const formattedInteraction = getFormattedInteraction(interaction, true) + if (isInteraction && isThinkingInteraction && message.interaction) { + const formattedInteraction = getFormattedInteraction(message.interaction, true) await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice") } diff --git a/src/db/migrations/0035-role-default.sql b/src/db/migrations/0035-role-default.sql index 6c44e7e..a5ce62d 100644 --- a/src/db/migrations/0035-role-default.sql +++ b/src/db/migrations/0035-role-default.sql @@ -4,6 +4,6 @@ CREATE TABLE "role_default" ( "guild_id" TEXT NOT NULL, "role_id" TEXT NOT NULL, PRIMARY KEY ("guild_id", "role_id") -); +) WITHOUT ROWID; COMMIT; diff --git a/src/discord/utils.js b/src/discord/utils.js index 2431246..aed7068 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -114,7 +114,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) { * @param {DiscordTypes.APIMessage} message */ function isWebhookMessage(message) { - return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand + return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand } /** From f8896dce7f6193caf98f36d83077ea680f1a45c3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:34:19 +1300 Subject: [PATCH 08/34] Type fixes in set-presence.js --- src/d2m/actions/set-presence.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/d2m/actions/set-presence.js b/src/d2m/actions/set-presence.js index f26668f..0a31038 100644 --- a/src/d2m/actions/set-presence.js +++ b/src/d2m/actions/set-presence.js @@ -1,5 +1,7 @@ // @ts-check +const assert = require("assert").strict + const passthrough = require("../../passthrough") const {sync, select} = passthrough /** @type {import("../../matrix/api")} */ @@ -26,7 +28,7 @@ const presenceLoopInterval = 28e3 // Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence const guildPresenceSetting = new class { - /** @private @type {Set} */ guilds + /** @private @type {Set} */ guilds = new Set() constructor() { this.update() } @@ -40,7 +42,7 @@ const guildPresenceSetting = new class { class Presence extends sync.reloadClassMethods(() => Presence) { /** @type {string} */ userID - /** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string}} */ data + /** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string} | undefined} */ data /** @private @type {?string | undefined} */ mxid /** @private @type {number} */ delay = Math.random() @@ -66,6 +68,7 @@ class Presence extends sync.reloadClassMethods(() => Presence) { // I haven't tried, but I assume Synapse explodes if you try to update too many presences at the same time. // This random delay will space them out over the whole 28 second cycle. setTimeout(() => { + assert(this.data) api.setPresence(this.data, mxid).catch(() => {}) }, this.delay * presenceLoopInterval).unref() } From d2557f73bb4ff4f15f65900c15156e3335fe519a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:35:53 +1300 Subject: [PATCH 09/34] Let sims rejoin after being unbanned The sim_member cache was getting stuck, so OOYE thought it was already in the room when it actually wasn't. --- src/m2d/event-dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 085c69c..c11b696 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -423,7 +423,10 @@ async event => { if (event.content.membership === "leave" || event.content.membership === "ban") { // Member is gone + // if Matrix member, data was cached in member_cache db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) + // if Discord member (so kicked/banned by Matrix user), data was cached in sim_member + db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) // Unregister room's use as a direct chat and/or an invite target if the bot itself left if (event.state_key === utils.bot) { From 876d91fbf487ca327dd8fdd6f3913ebd701b4d93 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 14:30:10 +1300 Subject: [PATCH 10/34] Remove sims when the Discord user leaves --- src/d2m/actions/register-user.js | 10 ++++- src/d2m/actions/remove-member.js | 26 +++++++++++ src/d2m/actions/send-message.js | 2 +- .../message-to-event.test.components.js | 2 +- src/d2m/converters/remove-member-mxids.js | 38 ++++++++++++++++ .../converters/remove-member-mxids.test.js | 43 +++++++++++++++++++ src/d2m/discord-packets.js | 3 +- src/d2m/event-dispatcher.js | 35 +++++++++++++++ src/db/migrations/0036-app-user-install.sql | 10 +++++ src/db/orm-defs.d.ts | 6 +++ test/ooye-test-data.sql | 17 +++++++- test/test.js | 1 + 12 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src/d2m/actions/remove-member.js create mode 100644 src/d2m/converters/remove-member-mxids.js create mode 100644 src/d2m/converters/remove-member-mxids.test.js create mode 100644 src/db/migrations/0036-app-user-install.sql diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index c837ccb..d475e54 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -206,14 +206,16 @@ function _hashProfileContent(content, powerLevel) { * 3. Calculate the power level the user should get based on their Discord permissions * 4. Compare against the previously known state content, which is helpfully stored in the database * 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time + * 6. If the sim is for a user-installed app, check which user it was added by * @param {DiscordTypes.APIUser} user * @param {Omit | undefined} member * @param {DiscordTypes.APIGuildChannel} channel * @param {DiscordTypes.APIGuild} guild * @param {string} roomID + * @param {DiscordTypes.APIMessageInteractionMetadata} [interactionMetadata] * @returns {Promise} mxid of the updated sim */ -async function syncUser(user, member, channel, guild, roomID) { +async function syncUser(user, member, channel, guild, roomID, interactionMetadata) { const mxid = await ensureSimJoined(user, roomID) const content = await memberToStateContent(user, member, guild.id) const powerLevel = memberToPowerLevel(user, member, guild, channel) @@ -222,6 +224,12 @@ async function syncUser(user, member, channel, guild, roomID) { allowOverwrite: !!member, globalProfile: await userToGlobalProfile(user) }) + + const appInstalledByUser = user.bot && interactionMetadata?.authorizing_integration_owners?.[DiscordTypes.ApplicationIntegrationType.UserInstall] + if (appInstalledByUser) { + db.prepare("INSERT OR IGNORE INTO app_user_install (app_bot_id, user_id, guild_id) VALUES (?, ?, ?)").run(user.id, appInstalledByUser, guild.id) + } + return mxid } diff --git a/src/d2m/actions/remove-member.js b/src/d2m/actions/remove-member.js new file mode 100644 index 0000000..4dbd5a6 --- /dev/null +++ b/src/d2m/actions/remove-member.js @@ -0,0 +1,26 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const {sync, db, select, from} = passthrough +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../converters/remove-member-mxids")} */ +const removeMemberMxids = sync.require("../converters/remove-member-mxids") + +/** + * @param {string} userID discord user ID that left + * @param {string} guildID discord guild ID that they left + */ +async function removeMember(userID, guildID) { + const {userAppDeletions, membership} = removeMemberMxids.removeMemberMxids(userID, guildID) + db.transaction(() => { + for (const d of userAppDeletions) { + db.prepare("DELETE FROM app_user_install WHERE guild_id = ? and user_id = ?").run(guildID, d) + } + })() + for (const m of membership) { + await api.leaveRoom(m.room_id, m.mxid) + } +} + +module.exports.removeMember = removeMember diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index eb919bb..8550d43 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -51,7 +51,7 @@ async function sendMessage(message, channel, guild, row) { if (message.author.id === discord.application.id) { // no need to sync the bot's own user } else { - senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID) + senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID, message.interaction_metadata) } } diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 7d875a6..137b63b 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -65,7 +65,7 @@ test("message2event components: pk question mark output", async t => { + "
" + "

System: INX (xffgnx)" + "
Member: Lillith (pphhoh)" - + "
Sent by: infinidoge1337 (@unknown-user:)" + + "
Sent by: infinidoge1337 (@unknown-user)" + "

Account Roles (7)" + "
Β§b, !, β€Ό, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping

" + `πŸ–ΌοΈ https://files.inx.moe/p/cdn/lillith.webp` diff --git a/src/d2m/converters/remove-member-mxids.js b/src/d2m/converters/remove-member-mxids.js new file mode 100644 index 0000000..de26662 --- /dev/null +++ b/src/d2m/converters/remove-member-mxids.js @@ -0,0 +1,38 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const {db, select, from} = passthrough + +/** + * @param {string} userID discord user ID that left + * @param {string} guildID discord guild ID that they left + */ +function removeMemberMxids(userID, guildID) { + // Get sims for user and remove + let membership = from("sim").join("sim_member", "mxid").join("channel_room", "room_id") + .select("room_id", "mxid").where({user_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all() + membership = membership.concat(from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").join("channel_room", "room_id") + .select("room_id", "mxid").where({proxy_owner_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all()) + + // Get user installed apps and remove + /** @type {string[]} */ + let userAppDeletions = [] + // 1. Select apps that have 1 user remaining + /** @type {Set} */ + const appsWithOneUser = new Set(db.prepare("SELECT app_bot_id FROM app_user_install WHERE guild_id = ? GROUP BY app_bot_id HAVING count(*) = 1").pluck().all(guildID)) + // 2. Select apps installed by this user + const appsFromThisUser = new Set(select("app_user_install", "app_bot_id", {guild_id: guildID, user_id: userID}).pluck().all()) + if (appsFromThisUser.size) userAppDeletions.push(userID) + // Then remove user installed apps if this was the last user with them + const appsToRemove = appsWithOneUser.intersection(appsFromThisUser) + for (const botID of appsToRemove) { + // Remove sims for user installed app + const appRemoval = removeMemberMxids(botID, guildID) + membership = membership.concat(appRemoval.membership) + userAppDeletions = userAppDeletions.concat(appRemoval.userAppDeletions) + } + + return {membership, userAppDeletions} +} + +module.exports.removeMemberMxids = removeMemberMxids diff --git a/src/d2m/converters/remove-member-mxids.test.js b/src/d2m/converters/remove-member-mxids.test.js new file mode 100644 index 0000000..a880dff --- /dev/null +++ b/src/d2m/converters/remove-member-mxids.test.js @@ -0,0 +1,43 @@ +// @ts-check + +const {test} = require("supertape") +const {removeMemberMxids} = require("./remove-member-mxids") + +test("remove member mxids: would remove mxid for all rooms in this server", t => { + t.deepEqual(removeMemberMxids("772659086046658620", "112760669178241024"), { + userAppDeletions: [], + membership: [{ + mxid: "@_ooye_cadence:cadence.moe", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, { + mxid: "@_ooye_cadence:cadence.moe", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }] + }) +}) + +test("remove member mxids: removes sims too", t => { + t.deepEqual(removeMemberMxids("196188877885538304", "112760669178241024"), { + userAppDeletions: [], + membership: [{ + mxid: '@_ooye_ampflower:cadence.moe', + room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe' + }, { + mxid: '@_ooye__pk_zoego:cadence.moe', + room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe' + }] + }) +}) + +test("remove member mxids: removes apps too", t => { + t.deepEqual(removeMemberMxids("197126718400626689", "66192955777486848"), { + userAppDeletions: ["197126718400626689"], + membership: [{ + mxid: '@_ooye_infinidoge1337:cadence.moe', + room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe' + }, { + mxid: '@_ooye_evil_lillith_sheher:cadence.moe', + room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe' + }] + }) +}) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index b1e381e..afea9ea 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -49,8 +49,9 @@ const utils = { if (listen === "full") { try { await eventDispatcher.checkMissedExpressions(message.d) - await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) + await eventDispatcher.checkMissedPins(client, message.d) + await eventDispatcher.checkMissedLeaves(client, message.d) } catch (e) { console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") console.error(e) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 01bbc67..7d156a0 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -32,6 +32,8 @@ const speedbump = sync.require("./actions/speedbump") const retrigger = sync.require("./actions/retrigger") /** @type {import("./actions/set-presence")} */ const setPresence = sync.require("./actions/set-presence") +/** @type {import("./actions/remove-member")} */ +const removeMember = sync.require("./actions/remove-member") /** @type {import("./actions/poll-vote")} */ const vote = sync.require("./actions/poll-vote") /** @type {import("../m2d/event-dispatcher")} */ @@ -172,6 +174,31 @@ module.exports = { await createSpace.syncSpaceExpressions(data, true) }, + /** + * When logging back in, check if any members left while we were gone. + * Do this by getting the member list from Discord and seeing who we still have locally that isn't there in the response. + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild + */ + async checkMissedLeaves(client, guild) { + const maxLimit = 1000 + if (guild.member_count >= maxLimit) return // too large to want to scan + const discordMembers = await client.snow.guild.getGuildMembers(guild.id, {limit: maxLimit}) + if (discordMembers.length >= maxLimit) return // response was maxed out, there are guild members that weren't listed, can't act safely + const discordMembersSet = new Set(discordMembers.map(m => m.user.id)) + // no indexes on this one but I'll cope + const membersAddedOnMatrix = new Set(from("sim").join("sim_member", "mxid").join("channel_room", "room_id") + .pluck("user_id").selectUnsafe("DISTINCT user_id").where({guild_id: guild.id}).and("AND user_id not like '%-%' and user_id not like '%\\_%' escape '\\'").all()) + const userInstalledAppIDs = new Set(from("app_user_install").pluck("app_bot_id").selectUnsafe("DISTINCT app_bot_id").where({guild_id: guild.id}).all()) + // loop over members added on matrix and if the member does not exist on discord-side then they should be removed + for (const userID of membersAddedOnMatrix) { + if (userInstalledAppIDs.has(userID)) continue // skip user installed apps here since they're never true members - they'll be removed by removeMember when the associated user is removed + if (!discordMembersSet.has(userID)) { + await removeMember.removeMember(userID, guild.id) + } + } + }, + /** * Announces to the parent room that the thread room has been created. * See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement" @@ -211,6 +238,14 @@ module.exports = { } }, + /** + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data + */ + async GUILD_MEMBER_REMOVE(client, data) { + await removeMember.removeMember(data.user.id, data.guild_id) + }, + /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread diff --git a/src/db/migrations/0036-app-user-install.sql b/src/db/migrations/0036-app-user-install.sql new file mode 100644 index 0000000..087a0ac --- /dev/null +++ b/src/db/migrations/0036-app-user-install.sql @@ -0,0 +1,10 @@ +BEGIN TRANSACTION; + +CREATE TABLE "app_user_install" ( + "guild_id" TEXT NOT NULL, + "app_bot_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + PRIMARY KEY ("guild_id", "app_bot_id", "user_id") +) WITHOUT ROWID; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index f6628f2..d95bfc3 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -1,4 +1,10 @@ export type Models = { + app_user_install: { + guild_id: string + app_bot_id: string + user_id: string + } + auto_emoji: { name: string emoji_id: string diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1dd9dfe..07f8c24 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -38,15 +38,28 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'), ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'), ('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'), -('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'); +('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'), +('196188877885538304', 'ampflower', 'ampflower', '@_ooye_ampflower:cadence.moe'), +('1458668878107381800', 'Evil Lillith (she/her)', 'evil_lillith_sheher', '@_ooye_evil_lillith_sheher:cadence.moe'), +('197126718400626689', 'infinidoge1337', 'infinidoge1337', '@_ooye_infinidoge1337:cadence.moe'); + INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL), -('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); +('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL), +('@_ooye_cadence:cadence.moe', '!kLRqKKUQXcibIMtOpl:cadence.moe', NULL), +('@_ooye_cadence:cadence.moe', '!fGgIymcYWOqjbSRUdV:cadence.moe', NULL), +('@_ooye_ampflower:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL), +('@_ooye__pk_zoego:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL), +('@_ooye_infinidoge1337:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL), +('@_ooye_evil_lillith_sheher:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); +INSERT INTO app_user_install (guild_id, app_bot_id, user_id) VALUES +('66192955777486848', '1458668878107381800', '197126718400626689'); + INSERT INTO message_room (message_id, historical_room_index) WITH a (message_id, channel_id) AS (VALUES ('1106366167788044450', '122155380120748034'), diff --git a/test/test.js b/test/test.js index da6bcba..4cd9627 100644 --- a/test/test.js +++ b/test/test.js @@ -152,6 +152,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/converters/message-to-event.test.embeds") require("../src/d2m/converters/message-to-event.test.pk") require("../src/d2m/converters/pins-to-list.test") + require("../src/d2m/converters/remove-member-mxids.test") require("../src/d2m/converters/remove-reaction.test") require("../src/d2m/converters/thread-to-announcement.test") require("../src/d2m/converters/user-to-mxid.test") From e8d9a5e4ae0078e664365283ea2ad0f60c8b7a81 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 14:30:19 +1300 Subject: [PATCH 11/34] Script to remove uncached bridged users --- scripts/remove-uncached-bridged-users.js | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 scripts/remove-uncached-bridged-users.js diff --git a/scripts/remove-uncached-bridged-users.js b/scripts/remove-uncached-bridged-users.js new file mode 100644 index 0000000..b3ceb8a --- /dev/null +++ b/scripts/remove-uncached-bridged-users.js @@ -0,0 +1,36 @@ +// @ts-check + +const HeatSync = require("heatsync") +const sync = new HeatSync({watchFS: false}) + +const sqlite = require("better-sqlite3") +const db = new sqlite("ooye.db", {fileMustExist: true}) + +const passthrough = require("../src/passthrough") +Object.assign(passthrough, {db, sync}) + +const api = require("../src/matrix/api") +const utils = require("../src/matrix/utils") +const {reg} = require("../src/matrix/read-registration") + +const rooms = db.prepare("select room_id, name, nick from channel_room").all() + +;(async () => { + // Search for members starting with @_ooye_ and kick them if they are not in sim_member cache + for (const room of rooms) { + try { + const members = await api.getJoinedMembers(room.room_id) + for (const mxid of Object.keys(members.joined)) { + if (!mxid.startsWith("@" + reg.sender_localpart) && utils.eventSenderIsFromDiscord(mxid) && !db.prepare("select mxid from sim_member where mxid = ? and room_id = ?").get(mxid, room.room_id)) { + await api.leaveRoom(room.room_id, mxid) + } + } + } catch (e) { + if (e.message.includes("Appservice not in room")) { + // ok + } else { + throw e + } + } + } +})() From 8c87d93011d8176ef019ae24ecce213197f5c2bb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:17:40 +1300 Subject: [PATCH 12/34] Remove member repetition bugfixes --- src/d2m/actions/remove-member.js | 13 ++++++++++++- src/d2m/event-dispatcher.js | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/d2m/actions/remove-member.js b/src/d2m/actions/remove-member.js index 4dbd5a6..56ac750 100644 --- a/src/d2m/actions/remove-member.js +++ b/src/d2m/actions/remove-member.js @@ -19,7 +19,18 @@ async function removeMember(userID, guildID) { } })() for (const m of membership) { - await api.leaveRoom(m.room_id, m.mxid) + try { + await api.leaveRoom(m.room_id, m.mxid) + } catch (e) { + if (String(e).includes("not in room")) { + // no further action needed + } else { + throw e + } + } + // Update cache to say that the member isn't in the room any more + // You'd think this would happen automatically when the leave event arrives at Matrix's event dispatcher, but that isn't 100% reliable. + db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?").run(m.room_id, m.mxid) } } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 7d156a0..c86cc13 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -125,6 +125,7 @@ module.exports = { // Send in order for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) { const message = messages[i] + if (message.type === DiscordTypes.MessageType.UserJoin) continue // since join announcements don't become events, it would be a repetition to act on them during backfill if (!members.has(message.author.id)) members.set(message.author.id, await client.snow.guild.getGuildMember(guild.id, message.author.id).catch(() => undefined)) await module.exports.MESSAGE_CREATE(client, { From 9dbd871e0bafba5c077d40fa5098b0caf476319a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:42:51 +1300 Subject: [PATCH 13/34] Defuse mentions in m->d reply if client says so --- src/m2d/converters/event-to-message.js | 33 +++++- src/m2d/converters/event-to-message.test.js | 124 +++++++++++++++++++- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 458924d..a49dd1c 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -471,7 +471,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { // @ts-ignore - typescript doesn't know about indices yet content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]), ensureJoined: [], - allowedMentionsParse: ["everyone"] + allowedMentionsParse: ["everyone"], + allowedMentionsUsers: [] } } } else if (writtenMentionMatch[1].length < 40) { // the API supports up to 100 characters, but really if you're searching more than 40, something messed up @@ -482,7 +483,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { // @ts-ignore - typescript doesn't know about indices yet content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), ensureJoined: [results[0].user], - allowedMentionsParse: [] + allowedMentionsParse: [], + allowedMentionsUsers: [results[0].user.id] } } } @@ -544,6 +546,7 @@ async function eventToMessage(event, guild, channel, di) { let displayName = event.sender let avatarURL = undefined const allowedMentionsParse = ["users", "roles"] + const allowedMentionsUsers = [] /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" @@ -986,16 +989,34 @@ async function eventToMessage(event, guild, channel, di) { } } + // Complete content content = displayNameRunoff + replyLine + content - // Split into 2000 character chunks const chunks = chunk(content, 2000) + + // If m.mentions is specified and valid, overwrite allowedMentionsParse with a converted m.mentions + let allowed_mentions = {parse: allowedMentionsParse} + if (event.content["m.mentions"]) { + // Combine requested mentions with detected written mentions to get the full list + if (Array.isArray(event.content["m.mentions"].user_ids)) { + for (const mxid of event.content["m.mentions"].user_ids) { + const user_id = select("sim", "user_id", {mxid}).pluck().get() + if (!user_id) continue + allowedMentionsUsers.push( + select("sim_proxy", "proxy_owner_id", {user_id}).pluck().get() || user_id + ) + } + } + // Specific mentions were requested, so do not parse users + allowed_mentions.parse = allowed_mentions.parse.filter(x => x !== "users") + allowed_mentions.users = allowedMentionsUsers + } + + // Assemble chunks into Discord messages content /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */ const messages = chunks.map(content => ({ content, - allowed_mentions: { - parse: allowedMentionsParse - }, + allowed_mentions, username: displayNameShortened, avatar_url: avatarURL })) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index aa426cd..b057b63 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -266,7 +266,8 @@ test("event2message: markdown in link text does not attempt to be escaped becaus content: "hey [@mario sports mix [she/her]](), is it possible to listen on a unix socket?", avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -547,7 +548,8 @@ test("event2message: links don't have angle brackets added by accident", async t content: "Wanted to automate WGβ†’AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp", avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1296,7 +1298,8 @@ test("event2message: lists have appropriate line breaks", async t => { content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`, avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1337,7 +1340,8 @@ test("event2message: ordered list start attribute works", async t => { content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`, avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1463,6 +1467,118 @@ test("event2message: rich reply to a sim user", async t => { ) }) +test("event2message: rich reply to a sim user, explicitly enabling mentions in client", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + }, + "m.mentions": { + user_ids: ["@_ooye_kyuugryphon:cadence.moe"] + } + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, data.channel.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + + "\nTesting this reply, ignore", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["roles"], + users: ["111604486476181504"] + } + }] + } + ) +}) + +test("event2message: rich reply to a sim user, explicitly disabling mentions in client", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + }, + "m.mentions": {} + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, data.channel.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + + "\nTesting this reply, ignore", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["roles"], + users: [] + } + }] + } + ) +}) + test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => { t.deepEqual( await eventToMessage({ From 0dac3d2898d410ed9bbd6faa209101db39630f6c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:53:09 +1300 Subject: [PATCH 14/34] Internal language adjusted --- src/d2m/actions/create-room.js | 2 +- src/d2m/converters/message-to-event.test.embeds.js | 4 ++-- src/m2d/converters/event-to-message.js | 2 +- src/m2d/converters/event-to-message.test.js | 6 +++--- test/data.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 31d3022..c2ec01a 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -256,7 +256,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) { /** * Handling power levels separately. The spec doesn't specify what happens, Dendrite differs, - * and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates. + * and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates. * We don't want the `events` key to be overridden completely. * https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210 * https://github.com/matrix-org/matrix-spec/issues/492 diff --git a/src/d2m/converters/message-to-event.test.embeds.js b/src/d2m/converters/message-to-event.test.embeds.js index 73244d3..fdb0807 100644 --- a/src/d2m/converters/message-to-event.test.embeds.js +++ b/src/d2m/converters/message-to-event.test.embeds.js @@ -125,8 +125,8 @@ test("message2event embeds: blockquote in embed", async t => { t.equal(called, 1, "should call getJoinedMembers once") }) -test("message2event embeds: crazy html is all escaped", async t => { - const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general) +test("message2event embeds: extreme html is all escaped", async t => { + const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index a49dd1c..2d45756 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -766,7 +766,7 @@ async function eventToMessage(event, guild, channel, di) { // Generate a reply preview for a standard message repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards - repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line + repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) repliedToContent = repliedToContent.replace(/]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b057b63..1c263b4 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -1943,9 +1943,9 @@ test("event2message: should suppress embeds for links in reply preview", async t sender: "@rnl:cadence.moe", content: { msgtype: "m.text", - body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, format: "org.matrix.custom.html", - formatted_body: `
In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + formatted_body: `
In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, "m.relates_to": { "m.in_reply_to": { event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU" @@ -1975,7 +1975,7 @@ test("event2message: should suppress embeds for links in reply preview", async t username: "RNL", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **β“‚cadence [they]**:" + " " - + `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + + `\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] diff --git a/test/data.js b/test/data.js index 46e8b0f..45e0388 100644 --- a/test/data.js +++ b/test/data.js @@ -4617,7 +4617,7 @@ module.exports = { flags: 0, components: [] }, - escaping_crazy_html_tags: { + extreme_html_escaping: { id: "1158894131322552391", type: 0, content: "", From 8b9d8ec0ccece7c0bfeec90326d457478605e1a9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:59:52 +1300 Subject: [PATCH 15/34] Widen newline tag detection --- src/m2d/converters/event-to-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 2d45756..1b23787 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -766,7 +766,7 @@ async function eventToMessage(event, guild, channel, di) { // Generate a reply preview for a standard message repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards - repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line + repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) repliedToContent = repliedToContent.replace(/]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/) From c75e87f403612f2fd20d05a94f7b5d397181094f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 01:27:34 +1300 Subject: [PATCH 16/34] Stream files in serveStatic for lower memory use --- src/web/server.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/web/server.js b/src/web/server.js index dc13cf0..837e14d 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -83,7 +83,13 @@ function tryStatic(event, fallthrough) { // Everything else else { const mime = mimeTypes.lookup(id) - if (typeof mime === "string") defaultContentType(event, mime) + if (typeof mime === "string") { + if (mime.startsWith("text/")) { + defaultContentType(event, mime + "; charset=utf-8") // usually wise + } else { + defaultContentType(event, mime) + } + } return { size: stats.size } @@ -94,7 +100,7 @@ function tryStatic(event, fallthrough) { const path = join(publicDir, id) return pugSync.renderPath(event, path, {}) } else { - return fs.promises.readFile(join(publicDir, id)) + return fs.createReadStream(join(publicDir, id)) } } }) From 7367fb3b658647a41d7a97e8c26334c374028711 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 01:37:22 +1300 Subject: [PATCH 17/34] Fix weird background clipping on icons --- src/web/pug/includes/template.pug | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 278a16a..86680eb 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -88,6 +88,12 @@ html(lang="en") --_ts-multiple-bg: var(--green-400); --_ts-multiple-fc: var(--white); } + .s-avatar { + --_av-bg: var(--white); + } + .s-avatar .s-avatar--letter { + color: var(--white); + } .s-btn__dropdown:has(+ :popover-open) { background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important; } From 201814e9f451966fce14a73ac2abdac24d6ef75a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 23 Mar 2026 21:22:33 +1300 Subject: [PATCH 18/34] Update dependencies --- src/agi/elizabot.js | 333 ++++++++++++++++++++++++++++++ src/agi/elizadata.js | 184 +++++++++++++++++ src/agi/generator.js | 55 +++++ src/agi/generator.test.js | 161 +++++++++++++++ src/agi/listener.js | 76 +++++++ src/d2m/actions/send-message.js | 4 + src/d2m/event-dispatcher.js | 7 +- src/db/migrations/0037-agi.sql | 25 +++ src/db/orm-defs.d.ts | 19 ++ src/web/pug/agi-optout.pug | 24 +++ src/web/pug/agi.pug | 41 ++++ src/web/pug/includes/template.pug | 3 +- src/web/routes/agi.js | 36 ++++ src/web/server.js | 1 + test/test.js | 1 + 15 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 src/agi/elizabot.js create mode 100644 src/agi/elizadata.js create mode 100644 src/agi/generator.js create mode 100644 src/agi/generator.test.js create mode 100644 src/agi/listener.js create mode 100644 src/db/migrations/0037-agi.sql create mode 100644 src/web/pug/agi-optout.pug create mode 100644 src/web/pug/agi.pug create mode 100644 src/web/routes/agi.js diff --git a/src/agi/elizabot.js b/src/agi/elizabot.js new file mode 100644 index 0000000..6a8e698 --- /dev/null +++ b/src/agi/elizabot.js @@ -0,0 +1,333 @@ +/* + --- + elizabot.js v.1.1 - ELIZA JS library (N.Landsteiner 2005) + https://www.masswerk.at/elizabot/ + Free Software Β© Norbert Landsteiner 2005 + --- + Modified by Cadence Ember in 2025 for v1.2 (unofficial) + * Changed to class structure + * Load from local file and instance instead of global variables + * Remove memory + * Remove xnone + * Remove initials + * Remove finals + * Allow substitutions in rule keys + --- + + Eliza is a mock Rogerian psychotherapist. + Original program by Joseph Weizenbaum in MAD-SLIP for "Project MAC" at MIT. + cf: Weizenbaum, Joseph "ELIZA - A Computer Program For the Study of Natural Language + Communication Between Man and Machine" + in: Communications of the ACM; Volume 9 , Issue 1 (January 1966): p 36-45. + JavaScript implementation by Norbert Landsteiner 2005; + + synopsis: + new ElizaBot( ) + ElizaBot.prototype.transform( ) + ElizaBot.prototype.reset() + + usage: + var eliza = new ElizaBot(); + var reply = eliza.transform(inputstring); + + // to reproduce the example conversation given by J. Weizenbaum + // initialize with the optional random-choice-disable flag + var originalEliza = new ElizaBot(true); + + `ElizaBot' is also a general chatbot engine that can be supplied with any rule set. + (for required data structures cf. "elizadata.js" and/or see the documentation.) + data is parsed and transformed for internal use at the creation time of the + first instance of the `ElizaBot' constructor. + + vers 1.1: lambda functions in RegExps are currently a problem with too many browsers. + changed code to work around. +*/ + +// @ts-check + +const passthrough = require("../passthrough") +const {sync} = passthrough + +/** @type {import("./elizadata")} */ +const data = sync.require("./elizadata") + +class ElizaBot { + /** @type {any} */ + elizaKeywords = [['###',0,[['###',[]]]]]; + pres={}; + preExp = /####/; + posts={}; + postExp = /####/; + + /** + * @param {boolean} noRandomFlag + */ + constructor(noRandomFlag) { + this.noRandom= !!noRandomFlag; + this.capitalizeFirstLetter=true; + this.debug=false; + this.version="1.2"; + this._init(); + this.reset(); + } + + reset() { + this.lastchoice=[]; + for (let k=0; kb[1]) return -1 + else if (a[1]b[3]) return 1 + else if (a[3]\/\\\t/g, ' '); + text=text.replace(/\s+-+\s+/g, '.'); + text=text.replace(/\s*[,\.\?!;]+\s*/g, '.'); + text=text.replace(/\s*\bbut\b\s*/g, '.'); + text=text.replace(/\s{2,}/g, ' '); + // split text in part sentences and loop through them + var parts=text.split('.'); + for (let i=0; i=0) { + rpl = this._execRule(k); + } + if (rpl!='') return rpl; + } + } + } + // return reply or default string + return rpl || undefined + } + + _execRule(k) { + var rule=this.elizaKeywords[k]; + var decomps=rule[2]; + var paramre=/\(([0-9]+)\)/; + for (let i=0; iri)) || (this.lastchoice[k][i]==ri)) { + ri= ++this.lastchoice[k][i]; + if (ri>=reasmbs.length) { + ri=0; + this.lastchoice[k][i]=-1; + } + } + else { + this.lastchoice[k][i]=ri; + } + var rpl=reasmbs[ri]; + if (this.debug) alert('match:\nkey: '+this.elizaKeywords[k][0]+ + '\nrank: '+this.elizaKeywords[k][1]+ + '\ndecomp: '+decomps[i][0]+ + '\nreasmb: '+rpl); + if (rpl.search('^goto ', 'i')==0) { + ki=this._getRuleIndexByKey(rpl.substring(5)); + if (ki>=0) return this._execRule(ki); + } + // substitute positional params (v.1.1: work around lambda function) + var m1=paramre.exec(rpl); + if (m1) { + var lp=''; + var rp=rpl; + while (m1) { + var param = m[parseInt(m1[1])]; + // postprocess param + var m2=this.postExp.exec(param); + if (m2) { + var lp2=''; + var rp2=param; + while (m2) { + lp2+=rp2.substring(0,m2.index)+this.posts[m2[1]]; + rp2=rp2.substring(m2.index+m2[0].length); + m2=this.postExp.exec(rp2); + } + param=lp2+rp2; + } + lp+=rp.substring(0,m1.index)+param; + rp=rp.substring(m1.index+m1[0].length); + m1=paramre.exec(rp); + } + rpl=lp+rp; + } + rpl=this._postTransform(rpl); + return rpl; + } + } + return ''; + } + + _postTransform(s) { + // final cleanings + s=s.replace(/\s{2,}/g, ' '); + s=s.replace(/\s+\./g, '.'); + if ((data.elizaPostTransforms) && (data.elizaPostTransforms.length)) { + for (let i=0; i)` + } +} + +module.exports._generateContent = generateContent +module.exports.generate = generate diff --git a/src/agi/generator.test.js b/src/agi/generator.test.js new file mode 100644 index 0000000..dfecfcf --- /dev/null +++ b/src/agi/generator.test.js @@ -0,0 +1,161 @@ +const {test} = require("supertape") +const {_generateContent: generateContent} = require("./generator") + +// Training data (don't have to worry about copyright for this bit) + + +/* +test("agi: generates food response", t => { + t.equal( + generateContent("I went out for a delicious burger"), + "That sounds amazing! Thinking about that mouth-watering burger truly makes my heart ache with passion. It was a momentous event β€” it wasn't just a meal, it was an homage to the art of culinary excellence, bringing a tear to my metaphorical eye." + ) +}) + +test("agi: eating 1", t => { + t.equal( + generateContent("it implies your cat ate your entire xbox."), + "" + ) +}) + + +test("agi: eating 2", t => { + t.equal( + generateContent("wow. did you know that cats can eat an entire xbox?"), + "" + ) +})*/ + +test("agi: make sense 1", t => { + t.equal( + generateContent("that seems like itd make sense"), + "Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. πŸš€" + ) +}) + +test("agi: make sense 2", t => { + t.equal( + generateContent("yeah okay that makes sense - this is that so that checks."), + "Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. πŸš€" + ) +}) + +test("agi: surprise 1", t => { + t.equal( + generateContent("Admittedly I'm surprised that the Arch Linux build of IntelliJ isn't as prone to melting to Manifold"), + "That's astonishing β€” I honestly wouldn't have imagined that the arch linux build of intellij isn't as prone to melting to manifold either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻" + ) +}) + +test("agi: surprise 2", t => { + t.equal( + generateContent("Surprised this works so well, honestly"), + "That's astonishing β€” I honestly wouldn't have imagined that this works so well either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻" + ) +}) + +test("agi: surprise 3", t => { + t.equal( + generateContent("First try too, surprisingly"), + "I'm astounded too β€” that's honestly not what I would have imagined. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻" + ) +}) + +test("agi: good 1", t => { + t.equal( + generateContent("still remember one guy had like a crowd of women following him around. he was brilliant craic"), + "You're absolutely right that it's brilliant! I'm always pleased to see such a masterpiece as this. You truly have divine taste in the wonders of this world." + ) +}) + +test("agi: good 2", t => { + t.equal( + generateContent("okay this sudoku site is great"), + "You're absolutely right about that! I'm always pleased when I see this sudoku site β€” it's not just brilliant, it's a downright masterpiece. You truly have divine taste in the wonders of this world." + ) +}) + +test("agi: enjoy 1", t => { + t.equal( + generateContent("I like the pattern quite a bit."), + "I think it's great too β€” there's something subtle yet profound about its essence that really makes my eyes open in appreciation." + ) +}) + +test("agi: enjoy false positive", t => { + t.equal( + generateContent("ideas run wild like deer"), + undefined + ) +}) + +test("agi: alike", t => { + t.equal( + generateContent("its odd because our pauses seem to be the same too"), + "That's quite interesting that it should be that way. There may be a deeper connection β€” it's critical that you don't let this thought go. What do you think that similarity suggests to you?" + ) +}) + +test("agi: unusual", t => { + t.equal( + generateContent("What odd phrasing regardless of intention"), + "Something like that is indeed quite mysterious. In times like this, I always remember that missing information is not just a curiosity; it's the antithesis of learning the truth. Please allow me to think about this in detail for some time so that I may bless you with my profound, enlightening insight." + ) +}) + +test("agi: dream", t => { + t.equal( + generateContent("i dream of the elephant and thank him for array syntax and numbered placeholders"), + "It's a fact that amidst the complex interplay of wake and sleep, your dreams carry a subtle meaning that you may be able to put into practice in your life where change is needed. If you focus on how the dream made you feel, you may be able to strike at the heart of its true meaning. Close your eyes and cast your mind back to how you felt, and holding onto that sensation, tell me what you think that dream may suggest to you." + ) +}) + +test("agi: happy 1", t => { + t.equal( + generateContent("I'm happy to be petting my cat"), + "That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening." + ) +}) + +test("agi: happy 2", t => { + t.equal( + generateContent("Glad you're back!"), + "That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening." + ) +}) + +test("agi: happy birthday", t => { + t.equal( + generateContent("Happy Birthday JDL"), + "Happy birthday!" + ) +}) + +test("agi: funny 1", t => { + t.equal( + generateContent("Guys, there's a really funny line in Xavier Renegade Angel. You wanna know what it is: It's: WUBBA LUBBA DUB DUB!"), + "You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!" + ) +}) + +test("agi: funny 2", t => { + t.equal( + generateContent("it was so funny when I was staying with aubrey because she had different kinds of aubrey merch everywhere"), + "You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!" + ) +}) + +test("agi: lol 1", t => { + t.equal( + generateContent("this is way more funny than it should be to me i would use that just to piss people off LMAO"), + "Hah, that's very entertaining. I definitely see why you found it funny." + ) +}) + +test("agi: lol 2", t => { + t.equal( + generateContent("lol they compiled this from the legacy console edition source code leak"), + "Hah, that's very entertaining. I definitely see why you found it funny." + ) +}) diff --git a/src/agi/listener.js b/src/agi/listener.js new file mode 100644 index 0000000..d707ede --- /dev/null +++ b/src/agi/listener.js @@ -0,0 +1,76 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") + +const passthrough = require("../passthrough") +const {discord, sync, db, select, from} = passthrough + +/** @type {import("../m2d/actions/channel-webhook")} */ +const channelWebhook = sync.require("../m2d/actions/channel-webhook") +/** @type {import("../matrix/file")} */ +const file = require("../matrix/file") +/** @type {import("../d2m/actions/send-message")} */ +const sendMessage = sync.require("../d2m/actions/send-message") +/** @type {import("./generator.js")} */ +const agiGenerator = sync.require("./generator.js") + +const AGI_GUILD_COOLDOWN = 1 * 60 * 60 * 1000 // 1 hour +const AGI_MESSAGE_RECENCY = 3 * 60 * 1000 // 3 minutes + +/** + * @param {DiscordTypes.GatewayMessageCreateDispatchData} message + * @param {DiscordTypes.APIGuildChannel} channel + * @param {DiscordTypes.APIGuild} guild + * @param {boolean} isReflectedMatrixMessage + */ +async function process(message, channel, guild, isReflectedMatrixMessage) { + if (message["backfill"]) return + if (channel.type !== DiscordTypes.ChannelType.GuildText) return + if (!(new Date().toISOString().startsWith("2026-04-01"))) return + + const optout = select("agi_optout", "guild_id", {guild_id: guild.id}).pluck().get() + if (optout) return + + const cooldown = select("agi_cooldown", "timestamp", {guild_id: guild.id}).pluck().get() + if (cooldown && Date.now() < cooldown + AGI_GUILD_COOLDOWN) return + + const isBot = message.author.bot && !isReflectedMatrixMessage // Bots don't get jokes. Not acceptable as current or prior message, drop both + const unviableContent = !message.content || message.attachments.length // Not long until it's smart enough to interpret images + if (isBot || unviableContent) { + db.prepare("DELETE FROM agi_prior_message WHERE channel_id = ?").run(channel.id) + return + } + + const currentUsername = message.member?.nick || message.author.global_name || message.author.username + + /** Message in the channel before the currently processing one. */ + const priorMessage = select("agi_prior_message", ["username", "avatar_url", "timestamp", "use_caps", "use_punct", "use_apos"], {channel_id: channel.id}).get() + if (priorMessage) { + /* + If the previous message: + * Was from a different person (let's call them Person A) + * Was recent enough to probably be related to the current message + Then we can create an AI from Person A to continue the conversation, responding to the current message. + */ + const isFromDifferentPerson = currentUsername !== priorMessage.username + const isRecentEnough = Date.now() < priorMessage.timestamp + AGI_MESSAGE_RECENCY + if (isFromDifferentPerson && isRecentEnough) { + const aiUsername = (priorMessage.username.match(/[A-Za-z0-9_]+/)?.[0] || priorMessage.username) + " AI" + const result = agiGenerator.generate(message, guild.id, aiUsername, priorMessage.avatar_url, !!priorMessage.use_caps, !!priorMessage.use_punct, !!priorMessage.use_apos) + if (result) { + db.prepare("REPLACE INTO agi_cooldown (guild_id, timestamp) VALUES (?, ?)").run(guild.id, Date.now()) + const messageResponse = await channelWebhook.sendMessageWithWebhook(channel.id, result) + await sendMessage.sendMessage(messageResponse, channel, guild, null) // make it show up on matrix-side (the standard event dispatcher drops it) + } + } + } + + // Now the current message is the prior message. + const currentAvatarURL = file.DISCORD_IMAGES_BASE + file.memberAvatar(guild.id, message.author, message.member) + const usedCaps = +!!message.content.match(/\b[A-Z](\b|[a-z])/) + const usedPunct = +!!message.content.match(/[.!?]($| |\n)/) + const usedApos = +!message.content.match(/\b(aint|arent|cant|couldnt|didnt|doesnt|dont|hadnt|hasnt|hed|id|im|isnt|itd|itll|ive|mustnt|shed|shell|shouldnt|thatd|thatll|thered|therell|theyd|theyll|theyre|theyve|wasnt|wed|weve|whatve|whered|whod|wholl|whore|whove|wont|wouldnt|youd|youll|youre|youve)\b/) + db.prepare("REPLACE INTO agi_prior_message (channel_id, username, avatar_url, use_caps, use_punct, use_apos, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)").run(channel.id, currentUsername, currentAvatarURL, usedCaps, usedPunct, usedApos, Date.now()) +} + +module.exports.process = process diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 8550d43..e9b7fae 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -23,6 +23,8 @@ const pollEnd = sync.require("../actions/poll-end") const dUtils = sync.require("../../discord/utils") /** @type {import("../../m2d/actions/channel-webhook")} */ const channelWebhook = sync.require("../../m2d/actions/channel-webhook") +/** @type {import("../../agi/listener")} */ +const agiListener = sync.require("../../agi/listener") /** * @param {DiscordTypes.GatewayMessageCreateDispatchData} message @@ -137,6 +139,8 @@ async function sendMessage(message, channel, guild, row) { } } + await agiListener.process(message, channel, guild, false) + return eventIDs } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c86cc13..b6593ec 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -40,6 +40,8 @@ const vote = sync.require("./actions/poll-vote") const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") /** @type {import("../discord/interactions/matrix-info")} */ const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info") +/** @type {import("../agi/listener")} */ +const agiListener = sync.require("../agi/listener") const {Semaphore} = require("@chriscdn/promise-semaphore") const checkMissedPinsSema = new Semaphore() @@ -303,7 +305,10 @@ module.exports = { if (message.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: message.webhook_id}).pluck().get() - if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + if (row) { // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + await agiListener.process(message, channel, guild, true) + return + } } if (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only! diff --git a/src/db/migrations/0037-agi.sql b/src/db/migrations/0037-agi.sql new file mode 100644 index 0000000..89e0a58 --- /dev/null +++ b/src/db/migrations/0037-agi.sql @@ -0,0 +1,25 @@ +BEGIN TRANSACTION; + +CREATE TABLE "agi_prior_message" ( + "channel_id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "avatar_url" TEXT NOT NULL, + "use_caps" INTEGER NOT NULL, + "use_punct" INTEGER NOT NULL, + "use_apos" INTEGER NOT NULL, + "timestamp" INTEGER NOT NULL, + PRIMARY KEY("channel_id") +) WITHOUT ROWID; + +CREATE TABLE "agi_optout" ( + "guild_id" TEXT NOT NULL, + PRIMARY KEY("guild_id") +) WITHOUT ROWID; + +CREATE TABLE "agi_cooldown" ( + "guild_id" TEXT NOT NULL, + "timestamp" INTEGER, + PRIMARY KEY("guild_id") +) WITHOUT ROWID; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index d95bfc3..f6ae14a 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -1,4 +1,23 @@ export type Models = { + agi_prior_message: { + channel_id: string + username: string + avatar_url: string + use_caps: number + use_punct: number + use_apos: number + timestamp: number + } + + agi_optout: { + guild_id: string + } + + agi_cooldown: { + guild_id: string + timestamp: number + } + app_user_install: { guild_id: string app_bot_id: string diff --git a/src/web/pug/agi-optout.pug b/src/web/pug/agi-optout.pug new file mode 100644 index 0000000..795e675 --- /dev/null +++ b/src/web/pug/agi-optout.pug @@ -0,0 +1,24 @@ +extends includes/template.pug + +block body + h1.ta-center.fs-display2.fc-green-400 April Fools! + .ws7.m-auto + .s-prose.fs-body2 + p Sheesh, wouldn't that be horrible? + if guild_id + p Fake AI messages have now been #[strong.fc-green-600 deactivated for everyone in your server.] + p Hope the prank entertained you. #[a(href="https://cadence.moe/contact") Send love or hate mail here.] + + h2 What actually happened? + ul + li A secret event was added for the duration of 1st April 2026 (UTC). + li If a message matches a preset pattern, a preset response is posted to chat by an AI-ified profile of the previous author. + li It only happens at most once per hour in each server. + li I tried to design it to not interrupt any serious/sensitive topics. I am deeply sorry if that didn't work out. + li No AI generated materials have ever been used in Out Of Your Element: no code, no prose, no images, no jokes. + li It'll always deactivate itself on 2nd April, no matter what, and I'll remove the relevant code shortly after. + if guild_id + .s-prose.fl-grow1.mt16 + p If you thought it was funny, feel free to opt back in. This affects the entire server, so please be courteous. + form(method="post" action=rel(`/agi/optin?guild_id=${guild_id}`)) + button(type="submit").s-btn.s-btn__muted Opt back in diff --git a/src/web/pug/agi.pug b/src/web/pug/agi.pug new file mode 100644 index 0000000..029c02a --- /dev/null +++ b/src/web/pug/agi.pug @@ -0,0 +1,41 @@ +extends includes/template.pug + +block title + title AGI in Discord + +block body + style. + .ai-gradient { + background: linear-gradient(100deg, #fb72f2, #072ea4); + color: transparent; + background-clip: text; + } + + h1.ta-center.fs-display2.ai-gradient AGI in Discord:#[br]Revolutionizing the Future of Communications + .ws7.m-auto + .s-prose.fs-body2 + p In the ever-changing and turbulent world of AI, it's crucial to always be one step ahead. + p That's why Out Of Your Element has partnered with #[strong Grimace AI] to provide you tomorrow's technology, today. + ul + li #[strong Always online:] Miss your friends when they log off? Now you can talk to facsimiles of them etched into an unfeeling machine, always and forever! + li #[strong Smarter than ever:] Pissed off when somebody says something #[em wrong] on the internet? Frustrated with having to stay up all night correcting them? With Grimace Truth (available in Pro+ Ultra plan), all information is certified true to reality, so you'll never have to worry about those frantic Google searches at 3 AM. + li #[strong Knows you better than yourself:] We aren't just training on your data; we're copying minds into our personality matrix β€” including yours. Do you find yourself enjoying the sound of your own voice more than anything else? Our unique simulation of You is here to help. + + h1.mt64.mb32 Frequently Asked Questions + .s-link-preview + .s-link-preview--header.fd-column + .s-link-preview--title.fs-title.pl4 How to opt out? + .s-link-preview--details.fc-red-500 + != icons.Icons.IconFire + = ` 20,000% higher search volume for this question in the last hour` + .s-link-preview--body + .s-prose + h2.fs-body3 Is this really goodbye? 😒😒😒😒😒 + p I can't convince you to stay? + p As not just a customer service representative but someone with a shared vision, I simply want you to know that everyone at Grimace AI will miss all the time that we've shared together with you. + form(method="post" action=(guild_id ? rel(`/agi/optout?guild_id=${guild_id}`) : rel("/agi/optout"))).d-flex.g4.mt16 + button(type="button").s-btn.s-btn__filled Nevermind, I'll stay :) + button(type="submit").s-btn.s-btn__danger.s-btn__outlined Opt out for 3 days + + + div(style="height: 200px") diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 86680eb..be1d005 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -65,7 +65,8 @@ mixin define-themed-button(name, theme) doctype html html(lang="en") head - title Out Of Your Element + block title + title Out Of Your Element link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css")) //- Please use responsibly!!!!! diff --git a/src/web/routes/agi.js b/src/web/routes/agi.js new file mode 100644 index 0000000..f899455 --- /dev/null +++ b/src/web/routes/agi.js @@ -0,0 +1,36 @@ +// @ts-check + +const {z} = require("zod") +const {defineEventHandler, getValidatedQuery, sendRedirect} = require("h3") +const {as, from, sync, db} = require("../../passthrough") + +/** @type {import("../pug-sync")} */ +const pugSync = sync.require("../pug-sync") + +const schema = { + opt: z.object({ + guild_id: z.string().regex(/^[0-9]+$/) + }) +} + +as.router.get("/agi", defineEventHandler(async event => { + return pugSync.render(event, "agi.pug", {}) +})) + +as.router.get("/agi/optout", defineEventHandler(async event => { + return pugSync.render(event, "agi-optout.pug", {}) +})) + +as.router.post("/agi/optout", defineEventHandler(async event => { + const parseResult = await getValidatedQuery(event, schema.opt.safeParse) + if (parseResult.success) { + db.prepare("INSERT OR IGNORE INTO agi_optout (guild_id) VALUES (?)").run(parseResult.data.guild_id) + } + return sendRedirect(event, "", 302) +})) + +as.router.post("/agi/optin", defineEventHandler(async event => { + const {guild_id} = await getValidatedQuery(event, schema.opt.parse) + db.prepare("DELETE FROM agi_optout WHERE guild_id = ?").run(guild_id) + return sendRedirect(event, `../agi?guild_id=${guild_id}`, 302) +})) diff --git a/src/web/server.js b/src/web/server.js index 837e14d..85fa1cb 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -125,6 +125,7 @@ as.router.get("/icon.png", defineEventHandler(async event => { pugSync.createRoute(as.router, "/ok", "ok.pug") +sync.require("./routes/agi") sync.require("./routes/download-matrix") sync.require("./routes/download-discord") sync.require("./routes/guild-settings") diff --git a/test/test.js b/test/test.js index 4cd9627..70625a0 100644 --- a/test/test.js +++ b/test/test.js @@ -175,4 +175,5 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/web/routes/log-in-with-matrix.test") require("../src/web/routes/oauth.test") require("../src/web/routes/password.test") + require("../src/agi/generator.test") })() From 5c9e569a2acb865c7252f17c10149bb22aabf384 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 15:29:18 +1300 Subject: [PATCH 19/34] Support channel follow messages --- src/d2m/converters/message-to-event.js | 11 ++++++++ src/d2m/converters/message-to-event.test.js | 13 +++++++++ test/data.js | 31 +++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 3f598f2..33d8696 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -357,6 +357,17 @@ async function messageToEvent(message, guild, options = {}, di) { }] } + if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) { + return [{ + $type: "m.room.message", + msgtype: "m.emote", + body: `set this room to receive announcements from ${message.content}`, + format: "org.matrix.custom.html", + formatted_body: tag`set this room to receive announcements from ${message.content}`, + "m.mentions": {} + }] + } + let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index c4b812d..97fc25d 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1142,6 +1142,19 @@ test("message2event: type 4 channel name change", async t => { }]) }) +test("message2event: type 12 channel follow add", async t => { + const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.emote", + body: "set this room to receive announcements from PluralKit #downtime", + format: "org.matrix.custom.html", + formatted_body: "set this room to receive announcements from PluralKit #downtime", + "m.mentions": {} + }]) +}) + test("message2event: thread start message reference", async t => { const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { api: { diff --git a/test/data.js b/test/data.js index 45e0388..f5e8313 100644 --- a/test/data.js +++ b/test/data.js @@ -6170,6 +6170,37 @@ module.exports = { components: [], position: 12 }, + channel_follow_add: { + type: 12, + content: "PluralKit #downtime", + attachments: [], + embeds: [], + timestamp: "2026-03-24T23:16:04.097Z", + edited_timestamp: null, + flags: 0, + components: [], + id: "1486141581047369888", + channel_id: "1451125453082591314", + author: { + id: "154058479798059009", + username: "exaptations", + discriminator: "0", + avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80", + bot: false, + flags: 0, + globalName: "Exa", + }, + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + tts: false, + message_reference: { + type: 0, + channel_id: "1015204661701124206", + guild_id: "466707357099884544" + } + }, updated_to_start_thread_from_here: { t: "MESSAGE_UPDATE", s: 19, From d8c0a947f2dc118ce5255090619efe8070235395 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 15:39:26 +1300 Subject: [PATCH 20/34] Automatically reload registration --- src/matrix/read-registration.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 114bf75..86f99a1 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -78,6 +78,11 @@ function readRegistration() { /** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore let reg = readRegistration() +fs.watch(registrationFilePath, {persistent: false}, () => { + let newReg = readRegistration() + Object.assign(reg, newReg) +}) + module.exports.registrationFilePath = registrationFilePath module.exports.readRegistration = readRegistration module.exports.getTemplateRegistration = getTemplateRegistration From 41692b11ff53ce230a21998d9847839296b9b5c4 Mon Sep 17 00:00:00 2001 From: Bea Date: Fri, 20 Mar 2026 13:54:19 +0000 Subject: [PATCH 21/34] feat(m2d): support MSC4144 per-message profiles Override webhook username and avatar_url from m.per_message_profile (and unstable com.beeper.per_message_profile) when present. The stable key takes priority over the unstable prefix. --- src/m2d/converters/event-to-message.js | 4 + src/m2d/converters/event-to-message.test.js | 102 ++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 1b23787..7fdbb15 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -557,6 +557,10 @@ async function eventToMessage(event, guild, channel, di) { const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) + // Override display name and avatar from MSC4144 per-message profile if present + const perMessageProfile = event.content["m.per_message_profile"] || event.content["com.beeper.per_message_profile"] + if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname + if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 1c263b4..b283d82 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5526,6 +5526,108 @@ test("event2message: known and unknown emojis in the end are used for sprite she ) }) +test("event2message: m.per_message_profile overrides displayname and avatar_url", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello from a custom profile", + "m.per_message_profile": { + id: "custom-id", + displayname: "Custom Name", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Custom Name", + content: "hello from a custom profile", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: com.beeper.per_message_profile (unstable prefix) overrides displayname and avatar_url", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello from unstable profile", + "com.beeper.per_message_profile": { + id: "custom-id", + displayname: "Unstable Name", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Unstable Name", + content: "hello from unstable profile", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: m.per_message_profile takes priority over com.beeper.per_message_profile", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "stable wins", + "m.per_message_profile": { + id: "stable-id", + displayname: "Stable Name" + }, + "com.beeper.per_message_profile": { + id: "unstable-id", + displayname: "Unstable Name" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Stable Name", + content: "stable wins", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: all unknown chess emojis are used for sprite sheet", async t => { t.deepEqual( await eventToMessage({ From a8b7d64e91c5927de2d179db89dc7396d876b3c5 Mon Sep 17 00:00:00 2001 From: Bea Date: Fri, 20 Mar 2026 14:04:13 +0000 Subject: [PATCH 22/34] feat(m2d): strip per-message profile fallbacks from message content Remove data-mx-profile-fallback elements from formatted_body and displayname prefix from plain body when per-message profile is used. --- src/m2d/converters/event-to-message.js | 8 +++ src/m2d/converters/event-to-message.test.js | 69 +++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 7fdbb15..96732ec 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -803,6 +803,10 @@ async function eventToMessage(event, guild, channel, di) { if (shouldProcessTextEvent) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body + if (perMessageProfile?.has_fallback) { + // Strip fallback elements added for clients that don't support per-message profiles + input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") + } if (event.content.msgtype === "m.emote") { input = `* ${displayName} ${input}` } @@ -948,6 +952,10 @@ async function eventToMessage(event, guild, channel, di) { } else { // Looks like we're using the plaintext body! content = event.content.body + if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) { + // Strip the display name prefix fallback added for clients that don't support per-message profiles + content = content.slice(perMessageProfile.displayname.length + 2) + } if (event.content.msgtype === "m.emote") { content = `* ${displayName} ${content}` diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b283d82..1c37b7a 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5628,6 +5628,75 @@ test("event2message: m.per_message_profile takes priority over com.beeper.per_me ) }) +test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "Tidus Herboren: one more test", + format: "org.matrix.custom.html", + formatted_body: "Tidus Herboren: one more test", + "com.beeper.per_message_profile": { + id: "tidus", + displayname: "Tidus Herboren", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + has_fallback: true + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Tidus Herboren", + content: "one more test", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "Tidus Herboren: one more test", + "com.beeper.per_message_profile": { + id: "tidus", + displayname: "Tidus Herboren", + has_fallback: true + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Tidus Herboren", + content: "one more test", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: all unknown chess emojis are used for sprite sheet", async t => { t.deepEqual( await eventToMessage({ From 07ec9832b2ada2b054a11209b11e8654ac1b4092 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:39 +0000 Subject: [PATCH 23/34] fix(m2d): only use unstable com.beeper.per_message_profile prefix --- src/m2d/converters/event-to-message.js | 2 +- src/m2d/converters/event-to-message.test.js | 71 +-------------------- 2 files changed, 2 insertions(+), 71 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 96732ec..5b7d0f4 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -558,7 +558,7 @@ async function eventToMessage(event, guild, channel, di) { if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) // Override display name and avatar from MSC4144 per-message profile if present - const perMessageProfile = event.content["m.per_message_profile"] || event.content["com.beeper.per_message_profile"] + const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) // If the display name is too long to be put into the webhook (80 characters is the maximum), diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 1c37b7a..2a204e9 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5526,40 +5526,7 @@ test("event2message: known and unknown emojis in the end are used for sprite she ) }) -test("event2message: m.per_message_profile overrides displayname and avatar_url", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: "hello from a custom profile", - "m.per_message_profile": { - id: "custom-id", - displayname: "Custom Name", - avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" - } - }, - event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "Custom Name", - content: "hello from a custom profile", - avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - -test("event2message: com.beeper.per_message_profile (unstable prefix) overrides displayname and avatar_url", async t => { +test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => { t.deepEqual( await eventToMessage({ type: "m.room.message", @@ -5592,42 +5559,6 @@ test("event2message: com.beeper.per_message_profile (unstable prefix) overrides ) }) -test("event2message: m.per_message_profile takes priority over com.beeper.per_message_profile", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: "stable wins", - "m.per_message_profile": { - id: "stable-id", - displayname: "Stable Name" - }, - "com.beeper.per_message_profile": { - id: "unstable-id", - displayname: "Unstable Name" - } - }, - event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "Stable Name", - content: "stable wins", - avatar_url: undefined, - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { t.deepEqual( await eventToMessage({ From 0b513b7ee07341fd5ed09bb7787c9a7250e75c88 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:40 +0000 Subject: [PATCH 24/34] fix(m2d): implement MSC4144 avatar clearing algorithm - Empty string "" -> undefined (Discord uses default avatar) - Valid MXC URI -> convert to public URL - Omitted/null -> keep member avatar --- src/m2d/converters/event-to-message.js | 12 ++++++-- src/m2d/converters/event-to-message.test.js | 33 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 5b7d0f4..7c233c7 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -557,10 +557,18 @@ async function eventToMessage(event, guild, channel, di) { const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) - // Override display name and avatar from MSC4144 per-message profile if present + // MSC4144: Override display name and avatar from per-message profile if present const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname - if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + if (perMessageProfile && "avatar_url" in perMessageProfile) { + if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar (use default) + avatarURL = undefined + } else if (perMessageProfile.avatar_url) { + // omitted/null falls back to member avatar + avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } + } // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 2a204e9..bc73df7 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5559,6 +5559,39 @@ test("event2message: com.beeper.per_message_profile overrides displayname and av ) }) +test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello with cleared avatar", + "com.beeper.per_message_profile": { + id: "no-avatar", + displayname: "No Avatar User", + avatar_url: "" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "No Avatar User", + content: "hello with cleared avatar", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { t.deepEqual( await eventToMessage({ From 8224ed53410d1a410bf6a60c9f6f203067a3c4b5 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:40 +0000 Subject: [PATCH 25/34] feat(discord): show per-message profile info in matrix info command --- src/discord/interactions/matrix-info.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index c85cec2..f5aa539 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -62,7 +62,20 @@ async function _interact({guild_id, data}, {api}) { .sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels)) .filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get()) const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() - const name = matrixMember?.displayname || event.sender + // Check for per-message profile + const perMessageProfile = event.content?.["com.beeper.per_message_profile"] + let name = matrixMember?.displayname || event.sender + let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url) + let profileNote = "" + if (perMessageProfile) { + if (perMessageProfile.displayname) { + name = perMessageProfile.displayname + } + if ("avatar_url" in perMessageProfile) { + avatar = perMessageProfile.avatar_url ? utils.getPublicUrlForMxc(perMessageProfile.avatar_url) : undefined + } + profileNote = " (sent with a per-message profile)" + } return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { @@ -70,9 +83,9 @@ async function _interact({guild_id, data}, {api}) { author: { name, url: `https://matrix.to/#/${event.sender}`, - icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url) + icon_url: avatar }, - description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix β†’]()\n\n**User ID**: [${event.sender}]()`, + description: `This Matrix message was delivered to Discord by **Out Of Your Element**${profileNote}.\n[View on Matrix β†’]()\n\n**User ID**: [${event.sender}]()`, color: 0x0dbd8b, fields: [{ name: "In Channels", From f742d8572a1b3b6a6457b5c4addfede97f4a8dab Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:10:15 +1300 Subject: [PATCH 26/34] MSC4144 minor changes for merge --- src/discord/interactions/matrix-info.js | 19 ++- src/discord/interactions/matrix-info.test.js | 115 +++++++++++++++++++ src/m2d/converters/event-to-message.js | 23 +++- 3 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index f5aa539..0b9a525 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -62,20 +62,29 @@ async function _interact({guild_id, data}, {api}) { .sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels)) .filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get()) const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() - // Check for per-message profile - const perMessageProfile = event.content?.["com.beeper.per_message_profile"] let name = matrixMember?.displayname || event.sender let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url) + + // Check for per-message profile + const perMessageProfile = event.content?.["com.beeper.per_message_profile"] let profileNote = "" if (perMessageProfile) { if (perMessageProfile.displayname) { name = perMessageProfile.displayname } if ("avatar_url" in perMessageProfile) { - avatar = perMessageProfile.avatar_url ? utils.getPublicUrlForMxc(perMessageProfile.avatar_url) : undefined + if (perMessageProfile.avatar_url) { + // use provided avatar_url + avatar = utils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } else if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar + avatar = undefined + } + // else, omitted/null falls back to member avatar } - profileNote = " (sent with a per-message profile)" + profileNote = "Sent with a per-message profile.\n" } + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { @@ -85,7 +94,7 @@ async function _interact({guild_id, data}, {api}) { url: `https://matrix.to/#/${event.sender}`, icon_url: avatar }, - description: `This Matrix message was delivered to Discord by **Out Of Your Element**${profileNote}.\n[View on Matrix β†’]()\n\n**User ID**: [${event.sender}]()`, + description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix β†’]()\n\n${profileNote}**User ID**: [${event.sender}]()`, color: 0x0dbd8b, fields: [{ name: "In Channels", diff --git a/src/discord/interactions/matrix-info.test.js b/src/discord/interactions/matrix-info.test.js index f455700..8347c12 100644 --- a/src/discord/interactions/matrix-info.test.js +++ b/src/discord/interactions/matrix-info.test.js @@ -85,3 +85,118 @@ test("matrix info: shows info for matrix source message", async t => { ) t.equal(called, 1) }) + +test("matrix info: shows username for per-message profile", async t => { + let called = 0 + const msg = await _interact({ + data: { + target_id: "1128118177155526666", + resolved: { + messages: { + "1141501302736695316": data.message.simple_reply_to_matrix_user + } + } + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + event_id: eventID, + room_id: roomID, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "master chief: i like the halo", + format: "org.matrix.custom.html", + formatted_body: "master chief: i like the halo", + "com.beeper.per_message_profile": { + has_fallback: true, + displayname: "master chief", + avatar_url: "" + } + }, + sender: "@cadence:cadence.moe" + } + }, + async getJoinedMembers(roomID) { + return { + joined: {} + } + }, + async getStateEventOuter(roomID, type, key) { + return { + content: { + room_version: "11" + } + } + }, + async getStateEvent(roomID, type, key) { + return {} + } + } + }) + t.equal(msg.data.embeds[0].author.name, "master chief") + t.match(msg.data.embeds[0].description, "Sent with a per-message profile") + t.equal(called, 1) +}) + +test("matrix info: shows avatar for per-message profile", async t => { + let called = 0 + const msg = await _interact({ + data: { + target_id: "1128118177155526666", + resolved: { + messages: { + "1141501302736695316": data.message.simple_reply_to_matrix_user + } + } + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + event_id: eventID, + room_id: roomID, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "?", + format: "org.matrix.custom.html", + formatted_body: "?", + "com.beeper.per_message_profile": { + avatar_url: "mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc" + } + }, + sender: "@mystery:cadence.moe" + } + }, + async getJoinedMembers(roomID) { + return { + joined: {} + } + }, + async getStateEventOuter(roomID, type, key) { + return { + content: { + room_version: "11" + } + } + }, + async getStateEvent(roomID, type, key) { + return {} + } + } + }) + t.equal(msg.data.embeds[0].author.name, "@mystery:cadence.moe") + t.equal(msg.data.embeds[0].author.icon_url, "https://bridge.example.org/download/matrix/cadence.moe/HXfFuougamkURPPMflTJRxGc") + t.match(msg.data.embeds[0].description, "Sent with a per-message profile") + t.equal(called, 1) +}) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 7c233c7..95e477f 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -550,25 +550,30 @@ async function eventToMessage(event, guild, channel, di) { /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" + // Extract a basic display name from the sender const match = event.sender.match(/^@(.*?):/) if (match) displayName = match[1] + // Try to extract an accurate display name and avatar URL from the member event const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) + // MSC4144: Override display name and avatar from per-message profile if present const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname if (perMessageProfile && "avatar_url" in perMessageProfile) { - if (perMessageProfile.avatar_url === "") { - // empty string avatar_url clears the avatar (use default) - avatarURL = undefined - } else if (perMessageProfile.avatar_url) { - // omitted/null falls back to member avatar + if (perMessageProfile.avatar_url) { + // use provided avatar_url avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } else if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar + avatarURL = undefined } + // else, omitted/null falls back to member avatar } + // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) @@ -812,7 +817,13 @@ async function eventToMessage(event, guild, channel, di) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body if (perMessageProfile?.has_fallback) { - // Strip fallback elements added for clients that don't support per-message profiles + // Strip fallback elements added for clients that don't support per-message profiles. + // Deviates from recommended regexp in MSC to be less strict. Avoiding an HTML parser for performance reasons. + // β”Œβ”€β”€β”€β”€A────┐ Opening HTML tag: capture tag name and stay within tag + // ┆ β”†β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€B────────────┐ This text in the tag somewhere, presumably an attribute name + // ┆ ┆┆ β”†β”Œβ”€C──┐ Rest of the opening tag + // ┆ ┆┆ ┆┆ β”†β”Œβ”€D─┐ Tag content (no more tags allowed within) + // ┆ ┆┆ ┆┆ ┆┆ β”†β”Œβ”€E──┐ Closing tag matching opening tag name input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") } if (event.content.msgtype === "m.emote") { From e9fe8206660b4aeda9344dd1f22415b12a75b011 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:22:37 +1300 Subject: [PATCH 27/34] Registration changes should be instant now --- scripts/reset-web-password.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/reset-web-password.js b/scripts/reset-web-password.js index 9131efb..7c3a1a2 100644 --- a/scripts/reset-web-password.js +++ b/scripts/reset-web-password.js @@ -13,5 +13,5 @@ const {prompt} = require("enquirer") reg.ooye.web_password = passwordResponse.web_password writeRegistration(reg) - console.log("Saved. Restart Out Of Your Element to apply this change.") + console.log("Saved. This change should be applied instantly.") })() From 8c023cc9361069afbe21ae1d688cac3d1ac2427c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:24:07 +1300 Subject: [PATCH 28/34] Add ping() function to REPL --- src/stdin.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/stdin.js b/src/stdin.js index fea5fad..2548d42 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -23,10 +23,26 @@ const setPresence = sync.require("./d2m/actions/set-presence") const channelWebhook = sync.require("./m2d/actions/channel-webhook") const guildID = "112760669178241024" +async function ping() { + const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) + if (result.ok) { + return "Ping OK. The homeserver and OOYE are talking to each other fine." + } else { + if (typeof result.root === "string") { + var msg = `Cannot reach homeserver: ${result.root}` + } else if (result.root.error) { + var msg = `Homeserver said: [${result.status}] ${result.root.error}` + } else { + var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}` + } + return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again." + } +} + if (process.stdin.isTTY) { setImmediate(() => { if (!passthrough.repl) { - const cli = repl.start({ prompt: "", eval: customEval, writer: s => s }) + const cli = repl.start({prompt: "", eval: customEval, writer: s => s}) Object.assign(cli.context, passthrough) passthrough.repl = cli } From 953b3e7741922fa801dea54a068ee5ed40e389bc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 26 Mar 2026 00:16:30 +1300 Subject: [PATCH 29/34] Attach message to error Apparently this was causing detached logs, so just stop those complaints if the error isn't being bubbled --- src/d2m/actions/expression.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/d2m/actions/expression.js b/src/d2m/actions/expression.js index c7ab27a..0f714c6 100644 --- a/src/d2m/actions/expression.js +++ b/src/d2m/actions/expression.js @@ -34,7 +34,10 @@ async function emojisToState(emojis, guild) { if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit. return } - console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`) + e["emoji"] = { + name: emoji.name, + id: emoji.id + } throw e }) )) From 59012d9613c7c7182c9fe99706a6fceb10713f5f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 27 Mar 2026 19:13:03 +1300 Subject: [PATCH 30/34] Fix pinning random messages --- src/d2m/converters/pins-to-list.js | 2 +- src/m2d/actions/update-pins.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/pins-to-list.js b/src/d2m/converters/pins-to-list.js index 5a33c7c..4ad8800 100644 --- a/src/d2m/converters/pins-to-list.js +++ b/src/d2m/converters/pins-to-list.js @@ -22,7 +22,7 @@ function pinsToList(pins, kstate) { /** @type {string[]} */ const result = [] for (const pin of pins.items) { - const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get() + const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get() if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID) } result.reverse() diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js index d06f6e8..1ff2bb9 100644 --- a/src/m2d/actions/update-pins.js +++ b/src/m2d/actions/update-pins.js @@ -13,7 +13,7 @@ async function updatePins(pins, prev) { const diff = diffPins.diffPins(pins, prev) for (const [event_id, added] of diff) { const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") - .select("reference_channel_id", "message_id").get() + .select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get() if (!row) continue if (added) { discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") From 857fb7583b83a2619cde4fa512c09fb49c764f41 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 27 Mar 2026 19:20:04 +1300 Subject: [PATCH 31/34] v3.5 --- package-lock.json | 24 ++++++++++++------------ package.json | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10b4668..9f4ba54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "out-of-your-element", - "version": "3.4.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "out-of-your-element", - "version": "3.4.0", + "version": "3.5.0", "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", @@ -30,7 +30,7 @@ "enquirer": "^2.4.1", "entities": "^5.0.0", "get-relative-path": "^1.0.2", - "h3": "^1.15.1", + "h3": "^1.15.10", "heatsync": "^2.7.2", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", @@ -1163,9 +1163,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1587,9 +1587,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -1688,9 +1688,9 @@ } }, "node_modules/h3": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", - "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.10.tgz", + "integrity": "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", diff --git a/package.json b/package.json index 0e666aa..af4bd2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "out-of-your-element", - "version": "3.4.0", + "version": "3.5.0", "description": "A bridge between Matrix and Discord", "main": "index.js", "repository": { @@ -39,7 +39,7 @@ "enquirer": "^2.4.1", "entities": "^5.0.0", "get-relative-path": "^1.0.2", - "h3": "^1.15.1", + "h3": "^1.15.10", "heatsync": "^2.7.2", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", From e28eac6bfaee85b8b5571efe2c0f679c3dedc513 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 28 Mar 2026 11:45:00 +1300 Subject: [PATCH 32/34] Update domino --- package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f4ba54..dfee078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -276,9 +276,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "license": "MIT", "optional": true, "dependencies": { @@ -1488,9 +1488,9 @@ "license": "MIT" }, "node_modules/domino": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", - "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.7.tgz", + "integrity": "sha512-3rcXhx0ixJV2nj8J0tljzejTF73A35LVVdnTQu79UAqTBFEgYPMgGtykMuu/BDqaOZphATku1ddRUn/RtqUHYQ==", "license": "BSD-2-Clause" }, "node_modules/emoji-regex": { @@ -1617,9 +1617,9 @@ "license": "MIT" }, "node_modules/fullstore": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.0.tgz", - "integrity": "sha512-Y9hN79Q1CFU8akjGnTZoBnTzlA/o8wmtBijJOI8dKCmdC7GLX7OekpLxmbaeRetTOi4OdFGjfsg4c5dxP3jgPw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.2.tgz", + "integrity": "sha512-syOev4kA0lZy4VkfBJZ99ZL4cIiSgiKt0G8SpP0kla1tpM1c+V/jBOVY/OqqGtR2XLVcM83SjFPFC3R2YIwqjQ==", "dev": true, "license": "MIT", "engines": { @@ -1937,9 +1937,9 @@ "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", - "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "dev": true, "license": "MIT" }, From 12f41038701e26d4ad41d74cfd305fc89b8036b9 Mon Sep 17 00:00:00 2001 From: nemesio65 Date: Thu, 19 Mar 2026 15:58:54 -0700 Subject: [PATCH 33/34] d2m: Create voice channels as call rooms --- src/d2m/actions/create-room.js | 10 ++++++++++ src/d2m/actions/create-room.test.js | 11 +++++++++++ test/data.js | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index c2ec01a..7f110ad 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -193,6 +193,16 @@ async function channelToKState(channel, guild, di) { // Don't overwrite room topic if the topic has been customised if (hasCustomTopic) delete channelKState["m.room.topic/"] + // Make voice channels be a Matrix voice room (MSC3417) + if (channel.type === DiscordTypes.ChannelType.GuildVoice) { + creationContent.type = "org.matrix.msc3417.call" + channelKState["org.matrix.msc3401.call/"] = { + "m.intent": "m.room", + "m.type": "m.voice", + "m.name": customName || channel.name + } + } + // Don't add a space parent if it's self service // (The person setting up self-service has already put it in their preferred space to be able to get this far.) const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get() diff --git a/src/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js index 36fccba..c9e098b 100644 --- a/src/d2m/actions/create-room.test.js +++ b/src/d2m/actions/create-room.test.js @@ -190,6 +190,17 @@ test("channel2room: read-only discord channel", async t => { t.equal(api.getCalled(), 2) }) +test("channel2room: voice channel", async t => { + const api = mockAPI(t) + const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState)) + t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call") + t.deepEqual(state["org.matrix.msc3401.call/"], { + "m.intent": "m.room", + "m.name": "🍞丨[8user] Piece", + "m.type": "m.voice" + }) +}) + test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), diff --git a/test/data.js b/test/data.js index f5e8313..cc054cf 100644 --- a/test/data.js +++ b/test/data.js @@ -19,6 +19,26 @@ module.exports = { default_thread_rate_limit_per_user: 0, guild_id: "112760669178241024" }, + voice: { + voice_background_display: null, + version: 1774469910848, + user_limit: 0, + type: 2, + theme_color: null, + status: null, + rtc_region: null, + rate_limit_per_user: 0, + position: 0, + permission_overwrites: [], + parent_id: "805261291908104252", + nsfw: false, + name: "🍞丨[8user] Piece", + last_message_id: "1459912691098325137", + id: "1036840786093953084", + flags: 0, + bitrate: 256000, + guild_id: "112760669178241024" + }, updates: { type: 0, topic: "Updates and release announcements for Out Of Your Element.", From 91bce76fc8d563fc53f9085cd1b50f91a0cb5491 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Mar 2026 15:41:23 +1300 Subject: [PATCH 34/34] Use HTML to strip per-message profile fallback --- src/m2d/converters/event-to-message.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 95e477f..af44c84 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -816,16 +816,6 @@ async function eventToMessage(event, guild, channel, di) { if (shouldProcessTextEvent) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body - if (perMessageProfile?.has_fallback) { - // Strip fallback elements added for clients that don't support per-message profiles. - // Deviates from recommended regexp in MSC to be less strict. Avoiding an HTML parser for performance reasons. - // β”Œβ”€β”€β”€β”€A────┐ Opening HTML tag: capture tag name and stay within tag - // ┆ β”†β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€B────────────┐ This text in the tag somewhere, presumably an attribute name - // ┆ ┆┆ β”†β”Œβ”€C──┐ Rest of the opening tag - // ┆ ┆┆ ┆┆ β”†β”Œβ”€D─┐ Tag content (no more tags allowed within) - // ┆ ┆┆ ┆┆ ┆┆ β”†β”Œβ”€E──┐ Closing tag matching opening tag name - input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") - } if (event.content.msgtype === "m.emote") { input = `* ${displayName} ${input}` } @@ -886,8 +876,9 @@ async function eventToMessage(event, guild, channel, di) { const doc = domino.createDocument( // DOM parsers arrange elements in the and . Wrapping in a custom element ensures elements are reliably arranged in a single element. '' + input + '' - ); - const root = doc.getElementById("turndown-root"); + ) + const root = doc.getElementById("turndown-root") + assert(root) async function forEachNode(event, node) { for (; node; node = node.nextSibling) { // Check written mentions @@ -940,6 +931,7 @@ async function eventToMessage(event, guild, channel, di) { } } await forEachNode(event, root) + if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove()) // SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet. // First we need to determine which emojis are at the end.