diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js
index 92bc241..2026e07 100644
--- a/d2m/converters/message-to-event.js
+++ b/d2m/converters/message-to-event.js
@@ -269,12 +269,12 @@ async function messageToEvent(message, guild, options = {}, di) {
*/
async function transformContentMessageLinks(content) {
let offset = 0
- for (const match of [...content.matchAll(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)/g)]) {
+ for (const match of [...content.matchAll(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/[0-9]+\/([0-9]+)\/([0-9]+)/g)]) {
assert(typeof match.index === "number")
- const channelID = match[2]
- const messageID = match[3]
- const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
+ const [_, channelID, messageID] = match
let result
+
+ const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
if (roomID) {
const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get()
if (eventID && roomID) {
@@ -287,6 +287,7 @@ async function messageToEvent(message, guild, options = {}, di) {
} else {
result = `${match[0]} [event is from another server]`
}
+
content = content.slice(0, match.index + offset) + result + content.slice(match.index + match[0].length + offset)
offset += result.length - match[0].length
}
diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js
index ec84e07..f7053cd 100644
--- a/m2d/converters/event-to-message.js
+++ b/m2d/converters/event-to-message.js
@@ -11,7 +11,9 @@ const entities = require("entities")
const passthrough = require("../../passthrough")
const {sync, db, discord, select, from} = passthrough
/** @type {import("../converters/utils")} */
-const utils = sync.require("../converters/utils")
+const mxUtils = sync.require("../converters/utils")
+/** @type {import("../../discord/utils")} */
+const dUtils = sync.require("../../discord/utils")
/** @type {import("./emoji-sheet")} */
const emojiSheet = sync.require("./emoji-sheet")
@@ -102,6 +104,7 @@ turndownService.addRule("inlineLink", {
replacement: function (content, node) {
if (node.getAttribute("data-user-id")) return `<@${node.getAttribute("data-user-id")}>`
+ if (node.getAttribute("data-message-id")) return `https://discord.com/channels/${node.getAttribute("data-guild-id")}/${node.getAttribute("data-channel-id")}/${node.getAttribute("data-message-id")}`
if (node.getAttribute("data-channel-id")) return `<#${node.getAttribute("data-channel-id")}>`
const href = node.getAttribute("href")
let brackets = ["", ""]
@@ -162,7 +165,7 @@ turndownService.addRule("emoji", {
return `<::>`
} else {
// We prefer not to upload this as a sprite sheet because the emoji is not at the end of the message, it is in the middle.
- return `[${node.getAttribute("title")}](${utils.getPublicUrlForMxc(mxcUrl)})`
+ return `[${node.getAttribute("title")}](${mxUtils.getPublicUrlForMxc(mxcUrl)})`
}
}
})
@@ -276,7 +279,7 @@ 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) || undefined
+ if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) || undefined
// If the display name is too long to be put into the webhook (80 characters is the maximum),
// put the excess characters into displayNameRunoff, later to be put at the top of the message
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
@@ -390,7 +393,7 @@ async function eventToMessage(event, guild, di) {
// Handling mentions of Discord users
input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => {
- if (utils.eventSenderIsFromDiscord(mxid)) {
+ if (mxUtils.eventSenderIsFromDiscord(mxid)) {
// Handle mention of an OOYE sim user by their mxid
const userID = select("sim", "user_id", {mxid: mxid}).pluck().get()
if (!userID) return whole
@@ -405,12 +408,42 @@ async function eventToMessage(event, guild, di) {
}
})
- // Handling mentions of Discord rooms
- input = input.replace(/("https:\/\/matrix.to\/#\/(![^"]+)")>/g, (whole, attributeValue, roomID) => {
+ // Handling mentions of rooms and room-messages
+ let offset = 0
+ for (const match of [...input.matchAll(/("https:\/\/matrix.to\/#\/(![^"/?]+)(?:\/(\$[^"/?]+))?(?:\?[^"]*)?")>/g)]) {
+ assert(typeof match.index === "number")
+ const [_, attributeValue, roomID, eventID] = match
+ let result
+
+ // Don't process links that are part of the reply fallback, they'll be removed entirely by turndown
+ if (input.slice(match.index + match[0].length + offset).startsWith("In reply to")) continue
+
const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get()
- if (!channelID) return whole
- return `${attributeValue} data-channel-id="${channelID}">`
- })
+ if (!channelID) continue
+ if (!eventID) {
+ // 1: It's a room link, so <#link> to the channel
+ result = `${attributeValue} data-channel-id="${channelID}">`
+ } else {
+ // Linking to a particular event with a discord.com/channels/guildID/channelID/messageID link
+ // Need to know the guildID and messageID
+ const guildID = discord.channels.get(channelID)?.["guild_id"]
+ if (!guildID) continue
+ const messageID = select("event_message", "message_id", {event_id: eventID}).pluck().get()
+ if (messageID) {
+ // 2: Linking to a known event
+ result = `${attributeValue} data-channel-id="${channelID}" data-guild-id="${guildID}" data-message-id="${messageID}">`
+ } else {
+ // 3: Linking to an unknown event that OOYE didn't originally bridge - we can guess messageID from the timestamp
+ const originalEvent = await di.api.getEvent(roomID, eventID)
+ if (!originalEvent) continue
+ const guessedMessageID = dUtils.timestampToSnowflakeInexact(originalEvent.origin_server_ts)
+ result = `${attributeValue} data-channel-id="${channelID}" data-guild-id="${guildID}" data-message-id="${guessedMessageID}">`
+ }
+ }
+
+ input = input.slice(0, match.index + offset) + result + input.slice(match.index + match[0].length + offset)
+ offset += result.length - match[0].length
+ }
// Stripping colons after mentions
input = input.replace(/( data-user-id.*?<\/a>):?/g, "$1")
@@ -430,7 +463,7 @@ async function eventToMessage(event, guild, di) {
beforeTag = beforeTag || ""
afterContext = afterContext || ""
afterTag = afterTag || ""
- if (!utils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !utils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
+ if (!mxUtils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !mxUtils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
return beforeContext + "
" + afterContext
} else {
return whole
@@ -480,13 +513,13 @@ async function eventToMessage(event, guild, di) {
const filename = event.content.body
if ("url" in event.content) {
// Unencrypted
- const url = utils.getPublicUrlForMxc(event.content.url)
+ const url = mxUtils.getPublicUrlForMxc(event.content.url)
assert(url)
attachments.push({id: "0", filename})
pendingFiles.push({name: filename, url})
} else {
// Encrypted
- const url = utils.getPublicUrlForMxc(event.content.file.url)
+ const url = mxUtils.getPublicUrlForMxc(event.content.file.url)
assert(url)
assert.equal(event.content.file.key.alg, "A256CTR")
attachments.push({id: "0", filename})
@@ -494,7 +527,7 @@ async function eventToMessage(event, guild, di) {
}
} else if (event.type === "m.sticker") {
content = ""
- const url = utils.getPublicUrlForMxc(event.content.url)
+ const url = mxUtils.getPublicUrlForMxc(event.content.url)
assert(url)
let filename = event.content.body
if (event.type === "m.sticker") {
diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js
index 6dbe112..9d2c70c 100644
--- a/m2d/converters/event-to-message.test.js
+++ b/m2d/converters/event-to-message.test.js
@@ -1701,7 +1701,7 @@ test("event2message: mentioning bridged rooms works", async t => {
msgtype: "m.text",
body: "wrong body",
format: "org.matrix.custom.html",
- formatted_body: `I'm just worm-form testing channel mentions`
+ formatted_body: `I'm just worm-farm testing channel mentions`
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
origin_server_ts: 1688301929913,
@@ -1725,6 +1725,112 @@ test("event2message: mentioning bridged rooms works", async t => {
)
})
+test("event2message: mentioning known bridged events works", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: `it was uploaded earlier in amanda-spam`
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ origin_server_ts: 1688301929913,
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
+ sender: "@cadence:cadence.moe",
+ type: "m.room.message",
+ unsigned: {
+ age: 405299
+ }
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "it was uploaded earlier in https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020",
+ avatar_url: undefined
+ }]
+ }
+ )
+})
+
+test("event2message: mentioning unknown bridged events works", async t => {
+ let called = 0
+ t.deepEqual(
+ await eventToMessage({
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: `it was uploaded years ago in amanda-spam`
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ origin_server_ts: 1688301929913,
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
+ sender: "@cadence:cadence.moe",
+ type: "m.room.message",
+ unsigned: {
+ age: 405299
+ }
+ }, {}, {
+ api: {
+ async getEvent(roomID, eventID) {
+ called++
+ t.equal(roomID, "!CzvdIdUQXgUjDVKxeU:cadence.moe")
+ t.equal(eventID, "$zpzx6ABetMl8BrpsFbdZ7AefVU1Y_-t97bJRJM2JyW0")
+ return {
+ origin_server_ts: 1599813121000
+ }
+ }
+ }
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "it was uploaded years ago in https://discord.com/channels/497159726455455754/497161350934560778/753895613661184000",
+ avatar_url: undefined
+ }]
+ }
+ )
+ t.equal(called, 1, "getEvent should be called once")
+})
+
+test("event2message: link to event in an unknown room", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: 'ah yeah, here\'s where the bug was reported: https://matrix.to/#/!QtykxKocfZaZOUrTwp:matrix.org/$1542477546853947KGhZL:matrix.org'
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ origin_server_ts: 1688301929913,
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
+ sender: "@cadence:cadence.moe",
+ type: "m.room.message",
+ unsigned: {
+ age: 405299
+ }
+ }),
+ {
+ ensureJoined: [],
+ messagesToDelete: [],
+ messagesToEdit: [],
+ messagesToSend: [{
+ username: "cadence [they]",
+ content: "ah yeah, here's where the bug was reported: [https://matrix.to/#/!QtykxKocfZaZOUrTwp:matrix.org/$1542477546853947KGhZL:matrix.org]()",
+ avatar_url: undefined
+ }]
+ }
+ )
+})
+
test("event2message: colon after mentions is stripped", async t => {
t.deepEqual(
await eventToMessage({
diff --git a/test/test.js b/test/test.js
index 553ec44..4663b18 100644
--- a/test/test.js
+++ b/test/test.js
@@ -24,7 +24,12 @@ const discord = {
]),
application: {
id: "684280192553844747"
- }
+ },
+ channels: new Map([
+ ["497161350934560778", {
+ guild_id: "497159726455455754"
+ }]
+ ])
}
Object.assign(passthrough, { discord, config, sync, db })