From 1a1173413b27c9182914341d404e6c4daf7cde3f Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 19 Oct 2026 13:43:39 +0200 Subject: [PATCH 01/11] spoiler fix --- src/d2m/converters/message-to-event.js | 9 +++++---- src/discord/utils.js | 9 ++++++--- src/web/routes/download-discord.js | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index a291a5e..62eed46 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -135,7 +135,8 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ * @param {{fetch?: typeof fetch}} [di] */ async function attachmentToEvent(mentions, attachment, alwaysLink, di) { - const external_url = dUtils.getPublicUrlForCdn(attachment.url) + const spoiler = attachment.filename.startsWith("SPOILER_") || !!((attachment.flags || 0) & DiscordTypes.AttachmentFlags.IsSpoiler); + const external_url = dUtils.getPublicUrlForCdn(attachment.url, spoiler ? "spoiler/" : "") const emoji = attachment.content_type?.startsWith("image/jp") ? "šŸ“ø" : attachment.content_type?.startsWith("image/") ? "šŸ–¼ļø" @@ -143,15 +144,15 @@ async function attachmentToEvent(mentions, attachment, alwaysLink, di) { : attachment.content_type?.startsWith("text/") ? "šŸ“" : attachment.content_type?.startsWith("audio/") ? "šŸŽ¶" : "šŸ“„" - // no native media spoilers in Element, so we'll post a link instead, forcing it to not preview using a blockquote - if (attachment.filename.startsWith("SPOILER_") || !!((attachment.flags || 0) & DiscordTypes.AttachmentFlags.IsSpoiler)) { + // no native media spoilers in Element, so we'll post a spoiler (unpreviewable) link instead + if (spoiler) { return { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", body: `${emoji} Uploaded SPOILER file: ${external_url} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `
${emoji} Uploaded SPOILER file: ${external_url} (${pb(attachment.size)})
` + 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. diff --git a/src/discord/utils.js b/src/discord/utils.js index 6fb0b43..2f7a7e0 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -134,14 +134,17 @@ function timestampToSnowflakeInexact(timestamp) { return String((timestamp - EPOCH) * 2**22) } -/** @param {string} url */ -function getPublicUrlForCdn(url) { +/** + * @param {string} url + * @param {""|"spoiler/"} spoiler + */ +function getPublicUrlForCdn(url, spoiler="") { const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) if (!match) return url const unsignedHash = hasher.h64(match[3]) // attachment ID const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}` + return `${reg.ooye.bridge_origin}/download/discord${match[1]}${spoiler}/${match[2]}/${match[3]}/${match[4]}` } /** diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index 769fc9c..f802d17 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -9,7 +9,10 @@ let hasher = null // @ts-ignore require("xxhash-wasm")().then(h => hasher = h) -const {discord, as, select} = require("../../passthrough") +const {discord, as, select, sync} = require("../../passthrough") + +/** @type {import("../web-utils")} */ +const webUtils = sync.require("../web-utils") const schema = { params: z.object({ @@ -40,10 +43,16 @@ function timeUntilExpiry(url) { if (time > 0) return time } -function defineMediaProxyHandler(domain) { +/** + * @param {string} domain + * @param {boolean} spoiler + */ +function defineMediaProxyHandler(domain, spoiler) { return defineEventHandler(async event => { const params = await getValidatedRouterParams(event, schema.params.parse) + if (spoiler) webUtils.rejectUrlPreviewer(event) + const unsignedHash = hasher.h64(params.attachment_id) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range @@ -81,7 +90,9 @@ function defineMediaProxyHandler(domain) { }) } -as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com")) -as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net")) +as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com", false)) +as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net", false)) +as.router.get(`/download/discordcdn/:channel_id/spoiler/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com", true)) +as.router.get(`/download/discordmedia/:channel_id/spoiler/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net", true)) module.exports._cache = cache -- 2.34.1 From 966095c3610b5565c2dd902805ebfb3b00dd2187 Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 19 Oct 2026 13:57:48 +0200 Subject: [PATCH 02/11] of course, I fucked up where the / goes... --- src/discord/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/discord/utils.js b/src/discord/utils.js index 2f7a7e0..9d1775f 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -144,7 +144,7 @@ function getPublicUrlForCdn(url, spoiler="") { const unsignedHash = hasher.h64(match[3]) // attachment ID const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - return `${reg.ooye.bridge_origin}/download/discord${match[1]}${spoiler}/${match[2]}/${match[3]}/${match[4]}` + return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${spoiler}${match[2]}/${match[3]}/${match[4]}` } /** -- 2.34.1 From 4350bff202e83151b25c35ba8068537d75c06bf4 Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 19 Oct 2026 14:06:33 +0200 Subject: [PATCH 03/11] kill me --- src/discord/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/discord/utils.js b/src/discord/utils.js index 9d1775f..8effd88 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -144,7 +144,7 @@ function getPublicUrlForCdn(url, spoiler="") { const unsignedHash = hasher.h64(match[3]) // attachment ID const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${spoiler}${match[2]}/${match[3]}/${match[4]}` + return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${spoiler}${match[3]}/${match[4]}` } /** -- 2.34.1 From 727b5a3e4f97c59de6d0cd69d6adc00c6eed6d81 Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 19 Oct 2026 15:33:54 +0200 Subject: [PATCH 04/11] update tests --- src/d2m/converters/message-to-event.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 44b9fe6..233a4ba 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -366,9 +366,9 @@ test("message2event: spoiler attachment", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", + body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/spoiler/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", format: "org.matrix.custom.html", - formatted_body: "
šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)
" + formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/spoiler/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)" }]) }) @@ -1205,13 +1205,13 @@ test("message2event: multiple attachments are combined into the same event where msgtype: "m.text", body: "hey" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)" - + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)" + + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/spoiler/456/SPOILER_secret.jpg (38 KB)" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", formatted_body: "hey" + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` - + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)
` - + `šŸ“„ Uploaded file: hey.jpg (100 MB)` + + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/spoiler/456/SPOILER_secret.jpg (38 KB)` + + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", "m.mentions": {}, -- 2.34.1 From 5da1a090c6b1393be53154b77e4984b9705516fe Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 12:35:03 +0200 Subject: [PATCH 05/11] rewrite in QP --- src/d2m/converters/message-to-event.js | 2 +- src/d2m/converters/message-to-event.test.js | 8 +++--- src/discord/utils.js | 4 +-- src/web/routes/download-discord.js | 25 ++++++++++--------- src/web/routes/download-discord.test.js | 27 +++++++++++++++++++++ 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 62eed46..334d55e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -136,7 +136,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ */ async function attachmentToEvent(mentions, attachment, alwaysLink, di) { const spoiler = attachment.filename.startsWith("SPOILER_") || !!((attachment.flags || 0) & DiscordTypes.AttachmentFlags.IsSpoiler); - const external_url = dUtils.getPublicUrlForCdn(attachment.url, spoiler ? "spoiler/" : "") + const external_url = dUtils.getPublicUrlForCdn(attachment.url, spoiler ? "?spoiler" : "") const emoji = attachment.content_type?.startsWith("image/jp") ? "šŸ“ø" : attachment.content_type?.startsWith("image/") ? "šŸ–¼ļø" diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 233a4ba..59abb1e 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -366,9 +366,9 @@ test("message2event: spoiler attachment", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/spoiler/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", + body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler (74 KB)", format: "org.matrix.custom.html", - formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/spoiler/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)" + formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler (74 KB)" }]) }) @@ -1205,12 +1205,12 @@ test("message2event: multiple attachments are combined into the same event where msgtype: "m.text", body: "hey" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)" - + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/spoiler/456/SPOILER_secret.jpg (38 KB)" + + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler (38 KB)" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", formatted_body: "hey" + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` - + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/spoiler/456/SPOILER_secret.jpg (38 KB)` + + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler (38 KB)` + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", diff --git a/src/discord/utils.js b/src/discord/utils.js index 8effd88..19bf743 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -136,7 +136,7 @@ function timestampToSnowflakeInexact(timestamp) { /** * @param {string} url - * @param {""|"spoiler/"} spoiler + * @param {""|"?spoiler"} spoiler */ function getPublicUrlForCdn(url, spoiler="") { const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) @@ -144,7 +144,7 @@ function getPublicUrlForCdn(url, spoiler="") { const unsignedHash = hasher.h64(match[3]) // attachment ID const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${spoiler}${match[3]}/${match[4]}` + return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}${spoiler}` } /** diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index f802d17..550b669 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert/strict") -const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError, H3Event} = require("h3") +const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError, H3Event, getValidatedQuery} = require("h3") const {z} = require("zod") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore @@ -15,10 +15,13 @@ const {discord, as, select, sync} = require("../../passthrough") const webUtils = sync.require("../web-utils") const schema = { - params: z.object({ + router: z.object({ channel_id: z.string().regex(/^[0-9]+$/), attachment_id: z.string().regex(/^[0-9]+$/), file_name: z.string().regex(/^[-A-Za-z0-9_.,]+$/) + }), + query: z.object({ + spoiler: z.optional(z.string().max(0)) // Lets you choose between no query param, and a ?spoiler query param (H3 considers it an empty string). Booleans do NOT work here, unfortunately. }) } @@ -45,15 +48,15 @@ function timeUntilExpiry(url) { /** * @param {string} domain - * @param {boolean} spoiler */ -function defineMediaProxyHandler(domain, spoiler) { +function defineMediaProxyHandler(domain) { return defineEventHandler(async event => { - const params = await getValidatedRouterParams(event, schema.params.parse) + const routerParams = await getValidatedRouterParams(event, schema.router.parse) + const queryParams = await getValidatedQuery(event, schema.query.parse) - if (spoiler) webUtils.rejectUrlPreviewer(event) + if (queryParams.spoiler === "") webUtils.rejectUrlPreviewer(event) - const unsignedHash = hasher.h64(params.attachment_id) + const unsignedHash = hasher.h64(routerParams.attachment_id) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range const row = select("media_proxy", "permitted_hash", {permitted_hash: signedHash}).get() @@ -64,7 +67,7 @@ function defineMediaProxyHandler(domain, spoiler) { }) } - const url = `https://${domain}/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` + const url = `https://${domain}/attachments/${routerParams.channel_id}/${routerParams.attachment_id}/${routerParams.file_name}` let promise = cache.get(url) /** @type {string | undefined} */ let refreshed @@ -90,9 +93,7 @@ function defineMediaProxyHandler(domain, spoiler) { }) } -as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com", false)) -as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net", false)) -as.router.get(`/download/discordcdn/:channel_id/spoiler/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com", true)) -as.router.get(`/download/discordmedia/:channel_id/spoiler/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net", true)) +as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com")) +as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net")) module.exports._cache = cache diff --git a/src/web/routes/download-discord.test.js b/src/web/routes/download-discord.test.js index 0d4b884..e08832c 100644 --- a/src/web/routes/download-discord.test.js +++ b/src/web/routes/download-discord.test.js @@ -46,6 +46,33 @@ test("web download discord: works if a known attachment", async t => { t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/) }) +test("web download discord: works with spoilers", async t => { + const event = {} + await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name?spoiler", { + params: { + channel_id: "655216173696286746", + attachment_id: "1314358913482621010", + file_name: "image.png" + }, + event, + snow: { + channel: { + async refreshAttachmentURLs(attachments) { + assert(Array.isArray(attachments)) + return { + refreshed_urls: attachments.map(a => ({ + original: a, + refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}` + })) + } + } + } + } + }) + t.equal(event.node.res.statusCode, 302) + t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/) +}) + test("web download discord: uses cache", async t => { let notCalled = true const event = {} -- 2.34.1 From 15055b8ee09a98aaead8c48e462abb65c7d05c33 Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 13:22:18 +0200 Subject: [PATCH 06/11] Use less hacky way of setting the params --- src/d2m/converters/message-to-event.js | 2 +- src/d2m/converters/message-to-event.test.js | 226 ++++++++++---------- src/discord/utils.js | 8 +- src/web/routes/download-discord.js | 2 +- 4 files changed, 122 insertions(+), 116 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 334d55e..65fe3e4 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -136,7 +136,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ */ async function attachmentToEvent(mentions, attachment, alwaysLink, di) { const spoiler = attachment.filename.startsWith("SPOILER_") || !!((attachment.flags || 0) & DiscordTypes.AttachmentFlags.IsSpoiler); - const external_url = dUtils.getPublicUrlForCdn(attachment.url, spoiler ? "?spoiler" : "") + const external_url = dUtils.getPublicUrlForCdn(attachment.url, spoiler) const emoji = attachment.content_type?.startsWith("image/jp") ? "šŸ“ø" : attachment.content_type?.startsWith("image/") ? "šŸ–¼ļø" diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 59abb1e..dd2449f 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1,10 +1,10 @@ -const {test} = require("supertape") -const {messageToEvent} = require("./message-to-event") -const {MatrixServerError} = require("../../matrix/mreq") +const { test } = require("supertape") +const { messageToEvent } = require("./message-to-event") +const { MatrixServerError } = require("../../matrix/mreq") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../matrix/utils.test") +const { mockGetEffectivePower } = require("../../matrix/utils.test") const Ty = require("../../types") -const {db} = require("../../passthrough") +const { db } = require("../../passthrough") /** * @param {string} roomID @@ -12,7 +12,7 @@ const {db} = require("../../passthrough") * @returns {(roomID: string, eventID: string) => Promise>} */ function mockGetEvent(t, roomID_in, eventID_in, outer) { - return async function(roomID, eventID) { + return async function (roomID, eventID) { t.equal(roomID, roomID_in) t.equal(eventID, eventID_in) return new Promise(resolve => { @@ -74,8 +74,8 @@ test("message2event: simple room mention", async t => { t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -102,8 +102,8 @@ test("message2event: simple room link", async t => { t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -130,8 +130,8 @@ test("message2event: nicked room mention", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -234,8 +234,8 @@ test("message2event: simple message link", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:super.invalid": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:super.invalid": { display_name: null, avatar_url: null } } } } @@ -270,8 +270,8 @@ test("message2event: message link that OOYE doesn't know about", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -308,8 +308,8 @@ test("message2event: message timestamp failed to fetch", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -366,9 +366,9 @@ test("message2event: spoiler attachment", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler (74 KB)", + body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler= (74 KB)", format: "org.matrix.custom.html", - formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler (74 KB)" + formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler= (74 KB)" }]) }) @@ -505,7 +505,7 @@ test("message2event: simple reply to matrix user", async t => { }) test("message2event: simple reply to matrix user, reply fallbacks disabled", async t => { - const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {includeReplyFallback: false}, { + const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { includeReplyFallback: false }, { api: { getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { type: "m.room.message", @@ -605,17 +605,17 @@ test("message2event: voice message", async t => { const events = await messageToEvent(data.message.voice_message) t.deepEqual(events, [{ $type: "m.room.message", - body: "voice-message.ogg", - external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", - filename: "voice-message.ogg", - info: { - duration: 3960, - mimetype: "audio/ogg", - size: 10584, + body: "voice-message.ogg", + external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", + filename: "voice-message.ogg", + info: { + duration: 3960, + mimetype: "audio/ogg", + size: 10584, }, - "m.mentions": {}, - msgtype: "m.audio", - url: "mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB" + "m.mentions": {}, + msgtype: "m.audio", + url: "mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB" }]) }) @@ -628,16 +628,16 @@ test("message2event: misc file", async t => { "m.mentions": {} }, { $type: "m.room.message", - body: "the.yml", - external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1174514575220158545/the.yml", - filename: "the.yml", + body: "the.yml", + external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1174514575220158545/the.yml", + filename: "the.yml", info: { mimetype: "text/plain; charset=utf-8", size: 2274 }, - "m.mentions": {}, - msgtype: "m.file", - url: "mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP" + "m.mentions": {}, + msgtype: "m.file", + url: "mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP" }]) }) @@ -674,7 +674,7 @@ test("message2event: simple reply in thread to a matrix user's reply", async t = user_ids: ["@cadence:cadence.moe"] }, msgtype: "m.text", - body: "Well, they don't seem to..." + body: "Well, they don't seem to..." }]) }) @@ -711,7 +711,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext reply to infin user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `Most likely` + body: `Most likely` }]) }) @@ -748,7 +748,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext singleline rep user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `Most likely` + body: `Most likely` }]) }) @@ -757,11 +757,11 @@ test("message2event: reply to a Discord message that wasn't bridged", async t => t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", - body: `> In reply to a 1-day-old unbridged message:` + body: `> In reply to a 1-day-old unbridged message:` + `\n> Occimyy: BILLY BOB THE GREAT` + `\n\nenigmatic`, - format: "org.matrix.custom.html", - formatted_body: `
In reply to a 1-day-old unbridged message from Occimyy:
BILLY BOB THE GREAT
enigmatic`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to a 1-day-old unbridged message from Occimyy:
BILLY BOB THE GREAT
enigmatic`, "m.mentions": {} }]) }) @@ -1151,10 +1151,12 @@ test("message2event: very large png is converted to jpeg for upload", async t => 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 - }}) + return new Response("wa", { + headers: { + "Content-Type": "image/jpeg", + "Content-Length": 483085 + } + }) } }) t.deepEqual(events, [{ @@ -1205,12 +1207,12 @@ test("message2event: multiple attachments are combined into the same event where msgtype: "m.text", body: "hey" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)" - + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler (38 KB)" + + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler= (38 KB)" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", formatted_body: "hey" + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` - + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler (38 KB)` + + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler= (38 KB)` + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", @@ -1332,7 +1334,7 @@ test("message2event: emoji triple long name", async t => { body: ":brillillillilliant_move::brillillillilliant_move::brillillillilliant_move:", format: "org.matrix.custom.html", formatted_body: - ':brillillillilliant_move:' + ':brillillillilliant_move:' + ':brillillillilliant_move:' + ':brillillillilliant_move:' }]) @@ -1446,8 +1448,8 @@ test("message2event: constructed forwarded message", async t => { async getJoinedMembers() { return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -1505,8 +1507,8 @@ test("message2event: constructed forwarded text", async t => { async getJoinedMembers() { return { joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, - "@user:matrix.org": {display_name: null, avatar_url: null} + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@user:matrix.org": { display_name: null, avatar_url: null } } } } @@ -1550,10 +1552,10 @@ test("message2event: don't scan forwarded messages for mentions", async t => { }) test("message2event: invite no details embed if no event", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { snow: { invite: { - getInvite: async () => ({...data.invite.irl, guild_scheduled_event: null}) + getInvite: async () => ({ ...data.invite.irl, guild_scheduled_event: null }) } } }) @@ -1570,7 +1572,7 @@ test("message2event: invite no details embed if no event", async t => { }) test("message2event: irl invite event renders embed", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { snow: { invite: { getInvite: async () => data.invite.irl @@ -1603,7 +1605,7 @@ test("message2event: irl invite event renders embed", async t => { }) test("message2event: vc invite event renders embed", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { + const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381174024801095751" }, {}, {}, { snow: { invite: { getInvite: async () => data.invite.vc @@ -1637,12 +1639,12 @@ test("message2event: vc invite event renders embed", async t => { }) test("message2event: vc invite event renders embed with room link", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { + const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381174024801095751" }, {}, {}, { api: { getEffectivePower: mockGetEffectivePower(), getJoinedMembers: async () => ({ joined: { - "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, } }) }, @@ -1679,7 +1681,7 @@ test("message2event: vc invite event renders embed with room link", async t => { }) test("message2event: expired/invalid invites are sent as-is", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { snow: { invite: { async getInvite() { @@ -1704,11 +1706,11 @@ test("message2event: channel links are converted even inside lists (parser post- let called = 0 const events = await messageToEvent({ content: "1. Don't be a dick" - + "\n2. Follow rule number 1" - + "\n3. Follow Discord TOS" - + "\n4. Do **not** post NSFW content, shock content, suggestive content" - + "\n5. Please keep <#176333891320283136> professional and helpful, no random off-topic joking" - + "\nThis list will probably change in the future" + + "\n2. Follow rule number 1" + + "\n3. Follow Discord TOS" + + "\n4. Do **not** post NSFW content, shock content, suggestive content" + + "\n5. Please keep <#176333891320283136> professional and helpful, no random off-topic joking" + + "\nThis list will probably change in the future" }, data.guild.general, {}, { api: { getEffectivePower: mockGetEffectivePower(), @@ -1730,11 +1732,11 @@ test("message2event: channel links are converted even inside lists (parser post- { $type: "m.room.message", body: "1. Don't be a dick" - + "\n2. Follow rule number 1" - + "\n3. Follow Discord TOS" - + "\n4. Do **not** post NSFW content, shock content, suggestive content" - + "\n5. Please keep #wonderland professional and helpful, no random off-topic joking" - + "\nThis list will probably change in the future", + + "\n2. Follow rule number 1" + + "\n3. Follow Discord TOS" + + "\n4. Do **not** post NSFW content, shock content, suggestive content" + + "\n5. Please keep #wonderland professional and helpful, no random off-topic joking" + + "\nThis list will probably change in the future", format: "org.matrix.custom.html", formatted_body: "
  1. Don't be a dick
  2. Follow rule number 1
  3. Follow Discord TOS
  4. Do not post NSFW content, shock content, suggestive content
  5. Please keep #wonderland professional and helpful, no random off-topic joking
This list will probably change in the future", "m.mentions": {}, @@ -1783,31 +1785,33 @@ test("message2event: cross-room reply", async t => { } }, content: "cross-room reply" - }, {}, {}, {api: { - async getEvent(roomID, eventID) { - called++ - t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe") - t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY") - return { - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - "body": "image.png", - "info": { - "h": 738, - "mimetype": "image/png", - "org.matrix.msc4230.is_animated": false, - "size": 111189, - "w": 772, - "xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu" - }, - "m.mentions": {}, - "msgtype": "m.image", - "url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS" + }, {}, {}, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe") + t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY") + return { + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + "body": "image.png", + "info": { + "h": 738, + "mimetype": "image/png", + "org.matrix.msc4230.is_animated": false, + "size": 111189, + "w": 772, + "xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu" + }, + "m.mentions": {}, + "msgtype": "m.image", + "url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS" + } } } } - }}) + }) t.deepEqual(events, [ { $type: "m.room.message", @@ -1911,15 +1915,15 @@ test("message2event: single-choice poll", async t => { kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. max_selections: 1, answers: [{ - id: "1", - "org.matrix.msc1767.text": "[\ud83d\udc4d] answer one" - }, { - id: "2", - "org.matrix.msc1767.text": "[\ud83d\udc4e] answer two" - }, { - id: "3", - "org.matrix.msc1767.text": "answer three" - }] + id: "1", + "org.matrix.msc1767.text": "[\ud83d\udc4d] answer one" + }, { + id: "2", + "org.matrix.msc1767.text": "[\ud83d\udc4e] answer two" + }, { + id: "3", + "org.matrix.msc1767.text": "answer three" + }] }, "org.matrix.msc1767.text": "only one answer allowed!\n1. [\ud83d\udc4d] answer one\n2. [\ud83d\udc4e] answer two\n3. answer three" }]) @@ -1938,15 +1942,15 @@ test("message2event: multiple-choice poll", async t => { kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. max_selections: 3, answers: [{ - id: "1", - "org.matrix.msc1767.text": "[😭] no" - }, { - id: "2", - "org.matrix.msc1767.text": "oh no" - }, { - id: "3", - "org.matrix.msc1767.text": "oh noooooo" - }] + id: "1", + "org.matrix.msc1767.text": "[😭] no" + }, { + id: "2", + "org.matrix.msc1767.text": "oh no" + }, { + id: "3", + "org.matrix.msc1767.text": "oh noooooo" + }] }, "org.matrix.msc1767.text": "more than one answer allowed\n1. [😭] no\n2. oh no\n3. oh noooooo" }]) diff --git a/src/discord/utils.js b/src/discord/utils.js index 19bf743..40231d1 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -136,15 +136,17 @@ function timestampToSnowflakeInexact(timestamp) { /** * @param {string} url - * @param {""|"?spoiler"} spoiler + * @param {boolean} spoiler */ -function getPublicUrlForCdn(url, spoiler="") { +function getPublicUrlForCdn(url, spoiler=false) { const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) if (!match) return url const unsignedHash = hasher.h64(match[3]) // attachment ID const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}${spoiler}` + const transformedUrl = new URL(`${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}`) + if (spoiler) transformedUrl.searchParams.set("spoiler", "") + return transformedUrl.toString() } /** diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index 550b669..8b9c57d 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -21,7 +21,7 @@ const schema = { file_name: z.string().regex(/^[-A-Za-z0-9_.,]+$/) }), query: z.object({ - spoiler: z.optional(z.string().max(0)) // Lets you choose between no query param, and a ?spoiler query param (H3 considers it an empty string). Booleans do NOT work here, unfortunately. + spoiler: z.string().optional() }) } -- 2.34.1 From 2849c7a051708e15ad605a1b2f7cb67cd9cb0bd4 Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 13:31:03 +0200 Subject: [PATCH 07/11] yesn't --- src/d2m/converters/message-to-event.test.js | 8 ++++---- src/discord/utils.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index dd2449f..63c4611 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -366,9 +366,9 @@ test("message2event: spoiler attachment", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler= (74 KB)", + body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler=yes (74 KB)", format: "org.matrix.custom.html", - formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler= (74 KB)" + formatted_body: "šŸ“„ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci?spoiler=yes (74 KB)" }]) }) @@ -1207,12 +1207,12 @@ test("message2event: multiple attachments are combined into the same event where msgtype: "m.text", body: "hey" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)" - + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler= (38 KB)" + + "\nšŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler=yes (38 KB)" + "\nšŸ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", formatted_body: "hey" + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` - + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler= (38 KB)` + + `
šŸ“ø Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg?spoiler=yes (38 KB)` + `
šŸ“„ Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", diff --git a/src/discord/utils.js b/src/discord/utils.js index 40231d1..05ec3d1 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -145,7 +145,7 @@ function getPublicUrlForCdn(url, spoiler=false) { const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) const transformedUrl = new URL(`${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}`) - if (spoiler) transformedUrl.searchParams.set("spoiler", "") + if (spoiler) transformedUrl.searchParams.set("spoiler", "yes") return transformedUrl.toString() } -- 2.34.1 From 74cf6a1d8956588184ba618cbfcc6ab83179064a Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 13:33:47 +0200 Subject: [PATCH 08/11] shit --- src/web/routes/download-discord.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index 8b9c57d..aa03871 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -54,7 +54,7 @@ function defineMediaProxyHandler(domain) { const routerParams = await getValidatedRouterParams(event, schema.router.parse) const queryParams = await getValidatedQuery(event, schema.query.parse) - if (queryParams.spoiler === "") webUtils.rejectUrlPreviewer(event) + if (queryParams.spoiler) webUtils.rejectUrlPreviewer(event) const unsignedHash = hasher.h64(routerParams.attachment_id) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range -- 2.34.1 From cfafac2480f2a8a8a3a64ad1a20ba6710d82211a Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 14:12:51 +0200 Subject: [PATCH 09/11] fix format --- src/d2m/converters/message-to-event.test.js | 230 ++++++++++---------- 1 file changed, 113 insertions(+), 117 deletions(-) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 63c4611..fe4801c 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1,10 +1,10 @@ -const { test } = require("supertape") -const { messageToEvent } = require("./message-to-event") -const { MatrixServerError } = require("../../matrix/mreq") +const {test} = require("supertape") +const {messageToEvent} = require("./message-to-event") +const {MatrixServerError} = require("../../matrix/mreq") const data = require("../../../test/data") -const { mockGetEffectivePower } = require("../../matrix/utils.test") +const {mockGetEffectivePower} = require("../../matrix/utils.test") const Ty = require("../../types") -const { db } = require("../../passthrough") +const {db} = require("../../passthrough") /** * @param {string} roomID @@ -12,7 +12,7 @@ const { db } = require("../../passthrough") * @returns {(roomID: string, eventID: string) => Promise>} */ function mockGetEvent(t, roomID_in, eventID_in, outer) { - return async function (roomID, eventID) { + return async function(roomID, eventID) { t.equal(roomID, roomID_in) t.equal(eventID, eventID_in) return new Promise(resolve => { @@ -74,8 +74,8 @@ test("message2event: simple room mention", async t => { t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -102,8 +102,8 @@ test("message2event: simple room link", async t => { t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -130,8 +130,8 @@ test("message2event: nicked room mention", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -214,12 +214,12 @@ test("message2event: timestamps", async t => { + "\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)`, + formatted_body: `` + + `
` + + `
` + + `
` + + `
` + + `
`, "m.mentions": {} }]) }) @@ -234,8 +234,8 @@ test("message2event: simple message link", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:super.invalid": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:super.invalid": {display_name: null, avatar_url: null} } } } @@ -270,8 +270,8 @@ test("message2event: message link that OOYE doesn't know about", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -308,8 +308,8 @@ test("message2event: message timestamp failed to fetch", async t => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -505,7 +505,7 @@ test("message2event: simple reply to matrix user", async t => { }) test("message2event: simple reply to matrix user, reply fallbacks disabled", async t => { - const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { includeReplyFallback: false }, { + const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {includeReplyFallback: false}, { api: { getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { type: "m.room.message", @@ -605,17 +605,17 @@ test("message2event: voice message", async t => { const events = await messageToEvent(data.message.voice_message) t.deepEqual(events, [{ $type: "m.room.message", - body: "voice-message.ogg", - external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", - filename: "voice-message.ogg", - info: { - duration: 3960, - mimetype: "audio/ogg", - size: 10584, + body: "voice-message.ogg", + external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", + filename: "voice-message.ogg", + info: { + duration: 3960, + mimetype: "audio/ogg", + size: 10584, }, - "m.mentions": {}, - msgtype: "m.audio", - url: "mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB" + "m.mentions": {}, + msgtype: "m.audio", + url: "mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB" }]) }) @@ -628,16 +628,16 @@ test("message2event: misc file", async t => { "m.mentions": {} }, { $type: "m.room.message", - body: "the.yml", - external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1174514575220158545/the.yml", - filename: "the.yml", + body: "the.yml", + external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1174514575220158545/the.yml", + filename: "the.yml", info: { mimetype: "text/plain; charset=utf-8", size: 2274 }, - "m.mentions": {}, - msgtype: "m.file", - url: "mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP" + "m.mentions": {}, + msgtype: "m.file", + url: "mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP" }]) }) @@ -674,7 +674,7 @@ test("message2event: simple reply in thread to a matrix user's reply", async t = user_ids: ["@cadence:cadence.moe"] }, msgtype: "m.text", - body: "Well, they don't seem to..." + body: "Well, they don't seem to..." }]) }) @@ -711,7 +711,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext reply to infin user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `Most likely` + body: `Most likely` }]) }) @@ -748,7 +748,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext singleline rep user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `Most likely` + body: `Most likely` }]) }) @@ -757,11 +757,11 @@ test("message2event: reply to a Discord message that wasn't bridged", async t => t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", - body: `> In reply to a 1-day-old unbridged message:` + body: `> In reply to a 1-day-old unbridged message:` + `\n> Occimyy: BILLY BOB THE GREAT` + `\n\nenigmatic`, - format: "org.matrix.custom.html", - formatted_body: `
In reply to a 1-day-old unbridged message from Occimyy:
BILLY BOB THE GREAT
enigmatic`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to a 1-day-old unbridged message from Occimyy:
BILLY BOB THE GREAT
enigmatic`, "m.mentions": {} }]) }) @@ -1151,12 +1151,10 @@ test("message2event: very large png is converted to jpeg for upload", async t => 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 - } - }) + return new Response("wa", {headers: { + "Content-Type": "image/jpeg", + "Content-Length": 483085 + }}) } }) t.deepEqual(events, [{ @@ -1334,7 +1332,7 @@ test("message2event: emoji triple long name", async t => { body: ":brillillillilliant_move::brillillillilliant_move::brillillillilliant_move:", format: "org.matrix.custom.html", formatted_body: - ':brillillillilliant_move:' + ':brillillillilliant_move:' + ':brillillillilliant_move:' + ':brillillillilliant_move:' }]) @@ -1448,8 +1446,8 @@ test("message2event: constructed forwarded message", async t => { async getJoinedMembers() { return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -1507,8 +1505,8 @@ test("message2event: constructed forwarded text", async t => { async getJoinedMembers() { return { joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, - "@user:matrix.org": { display_name: null, avatar_url: null } + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} } } } @@ -1552,10 +1550,10 @@ test("message2event: don't scan forwarded messages for mentions", async t => { }) test("message2event: invite no details embed if no event", async t => { - const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { snow: { invite: { - getInvite: async () => ({ ...data.invite.irl, guild_scheduled_event: null }) + getInvite: async () => ({...data.invite.irl, guild_scheduled_event: null}) } } }) @@ -1572,7 +1570,7 @@ test("message2event: invite no details embed if no event", async t => { }) test("message2event: irl invite event renders embed", async t => { - const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { snow: { invite: { getInvite: async () => data.invite.irl @@ -1605,7 +1603,7 @@ test("message2event: irl invite event renders embed", async t => { }) test("message2event: vc invite event renders embed", async t => { - const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381174024801095751" }, {}, {}, { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { snow: { invite: { getInvite: async () => data.invite.vc @@ -1639,12 +1637,12 @@ test("message2event: vc invite event renders embed", async t => { }) test("message2event: vc invite event renders embed with room link", async t => { - const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381174024801095751" }, {}, {}, { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { api: { getEffectivePower: mockGetEffectivePower(), getJoinedMembers: async () => ({ joined: { - "@_ooye_bot:cadence.moe": { display_name: null, avatar_url: null }, + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, } }) }, @@ -1681,7 +1679,7 @@ test("message2event: vc invite event renders embed with room link", async t => { }) test("message2event: expired/invalid invites are sent as-is", async t => { - const events = await messageToEvent({ content: "https://discord.gg/placeholder?event=1381190945646710824" }, {}, {}, { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { snow: { invite: { async getInvite() { @@ -1706,11 +1704,11 @@ test("message2event: channel links are converted even inside lists (parser post- let called = 0 const events = await messageToEvent({ content: "1. Don't be a dick" - + "\n2. Follow rule number 1" - + "\n3. Follow Discord TOS" - + "\n4. Do **not** post NSFW content, shock content, suggestive content" - + "\n5. Please keep <#176333891320283136> professional and helpful, no random off-topic joking" - + "\nThis list will probably change in the future" + + "\n2. Follow rule number 1" + + "\n3. Follow Discord TOS" + + "\n4. Do **not** post NSFW content, shock content, suggestive content" + + "\n5. Please keep <#176333891320283136> professional and helpful, no random off-topic joking" + + "\nThis list will probably change in the future" }, data.guild.general, {}, { api: { getEffectivePower: mockGetEffectivePower(), @@ -1732,11 +1730,11 @@ test("message2event: channel links are converted even inside lists (parser post- { $type: "m.room.message", body: "1. Don't be a dick" - + "\n2. Follow rule number 1" - + "\n3. Follow Discord TOS" - + "\n4. Do **not** post NSFW content, shock content, suggestive content" - + "\n5. Please keep #wonderland professional and helpful, no random off-topic joking" - + "\nThis list will probably change in the future", + + "\n2. Follow rule number 1" + + "\n3. Follow Discord TOS" + + "\n4. Do **not** post NSFW content, shock content, suggestive content" + + "\n5. Please keep #wonderland professional and helpful, no random off-topic joking" + + "\nThis list will probably change in the future", format: "org.matrix.custom.html", formatted_body: "
  1. Don't be a dick
  2. Follow rule number 1
  3. Follow Discord TOS
  4. Do not post NSFW content, shock content, suggestive content
  5. Please keep #wonderland professional and helpful, no random off-topic joking
This list will probably change in the future", "m.mentions": {}, @@ -1785,33 +1783,31 @@ test("message2event: cross-room reply", async t => { } }, content: "cross-room reply" - }, {}, {}, { - api: { - async getEvent(roomID, eventID) { - called++ - t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe") - t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY") - return { - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - "body": "image.png", - "info": { - "h": 738, - "mimetype": "image/png", - "org.matrix.msc4230.is_animated": false, - "size": 111189, - "w": 772, - "xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu" - }, - "m.mentions": {}, - "msgtype": "m.image", - "url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS" - } + }, {}, {}, {api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe") + t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY") + return { + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + "body": "image.png", + "info": { + "h": 738, + "mimetype": "image/png", + "org.matrix.msc4230.is_animated": false, + "size": 111189, + "w": 772, + "xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu" + }, + "m.mentions": {}, + "msgtype": "m.image", + "url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS" } } } - }) + }}) t.deepEqual(events, [ { $type: "m.room.message", @@ -1915,15 +1911,15 @@ test("message2event: single-choice poll", async t => { kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. max_selections: 1, answers: [{ - id: "1", - "org.matrix.msc1767.text": "[\ud83d\udc4d] answer one" - }, { - id: "2", - "org.matrix.msc1767.text": "[\ud83d\udc4e] answer two" - }, { - id: "3", - "org.matrix.msc1767.text": "answer three" - }] + id: "1", + "org.matrix.msc1767.text": "[\ud83d\udc4d] answer one" + }, { + id: "2", + "org.matrix.msc1767.text": "[\ud83d\udc4e] answer two" + }, { + id: "3", + "org.matrix.msc1767.text": "answer three" + }] }, "org.matrix.msc1767.text": "only one answer allowed!\n1. [\ud83d\udc4d] answer one\n2. [\ud83d\udc4e] answer two\n3. answer three" }]) @@ -1942,15 +1938,15 @@ test("message2event: multiple-choice poll", async t => { kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. max_selections: 3, answers: [{ - id: "1", - "org.matrix.msc1767.text": "[😭] no" - }, { - id: "2", - "org.matrix.msc1767.text": "oh no" - }, { - id: "3", - "org.matrix.msc1767.text": "oh noooooo" - }] + id: "1", + "org.matrix.msc1767.text": "[😭] no" + }, { + id: "2", + "org.matrix.msc1767.text": "oh no" + }, { + id: "3", + "org.matrix.msc1767.text": "oh noooooo" + }] }, "org.matrix.msc1767.text": "more than one answer allowed\n1. [😭] no\n2. oh no\n3. oh noooooo" }]) -- 2.34.1 From 45a105a3042757bb9a8c8ca488703be8f61712f4 Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 14:32:23 +0200 Subject: [PATCH 10/11] some renames --- src/web/routes/download-discord.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index aa03871..6b9b458 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -15,7 +15,7 @@ const {discord, as, select, sync} = require("../../passthrough") const webUtils = sync.require("../web-utils") const schema = { - router: z.object({ + params: z.object({ channel_id: z.string().regex(/^[0-9]+$/), attachment_id: z.string().regex(/^[0-9]+$/), file_name: z.string().regex(/^[-A-Za-z0-9_.,]+$/) @@ -51,12 +51,12 @@ function timeUntilExpiry(url) { */ function defineMediaProxyHandler(domain) { return defineEventHandler(async event => { - const routerParams = await getValidatedRouterParams(event, schema.router.parse) - const queryParams = await getValidatedQuery(event, schema.query.parse) + const params = await getValidatedRouterParams(event, schema.params.parse) + const query = await getValidatedQuery(event, schema.query.parse) - if (queryParams.spoiler) webUtils.rejectUrlPreviewer(event) + if (query.spoiler) webUtils.rejectUrlPreviewer(event) - const unsignedHash = hasher.h64(routerParams.attachment_id) + const unsignedHash = hasher.h64(params.attachment_id) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range const row = select("media_proxy", "permitted_hash", {permitted_hash: signedHash}).get() @@ -67,7 +67,7 @@ function defineMediaProxyHandler(domain) { }) } - const url = `https://${domain}/attachments/${routerParams.channel_id}/${routerParams.attachment_id}/${routerParams.file_name}` + const url = `https://${domain}/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` let promise = cache.get(url) /** @type {string | undefined} */ let refreshed -- 2.34.1 From 11acba45e655d4209ed8035e866035299c31a64f Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 20 Oct 2026 14:32:34 +0200 Subject: [PATCH 11/11] stronger tests --- src/web/routes/download-discord.test.js | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/web/routes/download-discord.test.js b/src/web/routes/download-discord.test.js index e08832c..a1d6540 100644 --- a/src/web/routes/download-discord.test.js +++ b/src/web/routes/download-discord.test.js @@ -46,9 +46,9 @@ test("web download discord: works if a known attachment", async t => { t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/) }) -test("web download discord: works with spoilers", async t => { +test("web download discord: works with spoilers for humans", async t => { const event = {} - await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name?spoiler", { + await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name?spoiler=yes", { params: { channel_id: "655216173696286746", attachment_id: "1314358913482621010", @@ -73,6 +73,35 @@ test("web download discord: works with spoilers", async t => { t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/) }) +test("web download discord: rejects with spoilers for bots", async t => { + const event = {} + const [error] = await tryToCatch(() => router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name?spoiler=yes", { + params: { + channel_id: "655216173696286746", + attachment_id: "1314358913482621010", + file_name: "image.png" + }, + headers: { + "user-agent": "Synapse (bot; +https://github.com/matrix-org/synapse)" + }, + event, + snow: { + channel: { + async refreshAttachmentURLs(attachments) { + assert(Array.isArray(attachments)) + return { + refreshed_urls: attachments.map(a => ({ + original: a, + refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}` + })) + } + } + } + } + })) + t.equal(error.data, "Sorry URL previewer, you can't have this URL.") +}) + test("web download discord: uses cache", async t => { let notCalled = true const event = {} -- 2.34.1