diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js
index f8f9b57..6fdf32e 100644
--- a/d2m/converters/message-to-event.js
+++ b/d2m/converters/message-to-event.js
@@ -247,8 +247,19 @@ async function messageToEvent(message, guild, options = {}, 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_")) {
+ return {
+ $type: "m.room.message",
+ "m.mentions": mentions,
+ msgtype: "m.text",
+ body: `${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})`,
+ format: "org.matrix.custom.html",
+ formatted_body: `
${emoji} Uploaded SPOILER file: View (${pb(attachment.size)})
`
+ }
+ }
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
- if (attachment.size > reg.ooye.max_file_size) {
+ else if (attachment.size > reg.ooye.max_file_size) {
return {
$type: "m.room.message",
"m.mentions": mentions,
diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js
index 117cdac..0f9260b 100644
--- a/d2m/converters/message-to-event.test.js
+++ b/d2m/converters/message-to-event.test.js
@@ -104,6 +104,18 @@ test("message2event: attachment with no content", async t => {
}])
})
+test("message2event: spoiler attachment", async t => {
+ const events = await messageToEvent(data.message.spoiler_attachment, data.guild.general, {})
+ t.deepEqual(events, [{
+ $type: "m.room.message",
+ "m.mentions": {},
+ msgtype: "m.text",
+ body: "📄 Uploaded SPOILER file: https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)",
+ format: "org.matrix.custom.html",
+ formatted_body: "📄 Uploaded SPOILER file: View (74 KB)
"
+ }])
+})
+
test("message2event: stickers", async t => {
const events = await messageToEvent(data.message.sticker, data.guild.general, {})
t.deepEqual(events, [{
diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js
index 18a1e0f..e870675 100644
--- a/m2d/actions/send-event.js
+++ b/m2d/actions/send-event.js
@@ -1,6 +1,7 @@
// @ts-check
const assert = require("assert").strict
+const Ty = require("../../types")
const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../../passthrough")
const {sync, discord, db} = passthrough
@@ -12,9 +13,30 @@ const eventToMessage = sync.require("../converters/event-to-message")
/** @type {import("../../matrix/api")}) */
const api = sync.require("../../matrix/api")
-/** @param {import("../../types").Event.Outer} event */
+/**
+ * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {pendingFiles?: {name: string, url: string}[]}} message
+ * @returns {Promise}
+ */
+async function resolvePendingFiles(message) {
+ if (!message.pendingFiles) return message
+ const files = await Promise.all(message.pendingFiles.map(async p => {
+ const file = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x))
+ return {
+ name: p.name,
+ file
+ }
+ }))
+ const newMessage = {
+ ...message,
+ files
+ }
+ delete newMessage.pendingFiles
+ return newMessage
+}
+
+/** @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event */
async function sendEvent(event) {
- // TODO: we just assume the bridge has already been created
+ // TODO: we just assume the bridge has already been created, is that really ok?
const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id)
let channelID = row.channel_id
let threadID = undefined
@@ -29,7 +51,15 @@ async function sendEvent(event) {
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
- const {messagesToEdit, messagesToSend, messagesToDelete} = await eventToMessage.eventToMessage(event, guild, {api})
+ let {messagesToEdit, messagesToSend, messagesToDelete} = await eventToMessage.eventToMessage(event, guild, {api})
+
+ messagesToEdit = await Promise.all(messagesToEdit.map(async e => {
+ e.message = await resolvePendingFiles(e.message)
+ return e
+ }))
+ messagesToSend = await Promise.all(messagesToSend.map(message => {
+ return resolvePendingFiles(message)
+ }))
let eventPart = 0 // 0 is primary, 1 is supporting
@@ -48,7 +78,7 @@ async function sendEvent(event) {
for (const message of messagesToSend) {
const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID)
db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, channelID)
- db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content.msgtype || null, messageResponse.id, eventPart) // source 0 = matrix
+ db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content["msgtype"] || null, messageResponse.id, eventPart) // source 0 = matrix
eventPart = 1
messageResponses.push(messageResponse)
diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js
index deb7b9f..a8a5b65 100644
--- a/m2d/converters/event-to-message.js
+++ b/m2d/converters/event-to-message.js
@@ -4,6 +4,7 @@ const Ty = require("../../types")
const DiscordTypes = require("discord-api-types/v10")
const chunk = require("chunk-text")
const TurndownService = require("turndown")
+const assert = require("assert").strict
const passthrough = require("../../passthrough")
const { sync, db, discord } = passthrough
@@ -124,7 +125,7 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
}
/**
- * @param {Ty.Event.Outer} event
+ * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event
* @param {import("discord-api-types/v10").APIGuild} guild
* @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API
*/
@@ -143,12 +144,15 @@ async function eventToMessage(event, guild, di) {
// Try to extract an accurate display name and avatar URL from the member event
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
if (member.displayname) displayName = member.displayname
- if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url)
+ if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url) || undefined
let content = event.content.body // ultimate fallback
+ const attachments = []
+ /** @type {{name: string, url: string}[]} */
+ const pendingFiles = []
// Convert content depending on what the message is
- if (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") {
+ if (event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")) {
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
// this event ---is an edit of--> original event ---is a reply to--> past event
await (async () => {
@@ -261,7 +265,7 @@ async function eventToMessage(event, guild, di) {
// @ts-ignore bad type from turndown
content = turndownService.turndown(input)
- // It's optimised for commonmark, we need to replace the space-space-newline with just newline
+ // It's designed for commonmark, we need to replace the space-space-newline with just newline
content = content.replace(/ \n/g, "\n")
} else {
// Looks like we're using the plaintext body!
@@ -274,6 +278,23 @@ async function eventToMessage(event, guild, di) {
// Markdown needs to be escaped
content = content.replace(/([*_~`#])/g, `\\$1`)
}
+ } else 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.body
+ const url = utils.getPublicUrlForMxc(event.content.url)
+ assert(url)
+ attachments.push({id: "0", filename})
+ pendingFiles.push({name: filename, url})
+ } else if (event.type === "m.sticker") {
+ content = ""
+ let filename = event.content.body
+ if (event.type === "m.sticker" && event.content.info.mimetype.includes("/")) {
+ filename += "." + event.content.info.mimetype.split("/")[1]
+ }
+ const url = utils.getPublicUrlForMxc(event.content.url)
+ assert(url)
+ attachments.push({id: "0", filename})
+ pendingFiles.push({name: filename, url})
}
content = replyLine + content
@@ -286,6 +307,19 @@ async function eventToMessage(event, guild, di) {
avatar_url: avatarURL
})))
+ if (attachments.length) {
+ // If content is empty (should be the case when uploading a file) then chunk-text will create 0 messages.
+ // There needs to be a message to add attachments to.
+ if (!messages.length) messages.push({
+ content,
+ username: displayName,
+ avatar_url: avatarURL
+ })
+ messages[0].attachments = attachments
+ // @ts-ignore these will be converted to real files when the message is about to be sent
+ messages[0].pendingFiles = pendingFiles
+ }
+
const messagesToEdit = []
const messagesToSend = []
for (let i = 0; i < messages.length; i++) {
diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js
index 411d97c..5a83f0c 100644
--- a/m2d/converters/event-to-message.test.js
+++ b/m2d/converters/event-to-message.test.js
@@ -1110,3 +1110,107 @@ test("event2message: skips caching the member if the member does not exist, some
t.deepEqual(db.prepare("SELECT avatar_url, displayname, mxid FROM member_cache WHERE room_id = '!not_real:cadence.moe'").all(), [])
t.equal(called, 1, "getStateEvent should be called once")
})
+
+test("event2message: text attachments work", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.room.message",
+ content: {
+ body: "chiki-powerups.txt",
+ info: {
+ size: 971,
+ mimetype: "text/plain"
+ },
+ msgtype: "m.file",
+ url: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"
+ },
+ sender: "@cadence:cadence.moe",
+ event_id: "$c2WVyP6KcfAqh5imOa8e0xzt2C8JTR-cWbEd3GargEQ",
+ room_id: "!PnyBKvUBOhjuCucEfk:cadence.moe"
+ }),
+ {
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "",
+ avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ attachments: [{id: "0", filename: "chiki-powerups.txt"}],
+ pendingFiles: [{name: "chiki-powerups.txt", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}]
+ }]
+ }
+ )
+})
+
+test("event2message: image attachments work", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "cool cat.png",
+ info: {
+ size: 43170,
+ 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: "!PnyBKvUBOhjuCucEfk:cadence.moe"
+ }),
+ {
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "",
+ avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ attachments: [{id: "0", filename: "cool cat.png"}],
+ pendingFiles: [{name: "cool cat.png", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
+ }]
+ }
+ )
+})
+
+test("event2message: stickers work", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ type: "m.sticker",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "get_real2",
+ url: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN",
+ info: {
+ w: 320,
+ h: 298,
+ mimetype: "image/gif",
+ size: 331394,
+ thumbnail_info: {
+ w: 320,
+ h: 298,
+ mimetype: "image/gif",
+ size: 331394
+ },
+ thumbnail_url: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"
+ }
+ },
+ event_id: "$PdI-KjdQ8Z_Tb4x9_7wKRPZCsrrXym4BXtbAPekypuM",
+ room_id: "!PnyBKvUBOhjuCucEfk:cadence.moe"
+ }),
+ {
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "",
+ avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
+ attachments: [{id: "0", filename: "get_real2.gif"}],
+ pendingFiles: [{name: "get_real2.gif", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/NyMXQFAAdniImbHzsygScbmN"}]
+ }]
+ }
+ )
+})
diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js
index 6adacf7..026b067 100644
--- a/m2d/event-dispatcher.js
+++ b/m2d/event-dispatcher.js
@@ -54,7 +54,16 @@ function guard(type, fn) {
sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
/**
- * @param {Ty.Event.Outer} event it is a m.room.message because that's what this listener is filtering for
+ * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
+ */
+async event => {
+ if (utils.eventSenderIsFromDiscord(event.sender)) return
+ const messageResponses = await sendEvent.sendEvent(event)
+}))
+
+sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
+/**
+ * @param {Ty.Event.M_Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for
*/
async event => {
if (utils.eventSenderIsFromDiscord(event.sender)) return
diff --git a/test/data.js b/test/data.js
index 32ee3b0..98c943a 100644
--- a/test/data.js
+++ b/test/data.js
@@ -531,6 +531,44 @@ module.exports = {
flags: 0,
components: []
},
+ spoiler_attachment: {
+ type: 0,
+ tts: false,
+ timestamp: '2023-09-02T09:38:29.480000+00:00',
+ referenced_message: null,
+ pinned: false,
+ nonce: '1147465562901708800',
+ mentions: [],
+ mention_roles: [],
+ mention_everyone: false,
+ id: '1147465564600676383',
+ flags: 0,
+ embeds: [],
+ edited_timestamp: null,
+ content: '',
+ components: [],
+ channel_id: '1100319550446252084',
+ author: {
+ username: 'cadence.worm',
+ public_flags: 0,
+ id: '772659086046658620',
+ global_name: 'cadence',
+ discriminator: '0',
+ avatar_decoration_data: null,
+ avatar: '4b5c4b28051144e4c111f0113a0f1cf1'
+ },
+ attachments: [
+ {
+ url: 'https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci',
+ size: 73792,
+ proxy_url: 'https://media.discordapp.net/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci',
+ id: '1147465564307079258',
+ flags: 8,
+ filename: 'SPOILER_69-GNDP-CADENCE.nfs.gci'
+ }
+ ],
+ guild_id: '1100319549670301727'
+ },
skull_webp_attachment_with_content: {
type: 0,
tts: false,
diff --git a/types.d.ts b/types.d.ts
index dcde3ad..b74172b 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -80,6 +80,39 @@ export namespace Event {
}
}
+ export type M_Outer_M_Room_Message = Outer & {type: "m.room.message"}
+
+ export type M_Room_Message_File = {
+ msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
+ body: string
+ url: string
+ info?: any
+ "m.relates_to"?: {
+ "m.in_reply_to": {
+ event_id: string
+ }
+ rel_type?: "m.replace"
+ event_id?: string
+ }
+ }
+
+ export type M_Outer_M_Room_Message_File = Outer & {type: "m.room.message"}
+
+ export type M_Sticker = {
+ body: string
+ url: string
+ info: {
+ mimetype: string
+ w?: number
+ h?: number
+ size?: number
+ thumbnail_info?: any
+ thumbnail_url?: string
+ }
+ }
+
+ export type M_Outer_M_Sticker = Outer & {type: "m.sticker"}
+
export type M_Room_Member = {
membership: string
displayname?: string