diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js
index b0bc072..404b555 100644
--- a/m2d/actions/channel-webhook.js
+++ b/m2d/actions/channel-webhook.js
@@ -58,6 +58,20 @@ async function sendMessageWithWebhook(channelID, data, threadID) {
return result
}
+/**
+ * @param {string} channelID
+ * @param {string} messageID
+ * @param {DiscordTypes.RESTPatchAPIWebhookWithTokenMessageJSONBody & {files?: {name: string, file: Buffer}[]}} data
+ * @param {string} [threadID]
+ */
+async function editMessageWithWebhook(channelID, messageID, data, threadID) {
+ const result = await withWebhook(channelID, async webhook => {
+ return discord.snow.webhook.editWebhookMessage(webhook.id, webhook.token, messageID, {...data, thread_id: threadID})
+ })
+ return result
+}
+
module.exports.ensureWebhook = ensureWebhook
module.exports.withWebhook = withWebhook
module.exports.sendMessageWithWebhook = sendMessageWithWebhook
+module.exports.editMessageWithWebhook = editMessageWithWebhook
diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js
index 6360d64..2667d26 100644
--- a/m2d/actions/send-event.js
+++ b/m2d/actions/send-event.js
@@ -34,9 +34,11 @@ async function sendEvent(event) {
/** @type {DiscordTypes.APIMessage[]} */
const messageResponses = []
let eventPart = 0 // 0 is primary, 1 is supporting
- // for (const message of messagesToEdit) {
- // eventPart = 1
- // TODO ...
+ for (const data of messagesToEdit) {
+ const messageResponse = await channelWebhook.editMessageWithWebhook(channelID, data.id, data.message, threadID)
+ eventPart = 1
+ messageResponses.push(messageResponse)
+ }
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)
diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js
index 5e6fa55..92e5ca4 100644
--- a/m2d/converters/event-to-message.js
+++ b/m2d/converters/event-to-message.js
@@ -141,19 +141,10 @@ async function eventToMessage(event, guild, di) {
if (member.displayname) displayName = member.displayname
if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url)
- // Convert content depending on what the message is
let content = event.content.body // ultimate fallback
- if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) {
- let input = event.content.formatted_body
- if (event.content.msgtype === "m.emote") {
- input = `* ${displayName} ${input}`
- }
-
- // Note: Element's renderers on Web and Android currently collapse whitespace, like the browser does. Turndown also collapses whitespace which is good for me.
- // If later I'm using a client that doesn't collapse whitespace and I want turndown to follow suit, uncomment the following line of code, and it Just Works:
- // input = input.replace(/ /g, " ")
- // There is also a corresponding test to uncomment, named "event2message: whitespace is retained"
+ // Convert content depending on what the message is
+ if (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 () => {
@@ -167,16 +158,26 @@ async function eventToMessage(event, guild, di) {
if (!originalEventId) return
messageIDsToEdit = db.prepare("SELECT message_id FROM event_message WHERE event_id = ? ORDER BY part").pluck().all(originalEventId)
if (!messageIDsToEdit.length) return
+
+ // Ok, it's an edit.
+ event.content = event.content["m.new_content"]
+
+ // Is it editing a reply? We need special handling if it is.
// Get the original event, then check if it was a reply
const originalEvent = await di.api.getEvent(event.room_id, originalEventId)
if (!originalEvent) return
const repliedToEventId = originalEvent.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
if (!repliedToEventId) return
+
// After all that, it's an edit of a reply.
- // We'll be sneaky and prepare the message data so that everything else can handle it just like original messages.
- Object.assign(event.content, event.content["m.new_content"])
- input = event.content.formatted_body || event.content.body
- relatesTo["m.in_reply_to"] = {event_id: repliedToEventId}
+ // We'll be sneaky and prepare the message data so that the next steps can handle it just like original messages.
+ Object.assign(event.content, {
+ "m.relates_to": {
+ "m.in_reply_to": {
+ event_id: repliedToEventId
+ }
+ }
+ })
})()
// Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver.
@@ -206,51 +207,69 @@ async function eventToMessage(event, guild, di) {
replyLine = `> ${replyLine}\n> ${contentPreview}\n`
})()
- // Handling mentions of Discord users
- input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => {
- if (!utils.eventSenderIsFromDiscord(mxid)) return whole
- const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid)
- if (!userID) return whole
- return `${attributeValue} data-user-id="${userID}">`
- })
-
- // Handling mentions of Discord rooms
- input = input.replace(/("https:\/\/matrix.to\/#\/(![^"]+)")>/g, (whole, attributeValue, roomID) => {
- const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(roomID)
- if (!channelID) return whole
- return `${attributeValue} data-channel-id="${channelID}">`
- })
-
- // Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those.
- input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "")
-
- // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason.
- // But I should not count it if it's between block elements.
- input = input.replace(/(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g, (whole, beforeContext, beforeTag, afterContext, afterTag) => {
- // console.error(beforeContext, beforeTag, afterContext, afterTag)
- if (typeof beforeTag !== "string" && typeof afterTag !== "string") {
- return "
"
+ if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) {
+ let input = event.content.formatted_body
+ if (event.content.msgtype === "m.emote") {
+ input = `* ${displayName} ${input}`
}
- beforeContext = beforeContext || ""
- beforeTag = beforeTag || ""
- afterContext = afterContext || ""
- afterTag = afterTag || ""
- if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
- return beforeContext + "
" + afterContext
- } else {
- return whole
+
+ // Handling mentions of Discord users
+ input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => {
+ if (!utils.eventSenderIsFromDiscord(mxid)) return whole
+ const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid)
+ if (!userID) return whole
+ return `${attributeValue} data-user-id="${userID}">`
+ })
+
+ // Handling mentions of Discord rooms
+ input = input.replace(/("https:\/\/matrix.to\/#\/(![^"]+)")>/g, (whole, attributeValue, roomID) => {
+ const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(roomID)
+ if (!channelID) return whole
+ return `${attributeValue} data-channel-id="${channelID}">`
+ })
+
+ // Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those.
+ input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "")
+
+ // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason.
+ // But I should not count it if it's between block elements.
+ input = input.replace(/(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g, (whole, beforeContext, beforeTag, afterContext, afterTag) => {
+ // console.error(beforeContext, beforeTag, afterContext, afterTag)
+ if (typeof beforeTag !== "string" && typeof afterTag !== "string") {
+ return "
"
+ }
+ beforeContext = beforeContext || ""
+ beforeTag = beforeTag || ""
+ afterContext = afterContext || ""
+ afterTag = afterTag || ""
+ if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
+ return beforeContext + "
" + afterContext
+ } else {
+ return whole
+ }
+ })
+
+ // Note: Element's renderers on Web and Android currently collapse whitespace, like the browser does. Turndown also collapses whitespace which is good for me.
+ // If later I'm using a client that doesn't collapse whitespace and I want turndown to follow suit, uncomment the following line of code, and it Just Works:
+ // input = input.replace(/ /g, " ")
+ // There is also a corresponding test to uncomment, named "event2message: whitespace is retained"
+
+ // @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
+ content = content.replace(/ \n/g, "\n")
+ } else {
+ // Looks like we're using the plaintext body!
+ content = event.content.body
+
+ if (event.content.msgtype === "m.emote") {
+ content = `* ${displayName} ${content}`
}
- })
- // @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
- content = content.replace(/ \n/g, "\n")
- } else {
- // Looks like we're using the plaintext body!
- // Markdown needs to be escaped
- content = content.replace(/([*_~`#])/g, `\\$1`)
+ // Markdown needs to be escaped
+ content = content.replace(/([*_~`#])/g, `\\$1`)
+ }
}
content = replyLine + content
@@ -266,8 +285,10 @@ async function eventToMessage(event, guild, di) {
const messagesToEdit = []
const messagesToSend = []
for (let i = 0; i < messages.length; i++) {
- if (messageIDsToEdit.length) {
- messagesToEdit.push({id: messageIDsToEdit.shift(), message: messages[i]})
+ const next = messageIDsToEdit[0]
+ if (next) {
+ messagesToEdit.push({id: next, message: messages[i]})
+ messageIDsToEdit.shift()
} else {
messagesToSend.push(messages[i])
}
diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js
index 8e494bb..d071f8d 100644
--- a/m2d/converters/event-to-message.test.js
+++ b/m2d/converters/event-to-message.test.js
@@ -584,7 +584,7 @@ test("event2message: editing a plaintext body message", async t => {
"room_id": "!PnyBKvUBOhjuCucEfk:cadence.moe"
}, data.guild.general, {
api: {
- getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
+ getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
@@ -609,6 +609,63 @@ test("event2message: editing a plaintext body message", async t => {
)
})
+test("event2message: editing a formatted body message", async t => {
+ t.deepEqual(
+ await eventToMessage({
+ "type": "m.room.message",
+ "sender": "@cadence:cadence.moe",
+ "content": {
+ "msgtype": "m.text",
+ "body": " * **well, I guess it's no longer brand new... it's existed for mere seconds...**",
+ "format": "org.matrix.custom.html",
+ "formatted_body": "* well, I guess it's no longer brand new... it's existed for mere seconds...",
+ "m.new_content": {
+ "msgtype": "m.text",
+ "body": "**well, I guess it's no longer brand new... it's existed for mere seconds...**",
+ "format": "org.matrix.custom.html",
+ "formatted_body": "well, I guess it's no longer brand new... it's existed for mere seconds..."
+ },
+ "m.relates_to": {
+ "rel_type": "m.replace",
+ "event_id": "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs"
+ }
+ },
+ "origin_server_ts": 1693223873912,
+ "unsigned": {
+ "age": 42,
+ "transaction_id": "m1693223873796.842"
+ },
+ "event_id": "$KxGwvVNzNcmlVbiI2m5kX-jMFNi3Jle71-uu1j7P7vM",
+ "room_id": "!PnyBKvUBOhjuCucEfk:cadence.moe"
+ }, data.guild.general, {
+ api: {
+ getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ msgtype: "m.text",
+ body: "**brand new, never before seen message**",
+ format: "org.matrix.custom.html",
+ formatted_body: "brand new, never before seen message"
+ }
+ })
+ }
+ }),
+ {
+ messagesToDelete: [],
+ messagesToEdit: [{
+ id: "1145688633186193479",
+ message: {
+ username: "cadence [they]",
+ content: "**well, I guess it's no longer brand new... it's existed for mere seconds...**",
+ avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU"
+ }
+ }],
+ messagesToSend: []
+ }
+ )
+})
+
test("event2message: rich reply to a matrix user's long message with formatting", async t => {
t.deepEqual(
await eventToMessage({