finalise message editing
This commit is contained in:
parent
53b5438756
commit
56fe710392
4 changed files with 157 additions and 63 deletions
|
@ -58,6 +58,20 @@ async function sendMessageWithWebhook(channelID, data, threadID) {
|
||||||
return result
|
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.ensureWebhook = ensureWebhook
|
||||||
module.exports.withWebhook = withWebhook
|
module.exports.withWebhook = withWebhook
|
||||||
module.exports.sendMessageWithWebhook = sendMessageWithWebhook
|
module.exports.sendMessageWithWebhook = sendMessageWithWebhook
|
||||||
|
module.exports.editMessageWithWebhook = editMessageWithWebhook
|
||||||
|
|
|
@ -34,9 +34,11 @@ async function sendEvent(event) {
|
||||||
/** @type {DiscordTypes.APIMessage[]} */
|
/** @type {DiscordTypes.APIMessage[]} */
|
||||||
const messageResponses = []
|
const messageResponses = []
|
||||||
let eventPart = 0 // 0 is primary, 1 is supporting
|
let eventPart = 0 // 0 is primary, 1 is supporting
|
||||||
// for (const message of messagesToEdit) {
|
for (const data of messagesToEdit) {
|
||||||
// eventPart = 1
|
const messageResponse = await channelWebhook.editMessageWithWebhook(channelID, data.id, data.message, threadID)
|
||||||
// TODO ...
|
eventPart = 1
|
||||||
|
messageResponses.push(messageResponse)
|
||||||
|
}
|
||||||
for (const message of messagesToSend) {
|
for (const message of messagesToSend) {
|
||||||
const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID)
|
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("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, channelID)
|
||||||
|
|
|
@ -141,19 +141,10 @@ async function eventToMessage(event, guild, di) {
|
||||||
if (member.displayname) displayName = member.displayname
|
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)
|
||||||
|
|
||||||
// Convert content depending on what the message is
|
|
||||||
let content = event.content.body // ultimate fallback
|
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.
|
// 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
|
// this event ---is an edit of--> original event ---is a reply to--> past event
|
||||||
await (async () => {
|
await (async () => {
|
||||||
|
@ -167,16 +158,26 @@ async function eventToMessage(event, guild, di) {
|
||||||
if (!originalEventId) return
|
if (!originalEventId) return
|
||||||
messageIDsToEdit = db.prepare("SELECT message_id FROM event_message WHERE event_id = ? ORDER BY part").pluck().all(originalEventId)
|
messageIDsToEdit = db.prepare("SELECT message_id FROM event_message WHERE event_id = ? ORDER BY part").pluck().all(originalEventId)
|
||||||
if (!messageIDsToEdit.length) return
|
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
|
// Get the original event, then check if it was a reply
|
||||||
const originalEvent = await di.api.getEvent(event.room_id, originalEventId)
|
const originalEvent = await di.api.getEvent(event.room_id, originalEventId)
|
||||||
if (!originalEvent) return
|
if (!originalEvent) return
|
||||||
const repliedToEventId = originalEvent.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
|
const repliedToEventId = originalEvent.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
|
||||||
if (!repliedToEventId) return
|
if (!repliedToEventId) return
|
||||||
|
|
||||||
// After all that, it's an edit of a reply.
|
// 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.
|
// 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, event.content["m.new_content"])
|
Object.assign(event.content, {
|
||||||
input = event.content.formatted_body || event.content.body
|
"m.relates_to": {
|
||||||
relatesTo["m.in_reply_to"] = {event_id: repliedToEventId}
|
"m.in_reply_to": {
|
||||||
|
event_id: repliedToEventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver.
|
// 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`
|
replyLine = `> ${replyLine}\n> ${contentPreview}\n`
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Handling mentions of Discord users
|
if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) {
|
||||||
input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => {
|
let input = event.content.formatted_body
|
||||||
if (!utils.eventSenderIsFromDiscord(mxid)) return whole
|
if (event.content.msgtype === "m.emote") {
|
||||||
const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid)
|
input = `* ${displayName} ${input}`
|
||||||
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 <br> before </blockquote> 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|<br ?\/?>\s*)*<\/blockquote>/g, "</blockquote>")
|
|
||||||
|
|
||||||
// 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 "<br>"
|
|
||||||
}
|
}
|
||||||
beforeContext = beforeContext || ""
|
|
||||||
beforeTag = beforeTag || ""
|
// Handling mentions of Discord users
|
||||||
afterContext = afterContext || ""
|
input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => {
|
||||||
afterTag = afterTag || ""
|
if (!utils.eventSenderIsFromDiscord(mxid)) return whole
|
||||||
if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
|
const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid)
|
||||||
return beforeContext + "<br>" + afterContext
|
if (!userID) return whole
|
||||||
} else {
|
return `${attributeValue} data-user-id="${userID}">`
|
||||||
return whole
|
})
|
||||||
|
|
||||||
|
// 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 <br> before </blockquote> 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|<br ?\/?>\s*)*<\/blockquote>/g, "</blockquote>")
|
||||||
|
|
||||||
|
// 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 "<br>"
|
||||||
|
}
|
||||||
|
beforeContext = beforeContext || ""
|
||||||
|
beforeTag = beforeTag || ""
|
||||||
|
afterContext = afterContext || ""
|
||||||
|
afterTag = afterTag || ""
|
||||||
|
if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
|
||||||
|
return beforeContext + "<br>" + 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
|
// Markdown needs to be escaped
|
||||||
content = turndownService.turndown(input)
|
content = content.replace(/([*_~`#])/g, `\\$1`)
|
||||||
|
}
|
||||||
// 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`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content = replyLine + content
|
content = replyLine + content
|
||||||
|
@ -266,8 +285,10 @@ async function eventToMessage(event, guild, di) {
|
||||||
const messagesToEdit = []
|
const messagesToEdit = []
|
||||||
const messagesToSend = []
|
const messagesToSend = []
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
if (messageIDsToEdit.length) {
|
const next = messageIDsToEdit[0]
|
||||||
messagesToEdit.push({id: messageIDsToEdit.shift(), message: messages[i]})
|
if (next) {
|
||||||
|
messagesToEdit.push({id: next, message: messages[i]})
|
||||||
|
messageIDsToEdit.shift()
|
||||||
} else {
|
} else {
|
||||||
messagesToSend.push(messages[i])
|
messagesToSend.push(messages[i])
|
||||||
}
|
}
|
||||||
|
|
|
@ -584,7 +584,7 @@ test("event2message: editing a plaintext body message", async t => {
|
||||||
"room_id": "!PnyBKvUBOhjuCucEfk:cadence.moe"
|
"room_id": "!PnyBKvUBOhjuCucEfk:cadence.moe"
|
||||||
}, data.guild.general, {
|
}, data.guild.general, {
|
||||||
api: {
|
api: {
|
||||||
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
|
getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
sender: "@cadence:cadence.moe",
|
sender: "@cadence:cadence.moe",
|
||||||
content: {
|
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": "* <strong>well, I guess it's no longer brand new... it's existed for mere seconds...</strong>",
|
||||||
|
"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": "<strong>well, I guess it's no longer brand new... it's existed for mere seconds...</strong>"
|
||||||
|
},
|
||||||
|
"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: "<strong>brand new, never before seen message</strong>"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
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 => {
|
test("event2message: rich reply to a matrix user's long message with formatting", async t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
await eventToMessage({
|
await eventToMessage({
|
||||||
|
|
Loading…
Reference in a new issue