diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js
index 61525e2..fd9289d 100644
--- a/src/m2d/converters/event-to-message.js
+++ b/src/m2d/converters/event-to-message.js
@@ -8,6 +8,8 @@ const TurndownService = require("@cloudrac3r/turndown")
const domino = require("domino")
const assert = require("assert").strict
const entities = require("entities")
+const pb = require("prettier-bytes")
+const {tag} = require("@cloudrac3r/html-template-tag")
const passthrough = require("../../passthrough")
const {sync, db, discord, select, from} = passthrough
@@ -483,6 +485,17 @@ const attachmentEmojis = new Map([
["m.file", "📄"]
])
+/** @param {DiscordTypes.APIGuild} guild */
+function getFileSizeForGuild(guild) {
+ // guild.features may include strings such as MAX_FILE_SIZE_50_MB and MAX_FILE_SIZE_100_MB, which are the current server boost amounts
+ const fileSizeFeature = guild?.features.map(f => Number(f.match(/^MAX_FILE_SIZE_([0-9]+)_MB$/)?.[1])).filter(f => f).sort()[0]
+ if (fileSizeFeature) {
+ return fileSizeFeature * 1024 * 1024 // discord uses big megabytes
+ } else {
+ return 10 * 1024 * 1024 // default file size is 10 MB
+ }
+}
+
async function getL1L2ReplyLine(called = false) {
// @ts-ignore
const autoEmoji = new Map(select("auto_emoji", ["name", "emoji_id"], {}, "WHERE name = 'L1' OR name = 'L2'").raw().all())
@@ -539,21 +552,39 @@ async function eventToMessage(event, guild, di) {
// Handle images first - might need to handle their `body`/`formatted_body` as well, which will fall through to the text processor
let shouldProcessTextEvent = event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")
if (event.type === "m.room.message" && (event.content.msgtype === "m.file" || event.content.msgtype === "m.video" || event.content.msgtype === "m.audio" || event.content.msgtype === "m.image")) {
- content = ""
- const filename = event.content.filename || event.content.body
- if ("file" in event.content) {
- // Encrypted
- assert.equal(event.content.file.key.alg, "A256CTR")
- attachments.push({id: "0", filename})
- pendingFiles.push({name: filename, mxc: event.content.file.url, key: event.content.file.key.k, iv: event.content.file.iv})
- } else {
- // Unencrypted
- attachments.push({id: "0", filename})
- pendingFiles.push({name: filename, mxc: event.content.url})
- }
- // Check if we also need to process a text event for this image - if it has a caption that's different from its filename
- if ((event.content.body && event.content.filename && event.content.body !== event.content.filename) || event.content.formatted_body) {
+ if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) {
+ // Upload (unencrypted) file as link, because it's too large for Discord
+ // Do this by constructing a sample Matrix message with the link and then use the text processor to convert that + the original caption.
+ const url = mxUtils.getPublicUrlForMxc(event.content.url)
+ assert(url)
+ const filename = event.content.filename || event.content.body
+ const newText = new mxUtils.MatrixStringBuilder()
+ const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : ""
+ newText.addLine(`${emoji}Uploaded file: ${url} (${pb(event.content.info.size)})`, tag`${emoji}Uploaded file: ${filename} (${pb(event.content.info.size)})`)
+ // Check if the event has a caption that we need to add as well
+ if ((event.content.body && event.content.filename && event.content.body !== event.content.filename) || event.content.formatted_body) {
+ newText.addLine(event.content.body || "", event.content.formatted_body || tag`${event.content.body || ""}`)
+ }
+ Object.assign(event.content, newText.get())
shouldProcessTextEvent = true
+ } else {
+ // Upload file as file
+ content = ""
+ const filename = event.content.filename || event.content.body
+ if ("file" in event.content) {
+ // Encrypted
+ assert.equal(event.content.file.key.alg, "A256CTR")
+ attachments.push({id: "0", filename})
+ pendingFiles.push({name: filename, mxc: event.content.file.url, key: event.content.file.key.k, iv: event.content.file.iv})
+ } else {
+ // Unencrypted
+ attachments.push({id: "0", filename})
+ pendingFiles.push({name: filename, mxc: event.content.url})
+ }
+ // Check if we also need to process a text event for this image - if it has a caption that's different from its filename
+ if ((event.content.body && event.content.filename && event.content.body !== event.content.filename) || event.content.formatted_body) {
+ shouldProcessTextEvent = true
+ }
}
}
if (event.type === "m.sticker") {
diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js
index 0d65b9d..73ca4e9 100644
--- a/src/m2d/converters/event-to-message.test.js
+++ b/src/m2d/converters/event-to-message.test.js
@@ -4041,6 +4041,166 @@ test("event2message: evil encrypted image attachment works", async t => {
)
})
+test("event2message: large attachments are uploaded if the server boost level is sufficient", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "cool cat.png",
+ filename: "cool cat.png",
+ info: {
+ size: 90_000_000,
+ mimetype: "image/png",
+ w: 480,
+ h: 480,
+ "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
+ },
+ msgtype: "m.image",
+ url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
+ },
+ event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
+ room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
+ }, {
+ features: ["MAX_FILE_SIZE_100_MB"]
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "",
+ avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ attachments: [{id: "0", filename: "cool cat.png"}],
+ pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
+ }]
+ }
+ )
+})
+
+test("event2message: files too large for Discord are linked as as URL", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "cool cat.png",
+ filename: "cool cat.png",
+ info: {
+ size: 40_000_000,
+ mimetype: "image/png",
+ w: 480,
+ h: 480,
+ "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
+ },
+ msgtype: "m.image",
+ url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
+ },
+ event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
+ room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_",
+ avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ allowed_mentions: {
+ parse: ["users", "roles"]
+ }
+ }]
+ }
+ )
+})
+
+test("event2message: files too large for Discord can have a plaintext caption", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "Cat emoji surrounded by pink hearts",
+ filename: "cool cat.png",
+ info: {
+ size: 40_000_000,
+ mimetype: "image/png",
+ w: 480,
+ h: 480,
+ "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
+ },
+ msgtype: "m.image",
+ url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
+ },
+ event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
+ room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_\nCat emoji surrounded by pink hearts",
+ avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ allowed_mentions: {
+ parse: ["users", "roles"]
+ }
+ }]
+ }
+ )
+})
+
+test("event2message: files too large for Discord can have a formatted caption", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ content: {
+ body: "this event has `formatting`",
+ filename: "5740.jpg",
+ format: "org.matrix.custom.html",
+ formatted_body: "this event has formatting",
+ info: {
+ h: 1340,
+ mimetype: "image/jpeg",
+ size: 40_000_000,
+ thumbnail_info: {
+ h: 670,
+ mimetype: "image/jpeg",
+ size: 80157,
+ w: 540
+ },
+ thumbnail_url: "mxc://thomcat.rocks/XhLsOCDBYyearsLQgUUrbAvw",
+ w: 1080,
+ "xyz.amorgan.blurhash": "KHJQG*55ic-.}?0M58J.9v"
+ },
+ msgtype: "m.image",
+ url: "mxc://thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh"
+ },
+ origin_server_ts: 1740607766895,
+ sender: "@cadence:cadence.moe",
+ type: "m.room.message",
+ event_id: "$NqNqVgukiQm1nynm9vIr9FIq31hZpQ3udOd7cBIW46U",
+ room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "🖼️ _Uploaded file: [5740.jpg](https://bridge.example.org/download/matrix/thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh) (40 MB)_\nthis event has `formatting`",
+ avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ allowed_mentions: {
+ parse: ["users", "roles"]
+ }
+ }]
+ }
+ )
+})
+
+
test("event2message: stickers work", async t => {
t.deepEqual(
await eventToMessage({