From 5bfd753af3c60a40c030d755b774f3f5516f8686 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 8 Sep 2026 18:19:53 +1200 Subject: [PATCH 01/20] Test moving the reply link --- src/m2d/converters/event-to-message.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index cff08f0..c15f8ae 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -762,11 +762,6 @@ async function eventToMessage(event, guild, channel, di) { } replyLine = await getL1L2ReplyLine() - const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") - .select("reference_channel_id", "message_id").where({event_id: repliedToEventId}).and("ORDER BY part").get() - if (row) { - replyLine += `https://discord.com/channels/${guild.id}/${row.reference_channel_id}/${row.message_id} ` - } // If the event has been edited, the homeserver will include the relation in `unsigned`. if (repliedToEvent.unsigned?.["m.relations"]?.["m.replace"]?.content?.["m.new_content"]) { repliedToEvent = repliedToEvent.unsigned["m.relations"]["m.replace"] // Note: this changes which event_id is in repliedToEvent. @@ -777,15 +772,15 @@ async function eventToMessage(event, guild, channel, di) { const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype) let contentPreview if (fileReplyContentAlternative) { - contentPreview = " " + fileReplyContentAlternative + contentPreview = fileReplyContentAlternative } else if (repliedToEvent.unsigned?.redacted_because) { - contentPreview = " (in reply to a deleted message)" + contentPreview = "(in reply to a deleted message)" } else if (typeof originalRepliedToContent !== "string") { // in reply to a weird metadata event like m.room.name, m.room.member... // I'm not implementing text fallbacks for arbitrary room events. this should cover most cases // this has never ever happened in the wild anyway repliedToEvent.sender = "" - contentPreview = " (channel details edited)" + contentPreview = "(channel details edited)" } else { // Generate a reply preview for a standard message let repliedToContent = originalRepliedToContent @@ -802,7 +797,7 @@ async function eventToMessage(event, guild, channel, di) { repliedToContent = entities.decodeHTML5Strict(repliedToContent) // Remove entities like & " const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { - contentPreview = ": " + contentPreviewChunks[0] + contentPreview = contentPreviewChunks[0] contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, url => { const originalUrlIndex = originalRepliedToContent.indexOf(url) if (originalUrlIndex !== -1 && originalRepliedToContent[originalUrlIndex + url.length]?.match(/[^ )>"'`]/)) { // URL was truncated by chunking, replace it @@ -829,7 +824,12 @@ async function eventToMessage(event, guild, channel, di) { if (!senderName) senderName = sender.match(/@([^:]*)/)?.[1] if (senderName) replyLine += `**โ“‚${senderName}**` } - replyLine = `-# > ${replyLine}${contentPreview}\n` + const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") + .select("reference_channel_id", "message_id").where({event_id: repliedToEventId}).and("ORDER BY part").get() + if (row) { + contentPreview = `[${contentPreview}]()` + } + replyLine = `-# > ${replyLine} ${contentPreview}\n` })() if (shouldProcessTextEvent) { From 9eb2ba1458a6711b3d4f2110554524932cc123ec Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 9 Sep 2026 00:19:10 +1200 Subject: [PATCH 02/20] Add 15 characters to m->d reply preview --- 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 adc5f97..ea53e0a 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -754,7 +754,7 @@ async function eventToMessage(event, guild, channel, di) { .replace(/(?:\n|
)+/g, " ") // Should all be on one line .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.) .replace(/<[^>]+>/g, "") // Completely strip all HTML tags and formatting. - ), 50) + ), 65) replyLine = "-# > " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) replyLine = replyLine.replace(/[,.']$/, "") + "..." replyLine += "\n" From 7faa8a3c9e29bbef93bf2e7c31c76c6011ef9167 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:47:15 +1200 Subject: [PATCH 03/20] Remove unwise nextTick delay --- src/d2m/discord-client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index b9b45e2..408c21c 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -55,9 +55,7 @@ class DiscordClient { this.guildChannelMap = new Map() if (listen !== "no") { this.cloud.on("event", message => { - process.nextTick(() => { - discordPackets.onPacket(this, message, listen) - }) + discordPackets.onPacket(this, message, listen) }) } From 5e9f3cd532e325f869b26d3a8e74c011b1415b40 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:51:29 +1200 Subject: [PATCH 04/20] Add retries back to PluralKit API --- src/d2m/actions/register-pk-user.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index e68d2cc..bb8335b 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -3,6 +3,7 @@ const assert = require("assert").strict const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") +const {scheduler} = require("timers/promises") const passthrough = require("../../passthrough") const {sync, db, select, from} = passthrough @@ -14,14 +15,21 @@ const file = sync.require("../../matrix/file") const registerUser = sync.require("./register-user") /** @returns {Promise} */ -async function fetchMessage(messageID) { +async function fetchMessage(messageID, attempt = 1) { try { var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) } catch (networkError) { // Network issue, raise a more readable message throw new Error(`Failed to connect to PK API: ${networkError.toString()}`) } - if (!res.ok) throw new Error(`PK API returned an error: ${await res.text()}`) + if (!res.ok) { + if (attempt < 2) { + await scheduler.wait(5000) + return fetchMessage(messageID, attempt + 1) + } else { + throw new Error(`PK API returned an error: ${await res.text()}`) + } + } /** @type {any} */ const root = await res.json() if (!root.member) throw new Error(`PK API didn't return member data: ${JSON.stringify(root)}`) From cf95f7d915cc9edf859b397de22fdf1fd11fba8e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:57:46 +1200 Subject: [PATCH 05/20] Add AI policy --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index e8a8e7e..deb98da 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,10 @@ Modern Matrix-to-Discord appservice bridge, created by [@cadence:cadence.moe](ht ![](https://cadence.moe/i/f42a3f) +## AI policy + +Out Of Your Element is 100% organically cultivated ethical hand-fed code. Let's work together to conserve that! + ## Why a new bridge? * Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership. From 965fcab646dd3fb1eb1f63994a364541d8ba3719 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 20:07:36 +1200 Subject: [PATCH 06/20] m->d edits now wait for original message --- src/d2m/actions/retrigger.js | 10 +++++----- src/m2d/actions/send-event.js | 2 +- src/m2d/converters/event-to-message.js | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js index 43f400d..d7ac1a6 100644 --- a/src/d2m/actions/retrigger.js +++ b/src/d2m/actions/retrigger.js @@ -41,7 +41,7 @@ const storage = new class { this.timers.set(id, setTimeout(() => this.resolve(id, false), 60 * 1000).unref()) // 1 minute } } - + /** @param {string} id */ isNotPaused(id) { return !storage.paused.has(id) @@ -98,7 +98,7 @@ function waitFor(id, resolve, existsInDatabase) { const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw() /** * @param {string} eventID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForEvent(eventID) { const {promise, resolve} = Promise.withResolvers() @@ -109,7 +109,7 @@ function waitForEvent(eventID) { const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw() /** * @param {string} messageID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForMessage(messageID) { const {promise, resolve} = Promise.withResolvers() @@ -120,7 +120,7 @@ function waitForMessage(messageID) { const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw() /** * @param {string} eventID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForReactionEvent(eventID) { const {promise, resolve} = Promise.withResolvers() @@ -156,4 +156,4 @@ module.exports.waitForMessage = waitForMessage module.exports.waitForEvent = waitForEvent module.exports.waitForReactionEvent = waitForReactionEvent module.exports.pauseChanges = pauseChanges -module.exports.finishedBridging = finishedBridging \ No newline at end of file +module.exports.finishedBridging = finishedBridging diff --git a/src/m2d/actions/send-event.js b/src/m2d/actions/send-event.js index 00557a1..2e6bf5c 100644 --- a/src/m2d/actions/send-event.js +++ b/src/m2d/actions/send-event.js @@ -153,7 +153,7 @@ async function sendEvent(event) { channel_id: messageResponse.channel_id, guild_id: guild.id, embeds: messageResponse.embeds - }, guild, null) + }, guild) ) } } diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index c014a3e..a017e99 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -32,6 +32,8 @@ const setupEmojis = sync.require("../actions/setup-emojis") const userToMxid = sync.require("../../d2m/converters/user-to-mxid") /** @type {import("../../web/routes/letter-avatar")} */ const letterAvatar = sync.require("../../web/routes/letter-avatar") +/** @type {import("../../d2m/actions/retrigger")} */ +const retrigger = sync.require("../../d2m/actions/retrigger") /** @type {[RegExp, string][]} */ const markdownEscapes = [ @@ -696,6 +698,7 @@ async function eventToMessage(event, guild, channel, di) { // Check if we have a pointer to what was edited const originalEventId = relatesTo.event_id if (!originalEventId) return + if (!await retrigger.waitForEvent(originalEventId)) return messageIDsToEdit = select("event_message", "message_id", {event_id: originalEventId}, "ORDER BY part").pluck().all() if (!messageIDsToEdit.length) return From c71120e463a43c113338265d1d1e902bfdb0f935 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 23:27:39 +1200 Subject: [PATCH 07/20] Better Discord startup checking --- src/d2m/discord-packets.js | 75 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 988f981..992689f 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const {scheduler} = require("timers/promises") const passthrough = require("../passthrough") const {sync} = passthrough @@ -8,7 +8,32 @@ const {sync} = passthrough /** @type {import("../matrix/homeserver-status")} */ const homeserverStatus = sync.require("../matrix/homeserver-status") -let checkedHomeserver = false +const guildReadyStatus = new class { + /** @type {Set | null} */ + unavailableGuilds = null + _allReady = Promise.withResolvers() + + /** + * @param {string} guildID + * @returns {boolean} true if it was the last one + */ + makeReady(guildID) { + assert(this.unavailableGuilds) + if (this.unavailableGuilds.delete(guildID) && this.unavailableGuilds.size === 0) { + this._allReady.resolve(null) + return true + } + return false + } + + allReady() { + return this.unavailableGuilds && this.unavailableGuilds.size === 0 + } + + waitForAllReady() { + return this._allReady.promise + } +} /** * @param {import("./discord-client")} client @@ -28,12 +53,27 @@ async function onPacket(client, message, listen) { client.ready = true client.user = message.d.user client.application = message.d.application + guildReadyStatus.unavailableGuilds = new Set(message.d.guilds.filter(g => g.unavailable).map(g => g.id)) console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + process.stdout.write("Waiting for guilds to warm up... ") 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 + message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot account's member - it's needed for roles to determine private channels on web client.guilds.set(message.d.id, message.d) + + /* + Info about guilds is populated one guild at a time. + For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild. + If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping. + We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver. + The "all guilds ready" delay can be removed if ONE of the following is done: + 1. m->d can queue incoming events until their guild exists in memory + 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first + */ + const firstReady = guildReadyStatus.allReady() + const lastGuildReady = guildReadyStatus.makeReady(message.d.id) + const arr = [] client.guildChannelMap.set(message.d.id, arr) for (const channel of message.d.channels || []) { @@ -51,28 +91,14 @@ async function onPacket(client, message, listen) { if (listen === "full") { try { - /* - Info about guilds is populated one guild at a time. - For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild. - If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping. - We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver. - This delay can be removed if ONE of the following is done: - 1. m->d can queue incoming events until their guild exists in memory - 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first - */ - let isMainCharacter = false - if (!checkedHomeserver) { - checkedHomeserver = true - isMainCharacter = true - console.log("Warming up guilds~") - } - await scheduler.wait(5000) - if (isMainCharacter) { - checkedHomeserver = true - process.stdout.write("Connecting to homeserver... ") + // Wait for guilds to be connected and homeserver to be online. If this is the last guild, a different code path is used to trigger the homeserver check. + if (lastGuildReady) { + process.stdout.write(`ok, ${client.guilds.size} available.\nConnecting to homeserver... `) + // await guildReadyStatus.waitForAllReady() - no need, we already checked this is the last guild await homeserverStatus.homeserverStatus.waitForOnline(true) console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.") } else { + await guildReadyStatus.waitForAllReady() await homeserverStatus.homeserverStatus.waitForOnline(false) } @@ -81,7 +107,9 @@ async function onPacket(client, message, listen) { 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:") + if (firstReady) { + console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") + } console.error(e) } } @@ -225,3 +253,4 @@ async function dispatchPacketToBridge(client, message) { module.exports.onPacket = onPacket module.exports.dispatchPacketToBridge = dispatchPacketToBridge +module.exports.guildReadyStatus = guildReadyStatus From 021f02f9813e2962c9629b406ec72bb5a58418f8 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 14 Sep 2026 00:30:03 +1200 Subject: [PATCH 08/20] d->m: Try too-large PNGs as JPEG for upload Large PNGs were almost certainly originally JPEG photos that were converted to PNG by the user copy-pasting. If the PNG is just supposed to be that large, you can still get the original through the external URL option. Had to change _removeExpiryParams to be based on URL parsing instead of regexp because regexp didn't work with &format=jpeg on the end. --- src/d2m/actions/send-message.js | 2 +- src/d2m/converters/edit-to-changes.js | 4 +- src/d2m/converters/message-to-event.js | 77 +++++++++++++++------ src/d2m/converters/message-to-event.test.js | 39 +++++++++++ src/matrix/file.js | 11 ++- src/matrix/file.test.js | 6 ++ test/ooye-test-data.sql | 3 +- 7 files changed, 113 insertions(+), 29 deletions(-) diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 41ef70d..6ad58c5 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -55,7 +55,7 @@ async function sendMessage(message, channel, guild) { } } - const events = await messageToEvent.messageToEvent(message, guild, {}, {api, snow: discord.snow}) + const events = await messageToEvent.messageToEvent(message, guild, {}, {api, snow: discord.snow, fetch}) const eventIDs = [] if (events.length) { db.prepare("INSERT OR IGNORE INTO message_room (message_id, historical_room_index) VALUES (?, ?)").run(message.id, historicalRoomIndex) diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index 61f3290..409adb0 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -69,8 +69,8 @@ async function editToChanges(message, guild, api) { // Figure out what we will be replacing them with - const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api}) - const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api}) + const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api, fetch}) + const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api, fetch}) assert.ok(newFallbackContent.length === newInnerContent.length) // Match the new events to the old events diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 619588e..49e3b64 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -109,10 +109,11 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ /** * @param {{room?: boolean, user_ids?: string[]}} mentions - * @param {Omit} attachment + * @param {Omit & {proxy_url?: string}} attachment * @param {boolean} [alwaysLink] + * @param {{fetch?: typeof fetch}} [di] */ -async function attachmentToEvent(mentions, attachment, alwaysLink) { +async function attachmentToEvent(mentions, attachment, alwaysLink, di) { const external_url = dUtils.getPublicUrlForCdn(attachment.url) const emoji = attachment.content_type?.startsWith("image/jp") ? "๐Ÿ“ธ" @@ -132,8 +133,37 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) { formatted_body: `
${emoji} Uploaded SPOILER file: ${external_url} (${pb(attachment.size)})
` } } + // attempt to convert large PNG image files to JPEG, since it's almost certainly a photo that was forced into PNG by the user copy-pasting. + if ( + attachment.content_type === "image/png" && attachment.size > reg.ooye.max_file_size + && !alwaysLink && attachment.proxy_url && attachment.width && attachment.height && di?.fetch + ) { + const proxyUrl = new URL(attachment.proxy_url) + proxyUrl.searchParams.set("format", "jpeg") + const jpegUrl = proxyUrl.toString() + const res = await di.fetch(jpegUrl, {method: "HEAD"}) + const newFilename = attachment.filename.replace(/\.png$/, ".jpg") + const newSize = Number(res.headers.get("content-length")) + if (res.ok && res.headers.get("content-type") === "image/jpeg" && !res.headers.has("content-encoding") && newSize <= reg.ooye.max_file_size) { + return { + $type: "m.room.message", + "m.mentions": mentions, + msgtype: "m.image", + url: await file.uploadDiscordFileToMxc(jpegUrl), + external_url, + body: attachment.description || newFilename, + filename: newFilename, + info: { + mimetype: "image/jpeg", + w: attachment.width, + h: attachment.height, + size: newSize + } + } + } + } // for large files, always link them instead of uploading so I don't use up all the space in the content repo - else if (alwaysLink || attachment.size > reg.ooye.max_file_size) { + if (alwaysLink || attachment.size > reg.ooye.max_file_size) { return { $type: "m.room.message", "m.mentions": mentions, @@ -142,7 +172,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) { format: "org.matrix.custom.html", formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` } - } else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { + } + if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { return { $type: "m.room.message", "m.mentions": mentions, @@ -158,7 +189,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) { size: attachment.size } } - } else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) { + } + if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) { return { $type: "m.room.message", "m.mentions": mentions, @@ -174,7 +206,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) { size: attachment.size } } - } else if (attachment.content_type?.startsWith("audio/")) { + } + if (attachment.content_type?.startsWith("audio/")) { return { $type: "m.room.message", "m.mentions": mentions, @@ -189,19 +222,19 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) { duration: attachment.duration_secs && Math.round(attachment.duration_secs * 1000) } } - } else { - return { - $type: "m.room.message", - "m.mentions": mentions, - msgtype: "m.file", - url: await file.uploadDiscordFileToMxc(attachment.url), - external_url, - body: attachment.description || attachment.filename, - filename: attachment.filename, - info: { - mimetype: attachment.content_type, - size: attachment.size - } + } + // else + return { + $type: "m.room.message", + "m.mentions": mentions, + msgtype: "m.file", + url: await file.uploadDiscordFileToMxc(attachment.url), + external_url, + body: attachment.description || attachment.filename, + filename: attachment.filename, + info: { + mimetype: attachment.content_type, + size: attachment.size } } } @@ -295,7 +328,7 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false * - includeEditFallbackStar: false * - alwaysReturnFormattedBody: false - formatted_body will be skipped if it is the same as body because the message is plaintext. if you want the formatted_body to be returned anyway, for example to merge it with another message, then set this to true. * - scanTextForMentions: true - needs to be set to false when converting forwarded messages etc which may be from a different channel that can't be scanned. - * @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer}} di simple-as-nails dependency injection for the matrix API + * @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer, fetch?: typeof fetch}} di simple-as-nails dependency injection for the matrix API * @returns {Promise<{$type: string, $sender?: string, [x: string]: any}[]>} */ async function messageToEvent(message, guild, options = {}, di) { @@ -882,7 +915,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Then attachments if (message.attachments) { - const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment))) + const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment, false, {fetch: di?.fetch}))) // Try to merge attachment events with the previous event // This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together. @@ -913,7 +946,7 @@ async function messageToEvent(message, guild, options = {}, di) { url: file.url, height: file.height, width: file.width, - }, true) + }, true, {fetch: di?.fetch}) stack.msb.addLine(ev.body, ev.formatted_body) } else if (component.type === DiscordTypes.ComponentType.MediaGallery) { diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 0742c7b..aea24a8 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1103,6 +1103,45 @@ test("message2event: very large attachment is linked instead of being uploaded", }]) }) +test("message2event: very large png is converted to jpeg for upload", async t => { + const events = await messageToEvent({ + content: "", + attachments: [{ + filename: "855064e9-bdae-487a-a497-c662bd510487.png", + url: "https://cdn.discordapp.com/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png", + proxy_url: "https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png", + content_type: "image/png", + width: 5472, + height: 3648, + size: 14967919 + }] + }, data.guild.general, {}, { + async fetch(url, init) { + t.equal(url, "https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg") + t.equal(init.method, "HEAD") + return new Response("wa", {headers: { + "Content-Type": "image/jpeg", + "Content-Length": 483085 + }}) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.image", + body: "855064e9-bdae-487a-a497-c662bd510487.jpg", + info: { + w: 5472, + h: 3648, + mimetype: "image/jpeg", + size: 483085 + }, + external_url: "https://bridge.example.org/download/discordcdn/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png", + filename: "855064e9-bdae-487a-a497-c662bd510487.jpg", + url: "mxc://cadence.moe/zXNXRvJwRDMIzZZVqHUXtGeD" + }]) +}) + test("message2event: multiple attachments are combined into the same event where possible", async t => { const events = await messageToEvent({ content: "hey", diff --git a/src/matrix/file.js b/src/matrix/file.js index c469aea..b338c59 100644 --- a/src/matrix/file.js +++ b/src/matrix/file.js @@ -15,10 +15,15 @@ const IMAGE_SIZE = 1024 const inflight = new Map() /** - * @param {string} url + * @param {string} urlString */ -function _removeExpiryParams(url) { - return url.replace(/\?(?:(?:ex|is|sg|hm)=[a-f0-9]+&?)*$/, "") +function _removeExpiryParams(urlString) { + const url = new URL(urlString) + url.searchParams.delete("ex") + url.searchParams.delete("is") + url.searchParams.delete("sg") + url.searchParams.delete("hm") + return url.toString() } /** diff --git a/src/matrix/file.test.js b/src/matrix/file.test.js index 2d59dbc..3a5c73b 100644 --- a/src/matrix/file.test.js +++ b/src/matrix/file.test.js @@ -20,3 +20,9 @@ test("removeExpiryParams: rearranged params are removed", t => { const result = file._removeExpiryParams(url) t.equal(result, "https://cdn.discordapp.com/attachments/112760669178241024/1157363960518029322/image.png") }) + +test("removeExpiryParams: works on media proxy and keeps quality", t => { + const url = "https://media.discordapp.net/attachments/1160894080998461480/1548665030898094240/855064e9-bdae-487a-a497-c662bd510487.png?ex=6aa7e234&is=6aa690b4&hm=7c153efcdd1feb1d36d0bb99955a2bfe12f26abea377fed02980521766f51620&format=jpeg" + const result = file._removeExpiryParams(url) + t.equal(result, "https://media.discordapp.net/attachments/1160894080998461480/1548665030898094240/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg") +}) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 524e1fe..072397f 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -168,7 +168,8 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'), ('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'), ('https://cdn.discordapp.com/attachments/123/456/my_enemies.txt', 'mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge'), -('https://cdn.discordapp.com/emojis/1254940125948022915.webp', 'mxc://cadence.moe/bvVJFgOIyNcAknKCbmaHDktG'); +('https://cdn.discordapp.com/emojis/1254940125948022915.webp', 'mxc://cadence.moe/bvVJFgOIyNcAknKCbmaHDktG'), +('https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg', 'mxc://cadence.moe/zXNXRvJwRDMIzZZVqHUXtGeD'); INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), From 3d648e23c82422930f9cf54d389cd15d416c330e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 14 Sep 2026 16:20:56 +1200 Subject: [PATCH 09/20] Check for homeserver liveliness more --- src/matrix/homeserver-status.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/matrix/homeserver-status.js b/src/matrix/homeserver-status.js index 3ceac84..da38ac2 100644 --- a/src/matrix/homeserver-status.js +++ b/src/matrix/homeserver-status.js @@ -27,13 +27,21 @@ const homeserverStatus = new class HomeserverStatus { /** @private */ this.pingInterval = undefined + /** @private */ + this.checkingWhileOnline = true + + /** @private */ + this.lastCheck = 0 + /** @private */ this.sm = new StateMachine("online") .defineState("online") .defineState("checking", { - onEnter: [async () => { + onEnter: [async (event) => { + this.checkingWhileOnline = event === "check while online" const pingResult = await api.ping().catch(e => ({ok: false})) + this.lastCheck = Date.now() if (pingResult.ok) { this.sm.doTransition("check ok") } else { @@ -48,6 +56,7 @@ const homeserverStatus = new class HomeserverStatus { onEnter: [() => { this.pingInterval = setInterval(async () => { const pingResult = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) + this.lastCheck = Date.now() if (pingResult.ok) { this.sm.doTransition("ping ok") } @@ -77,7 +86,8 @@ const homeserverStatus = new class HomeserverStatus { .defineUniversalTransition("error", "offline") .defineTransition("offline", "ping ok", "recovering") .defineTransition("recovering", "recovered", "online") - .defineTransition("online", "check", "checking") + .defineTransition("online", "check assume online", "checking") + .defineTransition("online", "check assume offline", "checking") .defineTransition("checking", "check ok", "recovering") .defineTransition("checking", "check fail", "offline") @@ -87,7 +97,7 @@ const homeserverStatus = new class HomeserverStatus { } isRealTime() { - return this.sm.currentStateName === "online" + return this.sm.currentStateName === "online" || (this.sm.currentStateName === "checking" && this.checkingWhileOnline) } /** @param {boolean} forceCheck */ @@ -96,7 +106,9 @@ const homeserverStatus = new class HomeserverStatus { // Already online? Start check or just done if (this.sm.currentStateName === "online") { if (forceCheck) { - this.sm.doTransition("check") + this.sm.doTransition("check assume offline") + } else if (this.lastCheck < Date.now() - 15e3) { + this.sm.doTransition("check assume online") } else { return resolve(null) } From 2430d60809f6fb914258a64ba07c735831c729b0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 14 Sep 2026 16:22:00 +1200 Subject: [PATCH 10/20] v3.7 --- package-lock.json | 242 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 122 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index 986e6ca..ee24410 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.7.0", "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", @@ -302,9 +302,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -320,13 +320,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -342,20 +342,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -365,9 +365,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -413,9 +413,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -429,9 +429,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -445,9 +445,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -477,9 +477,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -543,13 +543,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -565,13 +565,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -587,13 +587,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -609,13 +609,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -631,13 +631,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -653,13 +653,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -675,13 +675,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -697,17 +697,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -717,16 +717,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -736,9 +736,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -755,9 +755,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -774,9 +774,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -2490,9 +2490,9 @@ } }, "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -2506,31 +2506,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" }, "peerDependenciesMeta": { "@types/node": { diff --git a/package.json b/package.json index 06b2a49..00f11b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.7.0", "description": "A bridge between Matrix and Discord", "main": "index.js", "repository": { From 9058b60efed49cf071b5d89e3e19fa090e2d3538 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 15 Sep 2026 01:38:47 +1200 Subject: [PATCH 11/20] Update dependencies --- docs/developer-orientation.md | 20 +- package-lock.json | 755 +++++++------------- package.json | 8 +- scripts/start-server.js | 3 - src/d2m/actions/register-webhook-user.js | 2 +- src/d2m/converters/find-mentions.js | 2 +- src/d2m/converters/lottie.js | 2 +- src/d2m/converters/user-to-mxid.js | 6 +- src/d2m/discord-packets.js | 6 +- src/m2d/converters/event-to-message.js | 2 +- src/m2d/converters/event-to-message.test.js | 33 + src/matrix/homeserver-status.js | 2 +- src/stdin.js | 1 + src/web/server.js | 2 +- 14 files changed, 333 insertions(+), 511 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index 0f5e0c0..839b4af 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,14 +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: 144 +Total transitive production dependencies: 112 ### ๐Ÿฆ• -* (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. +* (1) better-sqlite3: SQLite is the best database, and this is the best library for it. ### ๐Ÿชฑ @@ -120,6 +120,7 @@ Total transitive production dependencies: 144 * (0) entities: Looks fine. No dependencies. * (0) get-relative-path: Looks fine. No dependencies. * (1) heatsync: Module hot-reloader that I trust. +* (0) htmx.org: Declarative partial page reloads that do just enough. I'm not totally happy with this one, but not sure what I'd do instead. * (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used. * (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. @@ -128,3 +129,18 @@ Total transitive production dependencies: 144 * (0) uqr: QR code SVG generator. Used on the website to scan in an invite link. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. * (0) zod: Input validation for the web server. It's popular and easy to use. + +### Outdated versions + +* @chriscdn/promise-semaphore + * Highest usable version is 3.1.3 because the author **dropped CJS** in 4.0.0. +* @stackoverflow/stacks + * Highest usable version is 2.9.0 (and 6.9.0 for stacks-icons) because the company **ruined their design** in 3.0.0. +* cross-env + * Highest usable version is 7.0.3 because the author **dropped CJS** in 10.0.0. +* entities + * Using version 5.0.0. The author messed things up in 6.0.0, started using LLMs in 7.0.0, and **dropped CJS** in 8.0.0. +* h3 + * Using major version 1. Version 2 has been stuck as **release candidates for months**, and all the code comments are Claude. +* htmx.org + * Using major version 2. Version 4 exists, and **I'd be okay updating to it**, but the author changed a lot and the entire site would need to be re-tested. diff --git a/package-lock.json b/package-lock.json index ee24410..9e35392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", - "better-sqlite3": "^12.2.0", + "better-sqlite3": "^13.0.3", "chunk-text": "^2.0.1", "cloudstorm": "^0.19.0", "denque": "^2.1.0", @@ -32,10 +32,10 @@ "entities": "^5.0.0", "get-relative-path": "^1.0.2", "h3": "^1.15.10", - "heatsync": "^2.7.2", + "heatsync": "^3.3.0", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", - "mime-types": "^2.1.35", + "mime-types": "^3.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", @@ -47,7 +47,7 @@ "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^22.17.1", - "c8": "^11.0.0", + "c8": "^12.0.0", "cross-env": "^7.0.3", "supertape": "^13.2.0" }, @@ -909,9 +909,9 @@ "license": "MIT" }, "node_modules/@stackoverflow/stacks": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.8.6.tgz", - "integrity": "sha512-pR0vMDBA5rNV5Cb/McG+2F1nG68fZk3UiNwuOvQrP82WoQy41k9PsBjRjD7wHDVVZyDrWHLb/WnosXX7uK3IdA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.9.0.tgz", + "integrity": "sha512-6VX9GTYFqsBFaelZ0sJ9YGZDa+1eLjr6Dh8r6cIh02SA/BcocWe0lxNZTcLpBQEKtX/9A0jTRsY3Au0BXcFJWQ==", "dependencies": { "@hotwired/stimulus": "^3.2.2", "@popperjs/core": "^2.11.8" @@ -1039,9 +1039,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "version": "22.20.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", "dev": true, "license": "MIT", "dependencies": { @@ -1129,9 +1129,9 @@ } }, "node_modules/backtracker": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-5.0.0.tgz", - "integrity": "sha512-2rY1s1iMlF1FVb4jpIMxTeGE+KRppuVvPyU61q7gvap1MWVahToUI8WUqy+v3L37iip5a4mJOTRBZxNRbTv4bg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-5.1.1.tgz", + "integrity": "sha512-+/rMk2Eyzc/k6AqguRxdlUVOzMmyjUqavmICnwI5XGNBpbKLxAu5Bps3G7yRvxCg0b5zc5crZoSgpPTo9i5lbQ==", "license": "MIT" }, "node_modules/balanced-match": { @@ -1144,58 +1144,16 @@ "node": "18 || 20 || >=22" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/better-sqlite3": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", - "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", - "hasInstallScript": true, + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "license": "MIT", "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "node-addon-api": "^8.0.0" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" - } - }, - "node_modules/bindings": { - "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" - } - }, - "node_modules/bl": { - "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": ">=22" } }, "node_modules/brace-expansion": { @@ -1211,34 +1169,10 @@ "node": "20 || >=22" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/c8": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", - "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", "dev": true, "license": "ISC", "dependencies": { @@ -1251,14 +1185,14 @@ "istanbul-reports": "^3.1.6", "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", + "yargs": "^18.0.0", "yargs-parser": "^21.1.1" }, "bin": { "c8": "bin/c8.js" }, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || ^22.12.0 || >=23" }, "peerDependencies": { "monocart-coverage-reports": "^2" @@ -1288,12 +1222,6 @@ "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==", - "license": "ISC" - }, "node_modules/chunk-text": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/chunk-text/-/chunk-text-2.0.1.tgz", @@ -1337,18 +1265,72 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/cloudstorm": { @@ -1463,30 +1445,6 @@ "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "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" - } - }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", @@ -1518,13 +1476,10 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.53", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.53.tgz", - "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==", - "license": "MIT", - "workspaces": [ - "scripts/actions/documentation" - ] + "version": "0.38.55", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.55.tgz", + "integrity": "sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==", + "license": "MIT" }, "node_modules/doctypes": { "version": "1.1.0", @@ -1533,9 +1488,9 @@ "license": "MIT" }, "node_modules/domino": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.7.tgz", - "integrity": "sha512-3rcXhx0ixJV2nj8J0tljzejTF73A35LVVdnTQu79UAqTBFEgYPMgGtykMuu/BDqaOZphATku1ddRUn/RtqUHYQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.8.tgz", + "integrity": "sha512-qLkTcPRkqvifAB0bweVznkWgAu2hwin8An+HbLy0bH1ZtMyJzwXiHKMhynwR1Wq4PgBvEyg2Y749j4JJnlfDKg==", "license": "BSD-2-Clause" }, "node_modules/emoji-regex": { @@ -1545,15 +1500,6 @@ "dev": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "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" - } - }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -1589,15 +1535,6 @@ "node": ">=6" } }, - "node_modules/expand-template": { - "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" - } - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1608,12 +1545,6 @@ "node": ">= 4.9.1" } }, - "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==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1655,12 +1586,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "license": "MIT" - }, "node_modules/fullstore": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.2.tgz", @@ -1691,6 +1616,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-relative-path": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-relative-path/-/get-relative-path-1.0.2.tgz", @@ -1708,12 +1646,6 @@ "source-map": "^0.6.1" } }, - "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==", - "license": "MIT" - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -1773,15 +1705,15 @@ } }, "node_modules/heatsync": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.8.3.tgz", - "integrity": "sha512-9pVRC3BZD1NZ0EYnU5akjoO10+s/aJc04QqUxgtBqAYUeberV8st0ctWH7selEnyU8OEAUKZhBCFxmH7MvCQQQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-3.3.0.tgz", + "integrity": "sha512-9qkijny0a9lo0He103na+sGVx8YRcDKizfGNBahkx5OX6LmeR8njNOftRFSCuSRkNqrNWuxRFOOVKIiY0FsEVQ==", "license": "MIT", "dependencies": { - "backtracker": "^5.0.0" + "backtracker": "^5.1.1" }, "engines": { - "node": ">=14.6.0" + "node": ">=20.6.0" } }, "node_modules/html-es6cape": { @@ -1803,38 +1735,6 @@ "integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==", "license": "0BSD" }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "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==", - "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==", - "license": "ISC" - }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -2019,9 +1919,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2044,36 +1944,28 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "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" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimatch": { @@ -2092,15 +1984,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "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" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -2111,28 +1994,13 @@ "node": ">=16 || 14 >=14.17" } }, - "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==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "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.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, "engines": { - "node": ">=10" + "node": "^18 || ^20 || >= 21" } }, "node_modules/node-mock-http": { @@ -2154,6 +2022,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2235,33 +2104,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/prebuild-install": { - "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": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prettier-bytes": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", @@ -2358,37 +2200,12 @@ "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", "license": "MIT" }, - "node_modules/pump": { - "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" - } - }, "node_modules/radix3": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, - "node_modules/rc": { - "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", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/re-emitter": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", @@ -2403,30 +2220,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "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.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2457,26 +2250,6 @@ "node": ">=4.0.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2574,51 +2347,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-markdown": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz", @@ -2671,15 +2399,6 @@ "get-source": "^2.0.12" } }, - "node_modules/string_decoder": { - "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" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2707,19 +2426,10 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "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": "13.2.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.2.0.tgz", - "integrity": "sha512-UoxZnyoMOdSJHvbcmD8i28MaGXsA7I0cJ0jr8anT4CkmfaE9M1y5mt9EoXyzfC8UdnQZwXOnJLUwqyKLAeUOug==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.6.0.tgz", + "integrity": "sha512-W/lm1wk370QF0u+LrxS+BMUFNYMfRgjFC9dTKyFB522fa77pKJHwOBDiTl3uhNbsw0Sb1NQPRYk+eFICcqxedA==", "dev": true, "license": "MIT", "dependencies": { @@ -2792,34 +2502,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "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", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -2874,18 +2556,6 @@ "license": "0BSD", "optional": true }, - "node_modules/tunnel-agent": { - "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" - }, - "engines": { - "node": "*" - } - }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", @@ -2911,12 +2581,6 @@ "integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==", "license": "MIT" }, - "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==", - "license": "MIT" - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -2973,43 +2637,95 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "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==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?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==", + "dev": true, "license": "ISC" }, "node_modules/wraptile": { @@ -3036,22 +2752,21 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { @@ -3064,6 +2779,62 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3078,9 +2849,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 00f11b7..5fbf928 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", - "better-sqlite3": "^12.2.0", + "better-sqlite3": "^13.0.3", "chunk-text": "^2.0.1", "cloudstorm": "^0.19.0", "denque": "^2.1.0", @@ -41,10 +41,10 @@ "entities": "^5.0.0", "get-relative-path": "^1.0.2", "h3": "^1.15.10", - "heatsync": "^2.7.2", + "heatsync": "^3.3.0", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", - "mime-types": "^2.1.35", + "mime-types": "^3.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", @@ -59,7 +59,7 @@ "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^22.17.1", - "c8": "^11.0.0", + "c8": "^12.0.0", "cross-env": "^7.0.3", "supertape": "^13.2.0" }, diff --git a/scripts/start-server.js b/scripts/start-server.js index 44edbcb..2cfee68 100755 --- a/scripts/start-server.js +++ b/scripts/start-server.js @@ -1,9 +1,6 @@ #!/usr/bin/env node // @ts-check -const {createServer} = require("http") -const EventEmitter = require("events") -const {createApp, createRouter, toNodeListener} = require("h3") const sqlite = require("better-sqlite3") const migrate = require("../src/db/migrate") const HeatSync = require("heatsync") diff --git a/src/d2m/actions/register-webhook-user.js b/src/d2m/actions/register-webhook-user.js index 145eeb8..f016843 100644 --- a/src/d2m/actions/register-webhook-user.js +++ b/src/d2m/actions/register-webhook-user.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") diff --git a/src/d2m/converters/find-mentions.js b/src/d2m/converters/find-mentions.js index 5135d17..e9c51c5 100644 --- a/src/d2m/converters/find-mentions.js +++ b/src/d2m/converters/find-mentions.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const {reg} = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) diff --git a/src/d2m/converters/lottie.js b/src/d2m/converters/lottie.js index 969d345..6641958 100644 --- a/src/d2m/converters/lottie.js +++ b/src/d2m/converters/lottie.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const stream = require("stream") const {PNG} = require("@cloudrac3r/pngjs") diff --git a/src/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js index 7705aff..fb37aa1 100644 --- a/src/d2m/converters/user-to-mxid.js +++ b/src/d2m/converters/user-to-mxid.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") @@ -66,7 +66,7 @@ function userToSimName(user) { // 1. Is sim user already registered? const existing = select("sim", "user_id", {user_id: user.id}).pluck().get() - assert.equal(existing, null, "Shouldn't try to create a new name for an existing sim") + assert.equal(existing, undefined, "Shouldn't try to create a new name for an existing sim") // 2. Register based on username (could be new or old format) // (Unless it's a special user, in which case copy their provided mappings.) @@ -113,7 +113,7 @@ function webhookAuthorToSimName(author) { // 1. Is sim user already registered? const fakeUserID = webhookAuthorToFakeUserID(author) const existing = select("sim", "user_id", {user_id: fakeUserID}).pluck().get() - assert.equal(existing, null, "Shouldn't try to create a new name for an existing sim") + assert.equal(existing, undefined, "Shouldn't try to create a new name for an existing sim") // 2. Register based on username (could be new or old format) const downcased = "webhook_" + downcaseUsername(author) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 992689f..9f3b291 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -74,6 +74,10 @@ async function onPacket(client, message, listen) { const firstReady = guildReadyStatus.allReady() const lastGuildReady = guildReadyStatus.makeReady(message.d.id) + if (lastGuildReady) { + console.log(`ok, ${client.guilds.size} available.`) + } + const arr = [] client.guildChannelMap.set(message.d.id, arr) for (const channel of message.d.channels || []) { @@ -93,7 +97,7 @@ async function onPacket(client, message, listen) { try { // Wait for guilds to be connected and homeserver to be online. If this is the last guild, a different code path is used to trigger the homeserver check. if (lastGuildReady) { - process.stdout.write(`ok, ${client.guilds.size} available.\nConnecting to homeserver... `) + process.stdout.write(`Connecting to homeserver... `) // await guildReadyStatus.waitForAllReady() - no need, we already checked this is the last guild await homeserverStatus.homeserverStatus.waitForOnline(true) console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.") diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index a017e99..5fe1109 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -620,7 +620,7 @@ async function eventToMessage(event, guild, channel, di) { let filename = event.content.filename || event.content.body if (!filename) { const extension = mimeTypes.extension(event.content.info?.mimetype) - if (!extension) throw new Error("Filename and mimetype missing. Please report this bug to your client.") + assert(extension, "Filename and mimetype missing. Please report this bug to your client.") filename = `file.${extension}` } diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 6b22773..b262a37 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -4816,6 +4816,39 @@ test("event2message: image attachments can have a formatted caption", async t => ) }) +test("event2message: attachments have a generated filename if it was missing", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + body: "", + filename: "", + info: { + mimetype: "image/png", + size: 50832 + }, + msgtype: "m.image", + url: "mxc://agiadn.org/3SvzETkab3XBDM6HCMJxpOryIDxMO31j" + }, + event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI", + room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + attachments: [{id: "0", filename: "file.png"}], + pendingFiles: [{name: "file.png", mxc: "mxc://agiadn.org/3SvzETkab3XBDM6HCMJxpOryIDxMO31j"}] + }] + } + ) +}) + test("event2message: encrypted image attachments work", async t => { t.deepEqual( await eventToMessage({ diff --git a/src/matrix/homeserver-status.js b/src/matrix/homeserver-status.js index da38ac2..6d0ea76 100644 --- a/src/matrix/homeserver-status.js +++ b/src/matrix/homeserver-status.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const Denque = require("denque") const StateMachine = require("snowtransfer").StateMachine diff --git a/src/stdin.js b/src/stdin.js index 43f9607..6e35e78 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -7,6 +7,7 @@ const {addbot} = require("../addbot") const passthrough = require("./passthrough") const {discord, sync, db, select, from, as} = passthrough +const reg = require("./matrix/read-registration") const data = sync.require("../test/data") const createSpace = sync.require("./d2m/actions/create-space") const createRoom = sync.require("./d2m/actions/create-room") diff --git a/src/web/server.js b/src/web/server.js index c8d363d..e2ce1b5 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const fs = require("fs") const {join} = require("path") const h3 = require("h3") From 2959fd8ae3823b08d80b09869295a1e7e9f891de Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 15 Sep 2026 23:37:45 +1200 Subject: [PATCH 12/20] Render pug blocks without a separate template --- .vscode/settings.json | 8 +- package-lock.json | 1 + scripts/start-server.js | 8 +- src/js/errors.js | 53 +++++++++++ src/js/errors.test.js | 23 +++++ src/m2d/event-dispatcher.js | 57 +----------- src/m2d/event-dispatcher.test.js | 21 +---- src/web/pug-sync.js | 96 +++++++++++++++++--- src/web/pug/fragments/default-roles-list.pug | 5 - src/web/pug/guild.pug | 25 ++++- src/web/pug/includes/default-roles-list.pug | 19 ---- src/web/pug/invite.pug | 4 +- src/web/routes/guild-settings.js | 2 +- src/web/server.js | 2 +- test/test.js | 1 + 15 files changed, 199 insertions(+), 126 deletions(-) create mode 100644 src/js/errors.js create mode 100644 src/js/errors.test.js delete mode 100644 src/web/pug/fragments/default-roles-list.pug delete mode 100644 src/web/pug/includes/default-roles-list.pug diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f1e183..61015ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,11 @@ { "editor.insertSpaces": false, "editor.detectIndentation": false, - "editor.tabSize": 3 + "editor.tabSize": 3, + "[jade]": { + "editor.stickyScroll.enabled": true, + "editor.stickyScroll.maxLineCount": 6, + "editor.tabSize": 2, + "editor.insertSpaces": true + } } diff --git a/package-lock.json b/package-lock.json index 9e35392..d9ff5cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1148,6 +1148,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "hasInstallScript": true, "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" diff --git a/scripts/start-server.js b/scripts/start-server.js index 2cfee68..883333d 100755 --- a/scripts/start-server.js +++ b/scripts/start-server.js @@ -13,6 +13,10 @@ const sync = new HeatSync() Object.assign(passthrough, {sync, db}) +const orm = sync.require("../src/db/orm") +passthrough.from = orm.from +passthrough.select = orm.select + const DiscordClient = require("../src/d2m/discord-client") const discord = new DiscordClient(reg.ooye.discord_token, "half") @@ -21,10 +25,6 @@ passthrough.discord = discord const {as} = require("../src/matrix/appservice") passthrough.as = as -const orm = sync.require("../src/db/orm") -passthrough.from = orm.from -passthrough.select = orm.select - ;(async () => { await migrate.migrate(db) await discord.cloud.connect() diff --git a/src/js/errors.js b/src/js/errors.js new file mode 100644 index 0000000..7770772 --- /dev/null +++ b/src/js/errors.js @@ -0,0 +1,53 @@ +/** + * This function is adapted from Evan Kaufman's fantastic work. + * The original function and my adapted function are both MIT licensed. + * @url https://github.com/EvanK/npm-loggable-error/ + * @param {number} [depth] + * @returns {string} +*/ +function stringifyErrorStack(err, depth = 0) { + let collapsed = " ".repeat(depth); + if (!(err instanceof Error)) { + return collapsed + err + } + + // add full stack trace if one exists, otherwise convert to string + let stackLines = String(err?.stack ?? err).replace(/^/gm, " ".repeat(depth)).trim().split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) + } + collapsed += stackLines.join("\n") + + const props = Object.getOwnPropertyNames(err).filter(p => !["message", "stack"].includes(p)) + + // only break into object notation if we have additional props to dump + if (props.length) { + const dedent = " ".repeat(depth); + const indent = " ".repeat(depth + 2); + + collapsed += " {\n"; + + // loop and print each (indented) prop name + for (let property of props) { + collapsed += `${indent}[${property}]: `; + + // if another error object, stringify it too + if (err[property] instanceof Error) { + collapsed += stringifyErrorStack(err[property], depth + 2).trimStart(); + } + // otherwise stringify as JSON + else { + collapsed += JSON.stringify(err[property]); + } + + collapsed += "\n"; + } + + collapsed += `${dedent}}\n`; + } + + return collapsed; +} + +module.exports.stringifyErrorStack = stringifyErrorStack diff --git a/src/js/errors.test.js b/src/js/errors.test.js new file mode 100644 index 0000000..ddff7d3 --- /dev/null +++ b/src/js/errors.test.js @@ -0,0 +1,23 @@ +// @ts-check + +const {test} = require("supertape") +const {stringifyErrorStack} = require("./errors") + +test("stringify error stack: works", t => { + function a() { + const e = new Error("message", {cause: new Error("inner")}) + // @ts-ignore + e.prop = 2.1 + throw e + } + try { + a() + t.fail("shouldn't get here") + } catch (e) { + const str = stringifyErrorStack(e) + t.match(str, /^Error: message$/m) + t.match(str, /^ at a \(.*errors\.test\.js/m) + t.match(str, /^ \[cause\]: Error: inner$/m) + t.match(str, /^ \[prop\]: 2.1$/m) + } +}) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 0560f46..f6f414a 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -38,62 +38,12 @@ const retrigger = sync.require("../d2m/actions/retrigger") const homeserverStatus = sync.require("../matrix/homeserver-status") /** @type {import("./actions/typing")} */ const typing = sync.require("./actions/typing") +/** @type {import("../js/errors")} */ +const errors = sync.require("../js/errors") const {reg} = require("../matrix/read-registration") let lastReportedEvent = 0 -/** - * This function is adapted from Evan Kaufman's fantastic work. - * The original function and my adapted function are both MIT licensed. - * @url https://github.com/EvanK/npm-loggable-error/ - * @param {number} [depth] - * @returns {string} -*/ -function stringifyErrorStack(err, depth = 0) { - let collapsed = " ".repeat(depth); - if (!(err instanceof Error)) { - return collapsed + err - } - - // add full stack trace if one exists, otherwise convert to string - let stackLines = String(err?.stack ?? err).replace(/^/gm, " ".repeat(depth)).trim().split("\n") - let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) - if (cloudstormLine !== -1) { - stackLines = stackLines.slice(0, cloudstormLine - 2) - } - collapsed += stackLines.join("\n") - - const props = Object.getOwnPropertyNames(err).filter(p => !["message", "stack"].includes(p)) - - // only break into object notation if we have additional props to dump - if (props.length) { - const dedent = " ".repeat(depth); - const indent = " ".repeat(depth + 2); - - collapsed += " {\n"; - - // loop and print each (indented) prop name - for (let property of props) { - collapsed += `${indent}[${property}]: `; - - // if another error object, stringify it too - if (err[property] instanceof Error) { - collapsed += stringifyErrorStack(err[property], depth + 2).trimStart(); - } - // otherwise stringify as JSON - else { - collapsed += JSON.stringify(err[property]); - } - - collapsed += "\n"; - } - - collapsed += `${dedent}}\n`; - } - - return collapsed; -} - function printError(type, source, e, payload) { console.error(`Error while processing a ${type} ${source} event:`) console.error(e) @@ -145,7 +95,7 @@ async function sendError(roomID, source, type, e, payload) { builder.addLine(errorIntroLine) // Where - const stack = cleanErrorStack(stringifyErrorStack(e)) + const stack = cleanErrorStack(errors.stringifyErrorStack(e)) builder.addLine(`Error trace:\n${stack}`, tag`
Error trace
${stack}
`) // How @@ -543,7 +493,6 @@ async event => { } })) -module.exports.stringifyErrorStack = stringifyErrorStack module.exports.cleanErrorStack = cleanErrorStack module.exports.sendError = sendError module.exports.printError = printError diff --git a/src/m2d/event-dispatcher.test.js b/src/m2d/event-dispatcher.test.js index 2de6381..302b33b 100644 --- a/src/m2d/event-dispatcher.test.js +++ b/src/m2d/event-dispatcher.test.js @@ -1,26 +1,7 @@ // @ts-check const {test} = require("supertape") -const {stringifyErrorStack, cleanErrorStack} = require("./event-dispatcher") - -test("stringify error stack: works", t => { - function a() { - const e = new Error("message", {cause: new Error("inner")}) - // @ts-ignore - e.prop = 2.1 - throw e - } - try { - a() - t.fail("shouldn't get here") - } catch (e) { - const str = stringifyErrorStack(e) - t.match(str, /^Error: message$/m) - t.match(str, /^ at a \(.*event-dispatcher\.test\.js/m) - t.match(str, /^ \[cause\]: Error: inner$/m) - t.match(str, /^ \[prop\]: 2.1$/m) - } -}) +const {cleanErrorStack} = require("./event-dispatcher") test("clean error stack: removes webhook token", t => { t.notMatch( diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js index e61c53b..cca90c2 100644 --- a/src/web/pug-sync.js +++ b/src/web/pug-sync.js @@ -2,20 +2,24 @@ const assert = require("assert/strict") const fs = require("fs") +const util = require("util") const {join} = require("path") const getRelativePath = require("get-relative-path") const h3 = require("h3") -const {defineEventHandler, defaultContentType, setResponseStatus, getQuery} = h3 +const {defineEventHandler, defaultContentType, setResponseStatus, getQuery, getRequestURL} = h3 const {compileFile} = require("@cloudrac3r/pug") +const walkAST = require("pug-walk") const pretty = process.argv.join(" ").includes("test") const {sync} = require("../passthrough") /** @type {import("./auth")} */ const auth = sync.require("./auth") +/** @type {import("../js/errors")} */ +const errors = sync.require("../js/errors") // Pug -let globals = {} +let globals = sync.remember(() => ({})) /** @type {Map) => Promise>} */ const pugCache = new Map() @@ -30,21 +34,60 @@ function addGlobals(obj) { * @param {Record} locals */ function render(event, filename, locals) { + return renderFragments(event, filename, null, locals) +} + +/** + * @param {import("h3").H3Event} event + * @param {string} filename + * @param {string[] | null} fragments + * @param {Record} locals + */ +function renderFragments(event, filename, fragments, locals) { const path = join(__dirname, "pug", filename) - return renderPath(event, path, locals) + return renderPath(event, path, fragments, locals) +} + +/** + * @param {string[] | null} fragments + * @param {string[]} requiresOut + */ +function fragmentSelectorPlugins(fragments, requiresOut) { + if (!fragments) return [] + return [{ + parse: true, + postParse(ast, options) { + const found = [] + walkAST(ast, (node, replace) => { + if (node.type === "NamedBlock") { + const [name, requires] = node.name.split("requires").map(x => x.trim()) + // console.log(name, "|", requires) + if (fragments?.includes(name)) { + if (requires) requiresOut.push(...requires.split(",").map(x => x.trim())) + found.push(node) + } + } + }) + // console.log(util.inspect(found, false, 9, true)) + return {type: "Block", nodes: found, line: 0, filename: found[0].filename} + } + }] } /** * @param {import("h3").H3Event} event * @param {string} path + * @param {string[] | null} fragments * @param {Record} locals */ -function renderPath(event, path, locals) { +function renderPath(event, path, fragments, locals) { + const cacheKey = path + (fragments ? fragments.map(f => `#${f}`).join("") : "") + function compile() { try { - const template = compileFile(path, {pretty}) - pugCache.set(path, async (event, locals) => { - defaultContentType(event, "text/html; charset=utf-8") + const requiredVars = [] + const template = compileFile(path, {pretty, plugins: fragmentSelectorPlugins(fragments, requiredVars)}) + pugCache.set(cacheKey, async (event, locals) => { const session = await auth.useSession(event) const managed = await auth.getManagedGuilds(event) const rel = (to, paramsObject) => { @@ -55,16 +98,36 @@ function renderPath(event, path, locals) { } return result } - return template(Object.assign({}, + const templateVars = Object.assign({}, getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything globals, // Globals locals, // Explicit locals overwrite globals in case we need to DI something {session, event, rel, managed} // These are assigned last so they overwrite everything else. It would be catastrophically bad if they can't be trusted. - )) + ) + // Verify all fragment block required variables are present + for (const key of requiredVars) { + if (templateVars[key] === undefined) { + setResponseStatus(event, 500, "Internal Template Variables Error") + defaultContentType(event, "text/plain") + return `requested fragments ${JSON.stringify(fragments)}\nrequire variables ${JSON.stringify(requiredVars)}\nbut "${key}" is ${templateVars[key]} (present: ${key in templateVars})` + } + } + try { + const content = template(templateVars) + defaultContentType(event, "text/html; charset=utf-8") + return content + } catch (e) { + console.error(e) + setResponseStatus(event, 500, "Internal Template Rendering Error") + defaultContentType(event, "text/plain") + e.url = getRequestURL(event).toString() + return errors.stringifyErrorStack(e) + } }) /* c8 ignore start */ } catch (e) { - pugCache.set(path, async (event) => { + pugCache.set(cacheKey, async (event) => { + console.error(e) setResponseStatus(event, 500, "Internal Template Error") defaultContentType(event, "text/plain") return e.toString() @@ -73,14 +136,16 @@ function renderPath(event, path, locals) { /* c8 ignore stop */ } - if (!pugCache.has(path)) { + if (!pugCache.has(cacheKey)) { compile() - fs.watch(path, {persistent: false}, compile) - fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile) - fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile) + const ac = new AbortController() + const {signal} = ac + fs.watch(path, {persistent: false, signal}, compile) + fs.watch(join(__dirname, "pug", "includes"), {persistent: false, signal}, compile) + sync.events.once(__filename, () => ac.abort()) } - const cb = pugCache.get(path) + const cb = pugCache.get(cacheKey) assert(cb) return cb(event, locals) } @@ -98,5 +163,6 @@ function createRoute(router, url, filename) { module.exports.addGlobals = addGlobals module.exports.render = render +module.exports.renderFragments = renderFragments module.exports.renderPath = renderPath module.exports.createRoute = createRoute diff --git a/src/web/pug/fragments/default-roles-list.pug b/src/web/pug/fragments/default-roles-list.pug deleted file mode 100644 index 3b36549..0000000 --- a/src/web/pug/fragments/default-roles-list.pug +++ /dev/null @@ -1,5 +0,0 @@ -//- locals: guild, guild_id - -include ../includes/default-roles-list.pug -+default-roles-list(guild, guild_id) -+add-roles-menu(guild, guild_id) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 7411a1e..c464954 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -1,5 +1,4 @@ extends includes/template.pug -include includes/default-roles-list.pug mixin badge-readonly .s-badge.s-badge__xs.s-badge__icon.s-badge__muted @@ -113,15 +112,33 @@ block body .d-flex.fw-wrap.g4 .s-tag.s-tag__md.fs-body1.s-tag__required @everyone - +default-roles-list(guild, guild_id) + block default-roles-list requires guild, guild_id + #default-roles-list(style="display: contents") + each roleID in select("role_default", "role_id", {guild_id}).pluck().all() + - let r = guild.roles.find(r => r.id === roleID) + if r + .s-tag.s-tag__md.fs-body1= r.name + span(id=`role-loading-${roleID}`) + button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss + != icons.Icons.IconClearSm button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0 .s-tag--dismiss.m1 != icons.Icons.IconPlusSm - #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible + #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible.box-border.lh-sm .s-popover--arrow.s-popover--arrow__tc - +add-roles-menu(guild, guild_id) + + block add-roles-menu requires guild, guild_id + ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu + li.s-menu--title.d-flex(role="separator") Select role + span#add-role-loading + each r in guild.roles.sort((a, b) => b.position - a.position) + if r.id !== guild_id && !r.managed + - let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get() + li(role="menuitem") + button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name + p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. h3.mt32.fs-category Features diff --git a/src/web/pug/includes/default-roles-list.pug b/src/web/pug/includes/default-roles-list.pug deleted file mode 100644 index 8c0a4e0..0000000 --- a/src/web/pug/includes/default-roles-list.pug +++ /dev/null @@ -1,19 +0,0 @@ -mixin default-roles-list(guild, guild_id) - #default-roles-list(style="display: contents") - each roleID in select("role_default", "role_id", {guild_id}).pluck().all() - - let r = guild.roles.find(r => r.id === roleID) - if r - .s-tag.s-tag__md.fs-body1= r.name - span(id=`role-loading-${roleID}`) - button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss - != icons.Icons.IconClearSm - -mixin add-roles-menu(guild, guild_id) - ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu - li.s-menu--title.d-flex(role="separator") Select role - span#add-role-loading - each r in guild.roles.sort((a, b) => b.position - a.position) - if r.id !== guild_id && !r.managed - - let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get() - li(role="menuitem") - button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name diff --git a/src/web/pug/invite.pug b/src/web/pug/invite.pug index 81a428d..0596d58 100644 --- a/src/web/pug/invite.pug +++ b/src/web/pug/invite.pug @@ -17,10 +17,10 @@ block body .fl-grow1 h2.fs-headline1 Invite a Matrix user - form.d-flex.gy16.fd-column(method="post" action=rel("/api/invite") hx-post=rel("/api/invite") hx-indicator="#invite-button" hx-select="#ok" hx-target="#form-container") + form.d-flex.gy16.wmx3.fd-column(method="post" action=rel("/api/invite") hx-post=rel("/api/invite") hx-indicator="#invite-button" hx-select="#ok" hx-target="#form-container") .d-flex.gy4.fd-column label.s-label(for="mxid") Matrix ID - input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") + input.fl-grow1.s-input#mxid(name="mxid" required placeholder="@user:example.org") .d-flex.gy4.fd-column label.s-label(for="permissions") Permissions .s-select diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 8119f93..9bfff3d 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -135,7 +135,7 @@ as.router.post("/api/default-roles", defineEventHandler(async 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}) + return pugSync.renderFragments(event, "guild.pug", ["default-roles-list", "add-roles-menu"], {guild, guild_id: guildID}) } else { return sendRedirect(event, `/guild?guild_id=${guildID}`, 302) } diff --git a/src/web/server.js b/src/web/server.js index e2ce1b5..3c06ede 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -98,7 +98,7 @@ function tryStatic(event, fallthrough) { getContents: id => { if (id.match(/\.pug$/)) { const path = join(publicDir, id) - return pugSync.renderPath(event, path, {}) + return pugSync.renderPath(event, path, null, {}) } else { return fs.createReadStream(join(publicDir, id)) } diff --git a/test/test.js b/test/test.js index 4cd9627..5079ed3 100644 --- a/test/test.js +++ b/test/test.js @@ -130,6 +130,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("./addbot.test") require("../src/db/orm.test") + require("../src/js/errors.test") require("../src/web/server.test") require("../src/discord/utils.test") require("../src/matrix/kstate.test") From 59b5e05b339b4ebdefbb6e4b6b88547aebe4b864 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Sep 2026 13:18:52 +1200 Subject: [PATCH 13/20] Make m->d Klipy links not produce error --- src/d2m/converters/message-to-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 49e3b64..e00e5c3 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -785,7 +785,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Send Klipy GIFs in customised form let isKlipyGIF = false let isOnlyKlipyGIF = false - if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { + if (message.content && message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { isKlipyGIF = true if (message.content.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { isOnlyKlipyGIF = true From 77cc1e6eae7fc0ba32645732d05251d35c5b484e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Sep 2026 14:05:01 +1200 Subject: [PATCH 14/20] Better solution for previous commit --- src/d2m/converters/message-to-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index e00e5c3..86b310c 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -785,9 +785,9 @@ async function messageToEvent(message, guild, options = {}, di) { // Send Klipy GIFs in customised form let isKlipyGIF = false let isOnlyKlipyGIF = false - if (message.content && message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { + if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { isKlipyGIF = true - if (message.content.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { + if (message.content?.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { isOnlyKlipyGIF = true } } From fab512a9bae51b59f50b575f0f9a8c41396c0ddb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:01:24 +1300 Subject: [PATCH 15/20] Fix Klipy gifs when url previews disabled --- src/d2m/converters/message-to-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 86b310c..c6d5e2c 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -783,9 +783,10 @@ async function messageToEvent(message, guild, options = {}, di) { } // Send Klipy GIFs in customised form + const urlPreviewEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 let isKlipyGIF = false let isOnlyKlipyGIF = false - if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { + if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url && urlPreviewEnabled) { isKlipyGIF = true if (message.content?.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { isOnlyKlipyGIF = true @@ -1046,7 +1047,6 @@ async function messageToEvent(message, guild, options = {}, di) { } // Then embeds - const urlPreviewEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 for (const embed of message.embeds || []) { if (!urlPreviewEnabled && !message.author?.bot) { continue // show embeds for everyone if enabled, or bot users only if disabled (bots often send content in embeds) From 5794bee128c45af69a98741fe7a174a0c6e62ccb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:19:35 +1300 Subject: [PATCH 16/20] Support Discord game mentions --- package-lock.json | 274 ++++++++++---------- package.json | 2 +- src/d2m/converters/message-to-event.js | 8 + src/d2m/converters/message-to-event.test.js | 10 + test/data.js | 52 ++++ 5 files changed, 208 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9ff5cb..6ec8ce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.7.0", + "@cloudrac3r/discord-markdown": "^2.8.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -131,9 +131,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.7.0.tgz", - "integrity": "sha512-1iR9tKI2WJe8UNB+4VSh7D8m6RP7ugByuf8RNWyJwyhIrSlqQ8ljY1BKXodSvDg7seZkf7B7V2t5FfK7UpTw/A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.8.0.tgz", + "integrity": "sha512-ciLxS6iaMxuEkEln7Y9rTpUBFf+gij8djNKS1QGrTfKoHcKABmDvcXQlPntdTx7odHjtMqV2RfvR/rnmQ25YPQ==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -302,9 +302,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -320,13 +320,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -342,20 +342,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -365,9 +365,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -413,9 +413,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -429,9 +429,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -445,9 +445,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -477,9 +477,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -543,13 +543,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -565,13 +565,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -587,13 +587,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -609,13 +609,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -631,13 +631,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -653,13 +653,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -675,13 +675,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -697,13 +697,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -717,16 +717,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -736,9 +736,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -755,9 +755,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -774,9 +774,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1039,9 +1039,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", - "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "dev": true, "license": "MIT", "dependencies": { @@ -1158,9 +1158,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,9 +1477,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.55", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.55.tgz", - "integrity": "sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==", + "version": "0.38.56", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.56.tgz", + "integrity": "sha512-Hti4RZsGvF5C7SM8rF2MDHtIm0Hyfrlk84GpEA29nj91tvVVMvX+oQSCodu10t2ZcjFGofq8TX0YyC04B2E69Q==", "license": "MIT" }, "node_modules/doctypes": { @@ -1920,9 +1920,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2264,9 +2264,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -2280,31 +2280,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -2428,9 +2428,9 @@ } }, "node_modules/supertape": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.6.0.tgz", - "integrity": "sha512-W/lm1wk370QF0u+LrxS+BMUFNYMfRgjFC9dTKyFB522fa77pKJHwOBDiTl3uhNbsw0Sb1NQPRYk+eFICcqxedA==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.6.2.tgz", + "integrity": "sha512-VPNYybfbgrTMhhOl4amTfEQ1NpyOPH+gq29rlHeBFyKmKMAcUa1ZEaUWbexY4YLUAzNvaFKOGVzogvZFKq4T9A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5fbf928..7c9ac57 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.7.0", + "@cloudrac3r/discord-markdown": "^2.8.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index c6d5e2c..5aa6529 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -86,6 +86,14 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { return `@${role.name}:` } }, + game: node => { + const game = message.mention_games?.find(g => g.id === node.id) + if (!game?.name) { + return "@$" + node.id + } else { + return game.name + } + }, everyone: () => { if (message.mention_everyone) return "@room" return "@everyone" diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index aea24a8..41f40ba 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -192,6 +192,16 @@ test("message2event: manually constructed unknown roles should use fallback", as }]) }) +test("message2event: simple game mentions", async t => { + const events = await messageToEvent(data.message.simple_game_mention, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "do you play Minecraft" + }]) +}) + test("message2event: simple message link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, { diff --git a/test/data.js b/test/data.js index 60e9bd8..f441778 100644 --- a/test/data.js +++ b/test/data.js @@ -1680,6 +1680,58 @@ module.exports = { flags: 0, components: [] }, + simple_game_mention: { + type: 0, + content: "do you play <@$1402418491272986635>", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-09-28T21:01:56.749000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1554236746756268077", + channel_id: "1160894080998461480", + author: { + id: "177438227563675648", + username: "owlbot", + avatar: "bafbcafcc0a6c2698d59a2cb567a9ecf", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + collectibles: null, + display_name_styles: null, + vad_colors: null, + banner_color: null, + clan: { + identity_guild_id: "773319828295385108", + identity_enabled: true, + tag: "puzl", + badge: "1935451f265a42d57241c7bcc9555abe" + }, + primary_guild: { + identity_guild_id: "773319828295385108", + identity_enabled: true, + tag: "puzl", + badge: "1935451f265a42d57241c7bcc9555abe" + } + }, + pinned: false, + mention_everyone: false, + tts: false, + mention_games: [ + { + id: "1402418491272986635", + name: "Minecraft", + aliases: [ "Minecraft Windows 10 Edition", "Minecraft Launcher" ], + } + ] + }, unknown_role: { id: "1162374402785153106", type: 0, From 302427037b18a6a522207592d2788a0bfab9dd38 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:24:13 +1300 Subject: [PATCH 17/20] Don't copy URL previews for GitHub --- src/d2m/converters/message-to-event.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 5aa6529..b3e3116 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -1076,6 +1076,10 @@ async function messageToEvent(message, guild, options = {}, di) { continue // If discord creates an embed preview for a discord channel link, don't copy that embed } + if (embed.provider?.name === "GitHub") { + continue // GitHub url previews are very large and fairly useless + } + if (embed.url && spoilers.some(sp => sp.match(/\bhttps?:\/\/[a-z]/))) { // If the original message had spoilered URLs, don't generate any embeds for links. // This logic is the same as the Discord desktop client. It doesn't match specific embeds to specific spoilered text, it's all or nothing. From c25b5a19c62d3c7e13d9d8c5ccdaafe55f9ba7c7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 7 Oct 2026 22:52:42 +1300 Subject: [PATCH 18/20] Support Discord @time --- package-lock.json | 27 +++- package.json | 2 +- src/d2m/converters/message-to-event.js | 13 ++ .../message-to-event.test.components.js | 4 +- src/d2m/converters/message-to-event.test.js | 22 +++ src/discord/time.js | 51 +++++++ src/discord/time.test.js | 48 ++++++ src/web/pug/time.pug | 143 ++++++++++++++++++ src/web/routes/time.js | 31 ++++ src/web/server.js | 1 + test/data.js | 50 ++++++ test/test.js | 1 + 12 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 src/discord/time.js create mode 100644 src/discord/time.test.js create mode 100644 src/web/pug/time.pug create mode 100644 src/web/routes/time.js diff --git a/package-lock.json b/package-lock.json index 6ec8ce6..32e7e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.8.0", + "@cloudrac3r/discord-markdown": "^2.9.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -55,6 +55,19 @@ "node": ">=22" } }, + "../discord-markdown": { + "name": "@cloudrac3r/discord-markdown", + "version": "2.9.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "simple-markdown": "^0.7.3" + }, + "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.2", + "supertape": "^13.6.2" + } + }, "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", @@ -131,9 +144,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.8.0.tgz", - "integrity": "sha512-ciLxS6iaMxuEkEln7Y9rTpUBFf+gij8djNKS1QGrTfKoHcKABmDvcXQlPntdTx7odHjtMqV2RfvR/rnmQ25YPQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.9.0.tgz", + "integrity": "sha512-0S22mNlT9rPhYnxP+qzKWayVe+hFWcEqxVCviF/pyzWAb9nLaBZkIVk0jHTImoA1MY0mqx1uMKjIcxVHkEZe3A==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -1049,9 +1062,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" diff --git a/package.json b/package.json index 7c9ac57..72004cf 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.8.0", + "@cloudrac3r/discord-markdown": "^2.9.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index b3e3116..f192aff 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -18,6 +18,8 @@ const lottie = sync.require("../actions/lottie") const mxUtils = sync.require("../../matrix/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") +/** @type {import("../../discord/time")} */ +const discordTime = sync.require("../../discord/time") /** @type {import("./find-mentions")} */ const findMentions = sync.require("./find-mentions") /** @type {import("../../discord/interactions/poll-responses")} */ @@ -72,6 +74,17 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { return `:${node.name}:` } }, + /** @param {{timestamp: number, style: string | null}} node */ + timestamp: node => { + const text = discordTime.convertDiscordTime(node.timestamp, node.style) + if (useHTML) { + let url = `${reg.ooye.bridge_origin}/time/${node.timestamp}` + if (node.style) url += `/${node.style}` + return tag`${text}` + } else { + return text + } + }, role: node => { const role = guild.roles.find(r => r.id === node.id) if (!role) { diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 1ef83c3..47e27b5 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -44,7 +44,7 @@ test("message2event components: pk question mark output", async t => { + "\n| Same hat" + "\n| ๐Ÿ–ผ๏ธ Image: https://bridge.example.org/download/discordcdn/934955898965729280/1466556006527012987/image.png" + "\n" - + "\n-# Original Message ID: 1466556003645657118 ยท ", + + "\n-# Original Message ID: 1466556003645657118 ยท 29 January 2026 at 10:09 pm (UTC)", format: "org.matrix.custom.html", formatted_body: "
" + "

Lillith (INX)

" @@ -73,7 +73,7 @@ test("message2event components: pk question mark output", async t => { + "
" + "

Same hat

" + `๐Ÿ–ผ๏ธ Image: image.png
` - + "

Original Message ID: 1466556003645657118 ยท <t:1769724599:f>

", + + "

Original Message ID: 1466556003645657118 ยท 29 January 2026 at 10:09 pm (UTC)

", "m.mentions": {}, msgtype: "m.text", }]) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 41f40ba..397be39 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -202,6 +202,28 @@ test("message2event: simple game mentions", async t => { }]) }) +test("message2event: timestamps", async t => { + const events = await messageToEvent(data.message.timestamps, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "4 Oct 2026, 9:33:33 am (UTC)" + + "\n4 October 2026 at 9:33 am (UTC)" + + "\nSunday, 4 October 2026 at 9:34 am (UTC)" + + "\n9:34:08 am (UTC)" + + "\n3 October 2026 at 9:30 am (UTC)" + + "\n24 December 2026 at 11:00 pm (UTC)", + format: "org.matrix.custom.html", + formatted_body: `4 Oct 2026, 9:33:33 am (UTC)` + + `
4 October 2026 at 9:33 am (UTC)` + + `
Sunday, 4 October 2026 at 9:34 am (UTC)` + + `
9:34:08 am (UTC)` + + `
3 October 2026 at 9:30 am (UTC)` + + `
24 December 2026 at 11:00 pm (UTC)`, + "m.mentions": {} + }]) +}) + test("message2event: simple message link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, { diff --git a/src/discord/time.js b/src/discord/time.js new file mode 100644 index 0000000..d0f7a04 --- /dev/null +++ b/src/discord/time.js @@ -0,0 +1,51 @@ +// @ts-check + +const assert = require("assert").strict + +/* + https://docs.discord.com/developers/reference#message-formatting + + Trying to make these formats fairly culture independent since the Matrix text will show up the same for everyone. + That means no ambiguous short dates like 03/04/26. The shortest allowable date is "medium", which spells the month. + It also means no 24-hour time, since this is ambiguous to 12-hour time users. Times must have am/pm. + Relative time is not supported for text conversion and will fall back to the default, "f". + + See the test file for sample outputs. +*/ + +/** @type {Map} discord style letter abbreviation -> Intl.DateTimeFormat constructor options */ +const styles = new Map([ + ["t", {timeStyle: "short", timeZone: "UTC"}], + ["T", {timeStyle: "medium", timeZone: "UTC"}], + ["d", {dateStyle: "medium", timeZone: "UTC"}], + ["D", {dateStyle: "long", timeZone: "UTC"}], + ["f", {dateStyle: "long", timeStyle: "short", timeZone: "UTC"}], + ["F", {dateStyle: "full", timeStyle: "short", timeZone: "UTC"}], + ["s", {dateStyle: "medium", timeStyle: "short", timeZone: "UTC"}], + ["S", {dateStyle: "medium", timeStyle: "medium", timeZone: "UTC"}] +]) +const defaultStyle = "f" + +/** + * @param {string | null | undefined} [style] + * @returns {Intl.DateTimeFormatOptions} */ +function getFormatOptions(style) { + if (!style || !styles.has(style)) style = defaultStyle + const formatOptions = styles.get(style) + assert(formatOptions) + return formatOptions +} + +/** + * @param {number} timestamp unix time in seconds + * @param {string | null | undefined} [style] discord style letter abbreviation, if present + * @returns {string} + */ +function convertDiscordTime(timestamp, style) { + const formatter = new Intl.DateTimeFormat("en-NZ", getFormatOptions(style)) + const d = new Date(timestamp * 1000) + return formatter.format(d) + " (UTC)" +} + +module.exports.getFormatOptions = getFormatOptions +module.exports.convertDiscordTime = convertDiscordTime diff --git a/src/discord/time.test.js b/src/discord/time.test.js new file mode 100644 index 0000000..b889682 --- /dev/null +++ b/src/discord/time.test.js @@ -0,0 +1,48 @@ +// @ts-check + +const time = require("./time") +const {test} = require("supertape") + +const d = new Date("2026-02-14T14:23:06.000Z") +const seconds = d.getTime() / 1000 + +test("time: default style", t => { + t.equal(time.convertDiscordTime(seconds), "14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: t style", t => { + t.equal(time.convertDiscordTime(seconds, "t"), "2:23 pm (UTC)") +}) + +test("time: T style", t => { + t.equal(time.convertDiscordTime(seconds, "T"), "2:23:06 pm (UTC)") +}) + +test("time: d style", t => { + t.equal(time.convertDiscordTime(seconds, "d"), "14 Feb 2026 (UTC)") +}) + +test("time: D style", t => { + t.equal(time.convertDiscordTime(seconds, "D"), "14 February 2026 (UTC)") +}) + +test("time: f style", t => { + t.equal(time.convertDiscordTime(seconds, "f"), "14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: F style", t => { + t.equal(time.convertDiscordTime(seconds, "F"), "Saturday, 14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: s style", t => { + t.equal(time.convertDiscordTime(seconds, "s"), "14 Feb 2026, 2:23 pm (UTC)") +}) + +test("time: S style", t => { + t.equal(time.convertDiscordTime(seconds, "S"), "14 Feb 2026, 2:23:06 pm (UTC)") +}) + +test("time: R style", t => { + // Discord would display this as relative time, but I don't want to do that in a static message. + t.equal(time.convertDiscordTime(seconds, "R"), "14 February 2026 at 2:23 pm (UTC)") +}) diff --git a/src/web/pug/time.pug b/src/web/pug/time.pug new file mode 100644 index 0000000..1d71c22 --- /dev/null +++ b/src/web/pug/time.pug @@ -0,0 +1,143 @@ +//- requires timestamp, text, style + +- const WIDTH = 600 + +doctype html +html(lang="en") + head + + + title= `Converting time ${text}...` + style. + body { + font-family: "Segoe UI", sans-serif; + margin: 0 auto; + padding: 4px; + font-size: 18px; + background-color: #f0f2f5; /* element compound tertiary off-white */ + max-width: !{WIDTH}px; + } + .only-script { + display: none; + } + .yes-script .only-script { + display: revert; + } + .local { + line-height: 1; + width: auto; + height: auto; + max-width: 100%; + } + .original { + color: #004c84; + } + .remaining { + font-weight: bold; + } + .primary-remaining { + font-size: 25px; + } + @media (prefers-color-scheme: dark) { + body { + color: #e3e3e3; + background-color: #131416; /* element compound bg but desaturated */ + } + .original { + color: #99c9ec; + } + .local { + fill: #fff; + } + .remaining { + color: #fff; + } + } + body + noscript + | You must enable JavaScript to convert to your local time. + .only-script + p In your local time, #[span.original= text] is... + if style === "R" + p.primary-remaining #[span#remaining.remaining]#[span#direction] #[span#tick.tick] + svg.local(viewBox=`0 -130 ${WIDTH} 253`) + text(style="font-size: 160px; font-weight: bold;")#localTime + text(y=70 style="font-size: 45px; font-weight: bold;")#localDate + text(y=115 style="font-size: 22px;")#localTz + if style !== "R" + p #[span#remaining.remaining]#[span#direction] #[span#tick.tick] + script. + document.body.classList.add("yes-script") + const WIDTH = !{WIDTH} + const timestamp = !{timestamp} + const style = "!{style}" + const formatOptions = !{JSON.stringify(formatOptions)} + const d = new Date(timestamp * 1000) + const UNITS = [ + ["week", 7*24*60*60*1000], + ["day", 24*60*60*1000], + ["hour", 60*60*1000], + ["minute", 60*1000], + ] + const showSeconds = formatOptions.timeStyle === "medium" || style === "R" + if (showSeconds) UNITS.push(["second", 1000]) + + if (formatOptions.timeStyle) { + localTime.textContent = new Intl.DateTimeFormat(undefined, {timeStyle: formatOptions.timeStyle}).format(d) + } else { + localTime.textContent = "Any time on" + localTime.style.fill = "#888" + } + if (formatOptions.dateStyle) { + localDate.textContent = new Intl.DateTimeFormat(undefined, {dateStyle: formatOptions.dateStyle}).format(d) + } else { + localDate.textContent = "On any date" + localDate.style.fill = "#888" + } + const localTzText = new Intl.DateTimeFormat(undefined, {timeZoneName: "long"}).formatToParts(d).find(p => p.type === "timeZoneName").value + const localTitleText = new Intl.DateTimeFormat(undefined, {...formatOptions, timeZone: undefined}).format(d) + document.title = localTitleText + localTz.textContent = localTzText + for (const st of [localTime, localDate, localTz]) { + const l = st.getComputedTextLength() + if (l > WIDTH) { + st.style.fontSize = Math.floor(parseInt(st.style.fontSize) * WIDTH / l) + "px"; + } + } + + let frame = 0 + function updateTimeRemaining() { + const now = new Date() + const timeForRemaining = new Date(d) + if (!formatOptions.dateStyle) { + timeForRemaining.setFullYear(now.getFullYear()) + timeForRemaining.setMonth(now.getMonth()) + timeForRemaining.setDate(now.getDate()) + } + let difference = Math.abs(timeForRemaining.getTime() - now.getTime()) + + let foundSignificantField = false + let remainingText = UNITS.map(([unit, duration], index) => { + const multiplier = Math.floor(difference / duration) + difference -= multiplier * duration + if (multiplier > 0 || foundSignificantField) { + foundSignificantField = true + return multiplier + "\u00a0" + unit + (multiplier !== 1 ? "s" : "") + } + }).filter(s => s).join(", ") + if (remainingText) { + remaining.textContent = remainingText + direction.textContent = + ( timeForRemaining.getTime() > now.getTime() + ? "\u00a0from\u00a0now." + : "\u00a0ago.") + } else { + remaining.textContent = "Right now!" + direction.textContent = "" + } + if (!showSeconds) { + tick.textContent = String.fromCodePoint(11028 + ++frame % 2) + } + } + updateTimeRemaining() + setInterval(updateTimeRemaining, 1000) diff --git a/src/web/routes/time.js b/src/web/routes/time.js new file mode 100644 index 0000000..ad89b58 --- /dev/null +++ b/src/web/routes/time.js @@ -0,0 +1,31 @@ +// @ts-check + +const {defineEventHandler, getValidatedRouterParams, H3Event, setResponseHeader} = require("h3") +const {as, db, sync} = require("../../passthrough") +const {z} = require("zod") + +/** @type {import("../pug-sync")} */ +const pugSync = sync.require("../pug-sync") +/** @type {import("../../discord/time")} */ +const discordTime = sync.require("../../discord/time") + +const schema = { + time: z.object({ + timestamp: z.coerce.number(), + style: z.string().optional() + }) +} + +as.router.get("/time/:timestamp", defineEventHandler(async event => { + const {timestamp} = await getValidatedRouterParams(event, schema.time.parse) + const formatOptions = discordTime.getFormatOptions() + const text = discordTime.convertDiscordTime(timestamp) + return pugSync.render(event, "time.pug", {text, timestamp, formatOptions}) +})) + +as.router.get("/time/:timestamp/:style", defineEventHandler(async event => { + const {timestamp, style} = await getValidatedRouterParams(event, schema.time.parse) + const formatOptions = discordTime.getFormatOptions(style) + const text = discordTime.convertDiscordTime(timestamp, style) + return pugSync.render(event, "time.pug", {text, timestamp, style, formatOptions}) +})) diff --git a/src/web/server.js b/src/web/server.js index 3c06ede..83aa588 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -141,3 +141,4 @@ sync.require("./routes/oauth") sync.require("./routes/opt-out") sync.require("./routes/password") sync.require("./routes/stats") +sync.require("./routes/time") diff --git a/test/data.js b/test/data.js index f441778..bcc9fdb 100644 --- a/test/data.js +++ b/test/data.js @@ -1732,6 +1732,56 @@ module.exports = { } ] }, + timestamps: { + type: 0, + content: "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-10-04T09:34:27.909000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1556238063774990499", + channel_id: "1160894080998461480", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "466df0c98b1af1e1388f595b4c1ad1b9", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + collectibles: null, + display_name_styles: null, + vad_colors: null, + banner_color: null, + clan: { + identity_guild_id: "532245108070809601", + identity_enabled: true, + tag: "doll", + badge: "dba08126b4e810a0e096cc7cd5bc37f0" + }, + primary_guild: { + identity_guild_id: "532245108070809601", + identity_enabled: true, + tag: "doll", + badge: "dba08126b4e810a0e096cc7cd5bc37f0" + } + }, + pinned: false, + mention_everyone: false, + tts: false + }, unknown_role: { id: "1162374402785153106", type: 0, diff --git a/test/test.js b/test/test.js index 5079ed3..74d99b1 100644 --- a/test/test.js +++ b/test/test.js @@ -132,6 +132,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/db/orm.test") require("../src/js/errors.test") require("../src/web/server.test") + require("../src/discord/time.test") require("../src/discord/utils.test") require("../src/matrix/kstate.test") require("../src/matrix/api.test") From b49c6e8fd4a51900fc70da9dce30cadf6971e448 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 8 Oct 2026 02:19:26 +1300 Subject: [PATCH 19/20] Add //time command for m->d scheduling --- docs/developer-orientation.md | 4 +- package-lock.json | 20 ++++ package.json | 2 + src/matrix/matrix-command-handler.js | 137 +++++++++++++++++++++++++-- start.js | 5 + 5 files changed, 159 insertions(+), 9 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index 839b4af..27ddb7d 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,7 +89,7 @@ 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: 112 +Total transitive production dependencies: 114 ### ๐Ÿฆ• @@ -108,6 +108,7 @@ Total transitive production dependencies: 112 * (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. * (0) @cloudrac3r/stream-type: Determine type of Matrix files that don't specify it in info. Switched from stream-mime-type to this. +* (0) @cloudrac3r/tell-me-when: Parse English natural language dates and times for the m->d //time command. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. * (3) @stackoverflow/stacks: Stack Overflow design language and icons. * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. @@ -125,6 +126,7 @@ Total transitive production dependencies: 112 * (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) temporal-polyfill-lite: Needed for the //time command. The Temporal API has only been stabilised as of Node.js v26. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. * (0) uqr: QR code SVG generator. Used on the website to scan in an invite link. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. diff --git a/package-lock.json b/package-lock.json index 32e7e88..5a60f1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", + "@cloudrac3r/tell-me-when": "^2.0.0", "@cloudrac3r/turndown": "^7.1.5", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -39,6 +40,7 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", + "temporal-polyfill-lite": "^0.4.3", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", @@ -280,6 +282,18 @@ "node": ">=8.0.0" } }, + "node_modules/@cloudrac3r/tell-me-when": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/tell-me-when/-/tell-me-when-2.0.0.tgz", + "integrity": "sha512-U+TEOTe6f/vSIeD/QP0VPrkLrhnFtQs2euxBOAaqC8rzQKu8nNoKSFuhipT6DyuCZH48B58+qrPMmH2w2IgMhA==", + "license": "MIT", + "bin": { + "tell-me-when": "dist/src/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudrac3r/turndown": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz", @@ -2516,6 +2530,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/temporal-polyfill-lite": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/temporal-polyfill-lite/-/temporal-polyfill-lite-0.4.3.tgz", + "integrity": "sha512-Lw/de9tydKmrU0icQy6Z+abX5mzDk4iqyt6Xp2iDDxY6SGISiuhuP3TcfWlYveM1FN1I7FjT5nG+Kh9eCiSnVg==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", diff --git a/package.json b/package.json index 72004cf..4c9de0e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", + "@cloudrac3r/tell-me-when": "^2.0.0", "@cloudrac3r/turndown": "^7.1.5", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -48,6 +49,7 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", + "temporal-polyfill-lite": "^0.4.3", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index aa80b9c..de81899 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -3,8 +3,9 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const Ty = require("../types") -const {pipeline} = require("stream").promises const sharp = require("sharp") +const tw = require("@cloudrac3r/tell-me-when") +const {tag} = require("@cloudrac3r/html-template-tag") const {discord, sync, db, select} = require("../passthrough") /** @type {import("./api")}) */ @@ -13,8 +14,9 @@ const api = sync.require("./api") const mxUtils = sync.require("./utils") /** @type {import("../discord/utils")} */ const dUtils = sync.require("../discord/utils") -/** @type {import("./kstate")} */ -const ks = sync.require("./kstate") +/** @type {import("../d2m/actions/retrigger")} */ +const retrigger = sync.require("../d2m/actions/retrigger") + const {reg} = require("./read-registration") const PREFIXES = ["//", "/"] @@ -33,7 +35,17 @@ function getSlotCount(tier) { return TIER_EMOJI_SLOTS.get(tier) || 50 } -let buttons = [] +/** + * @typedef Button + * @property {string} roomID + * @property {string} eventID + * @property {string} mxid + * @property {string} key + * @property {(any) => any} resolve + * @property {number} created + */ + +const buttonStore = sync.remember(() => /** @type {{buttons: Button[]}} */ ({buttons: []})) /** * @param {string} roomID where to add the button @@ -51,22 +63,23 @@ async function addButton(roomID, eventID, key, mxid) { } }) return new Promise(resolve => { - buttons.push({roomID, eventID, mxid, key, resolve, created: Date.now()}) + buttonStore.buttons.push({roomID, eventID, mxid, key, resolve, created: Date.now()}) }) } // Clear out old buttons every so often to free memory setInterval(() => { const now = Date.now() - buttons = buttons.filter(b => now - b.created < 2*60*60*1000) + buttonStore.buttons = buttonStore.buttons.filter(b => now - b.created < 2*60*60*1000) }, 10*60*1000).unref() /** @param {Ty.Event.Outer} event */ function onReactionAdd(event) { - const button = buttons.find(b => b.roomID === event.room_id && b.mxid === event.sender && b.eventID === event.content["m.relates_to"]?.event_id && b.key === event.content["m.relates_to"]?.key) + const button = buttonStore.buttons.find(b => b.roomID === event.room_id && b.mxid === event.sender && b.eventID === event.content["m.relates_to"]?.event_id && b.key === event.content["m.relates_to"]?.key) if (button) { - buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again + buttonStore.buttons = buttonStore.buttons.filter(b => b !== button) // remove button data so it can't be clicked again button.resolve(event) + return true } } @@ -318,6 +331,114 @@ const commands = [{ }) } ) +}, { + aliases: ["time"], + execute: replyctx( + async (event, realBody, words, ctx) => { + // Guard + /** @type {string} */ // @ts-ignore + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] + if (!guildID) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This room isn't bridged to the other side." + }) + } + + // Get user timezone + const profile = await api.getProfile(event.sender) + const senderTimeZone = profile?.["m.tz"] + if (!senderTimeZone) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command converts a time in your local time zone for everybody else. But I don't know your time zone. Please set your time zone (hopefully possible in your client's settings/account menu) and try again." + }) + } + + try { + var now = Temporal.Now.zonedDateTimeISO(senderTimeZone) + } catch (e) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + ...new mxUtils.MatrixStringBuilder().add( + `The time zone on your account, "${senderTimeZone}", is not valid. It needs to be an IANA time zone value, with a slash. See yours here: https://time.is/your_time_zone`, + tag`The time zone on your account, ${senderTimeZone}, is not valid. It needs to be an IANA time zone value with a slash. See yours here.` + ).get() + }) + } + + const input = words.slice(1).join(" ").trim() + if (!input) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command converts a time in your local time zone for everybody else. Use this command with a time or date (English natural language)." + }) + } + + try { + var parsed = tw.parse(input) + var when = tw.tellMeWhen(input, {now}) + } catch (e) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "I didn't understand your input. If you think your input was reasonable, please report this as a bug." + }) + } + + const messageID = select("event_message", "message_id", {event_id: event.event_id}).pluck().get() + + const hasDate = parsed.some(x => Array.isArray(x) && x[0].match(/year|month|week|date|day/i)) + const hasTime = parsed.some(x => Array.isArray(x) && x[0].match(/hour|minute|second/i)) + const colonStyle = + ( hasDate && hasTime ? ":F" + : hasDate ? ":D" + : ":t") + + /** @param {Temporal.ZonedDateTime} z */ + function convertOne(z) { + return "" + } + + if (Array.isArray(when)) { // convert range + var content = `From ${convertOne(when[0])} to ${convertOne(when[1])}` + } else { // convert single time + var content = `${convertOne(when)}` + } + + /** @type {DiscordTypes.RESTPostAPIChannelMessageJSONBody} */ + const message = { + flags: DiscordTypes.MessageFlags.IsComponentsV2, + components: [{ + type: DiscordTypes.ComponentType.Container, + components: [{ + type: DiscordTypes.ComponentType.TextDisplay, + content: `## ${content}\n-# Wrong? ๐Ÿ—‘๏ธ to delete` + }] + }] + } + if (messageID) { + Object.assign(message, { + message_reference: { + type: DiscordTypes.MessageReferenceType.Default, + message_id: messageID, + } + }) + } + const sent = await discord.snow.channel.createMessage(channelID, message) + if (!await retrigger.waitForMessage(sent.id)) return + const sentEventID = select("event_message", "event_id", {message_id: sent.id}, "ORDER BY reaction_part").pluck().get() + assert(sentEventID) + addButton(event.room_id, sentEventID, "๐Ÿ—‘๏ธ", event.sender).then(() => { + discord.snow.channel.deleteMessage(channelID, sent.id).catch(() => {}) + }) + } + ) }] diff --git a/start.js b/start.js index 2ab79cb..728c8af 100755 --- a/start.js +++ b/start.js @@ -1,6 +1,11 @@ #!/usr/bin/env node // @ts-check +// Temporal API stabilised in Node.js 26. Previous versions can use `--harmony-temporal`, but that's jank, so have a polyfill. +if (!("Temporal" in globalThis)) { + require("temporal-polyfill-lite/global") +} + const fs = require("fs") const sqlite = require("better-sqlite3") const migrate = require("./src/db/migrate") From a5032776731a458f4678ea5f0ab6514374fde121 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 8 Oct 2026 02:54:46 +1300 Subject: [PATCH 20/20] Use hyperlinked times for scheduled events --- src/d2m/converters/message-to-event.js | 43 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index f192aff..4314a0e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -883,32 +883,47 @@ async function messageToEvent(message, guild, options = {}, di) { // Then scheduled events if (message.content && di?.snow) { - for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old + for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})|\/events\/([0-9]+)\/([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old + /** @type {DiscordTypes.APIGuildScheduledEvent | undefined} */ + let event let invite - try { - invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) - } catch (e) { - // Skip expired/invalid invites and events - if (e.message === "Unknown Invite") { - break - } else { + + if (match[1]) { + try { + invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) + event = invite.guild_scheduled_event + } catch (e) { + // Skip expired/invalid invites and events + if (e.message === "Unknown Invite") { + break + } else { + throw e + } + } + } else if (match[3]) { + try { + event = await di.snow.guildScheduledEvent.getGuildScheduledEvent(match[3], match[4]) + } catch (e) { throw e } } - const event = invite.guild_scheduled_event if (!event) continue // the event ID provided was not valid - const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT const rep = new mxUtils.MatrixStringBuilder() + const unixSeconds = Math.floor(new Date(event.scheduled_start_time).getTime() / 1000) // Add time if (event.scheduled_end_time) { - // @ts-ignore - no definition available for formatRange - rep.addParagraph(`Scheduled Event - ${formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time))}`) + const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT + var displayTime = formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time)) } else { - rep.addParagraph(`Scheduled Event - ${formatter.format(new Date(event.scheduled_start_time))}`) + var displayTime = discordTime.convertDiscordTime(unixSeconds, "F") } + rep.addParagraph( + `Scheduled Event - ${displayTime}`, + tag`Scheduled Event - ${displayTime}` + ) // Add details rep.addLine(`## ${event.name}`, tag`${event.name}`) @@ -917,7 +932,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Add location if (event.entity_metadata?.location) { rep.addParagraph(`๐Ÿ“ ${event.entity_metadata.location}`) - } else if (invite.channel?.name) { + } else if (invite?.channel?.name) { const roomID = select("channel_room", "room_id", {channel_id: invite.channel.id}).pluck().get() if (roomID) { const via = await getViaServersMemo(roomID)