Compare commits

..

2 commits

3 changed files with 67 additions and 4 deletions

View file

@ -364,7 +364,7 @@ function defineEchoHandler() {
// set profile data on homeserver... // set profile data on homeserver...
console.log("⏩ Updating Matrix profile... (If you've joined lots of rooms, this is slow. Please allow at least 30 seconds.)") console.log("⏩ Updating Matrix profile... (If you've joined lots of rooms, this is slow. Please allow at least 30 seconds.)")
await api.profileSetDisplayname(mxid, "Out Of Your Element") await api.profileSetDisplayname(mxid, "Out Of Your Element (bot)")
await api.profileSetAvatarUrl(mxid, avatarUrl) await api.profileSetAvatarUrl(mxid, avatarUrl)
console.log("✅ Matrix profile updated...") console.log("✅ Matrix profile updated...")

View file

@ -773,14 +773,14 @@ async function eventToMessage(event, guild, channel, di) {
repliedToEvent.content = repliedToEvent.content["m.new_content"] repliedToEvent.content = repliedToEvent.content["m.new_content"]
} }
/** @type {string} */ /** @type {string} */
let repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body let originalRepliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body
const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype) const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype)
let contentPreview let contentPreview
if (fileReplyContentAlternative) { if (fileReplyContentAlternative) {
contentPreview = " " + fileReplyContentAlternative contentPreview = " " + fileReplyContentAlternative
} else if (repliedToEvent.unsigned?.redacted_because) { } else if (repliedToEvent.unsigned?.redacted_because) {
contentPreview = " (in reply to a deleted message)" contentPreview = " (in reply to a deleted message)"
} else if (typeof repliedToContent !== "string") { } else if (typeof originalRepliedToContent !== "string") {
// in reply to a weird metadata event like m.room.name, m.room.member... // in reply to a weird metadata event like m.room.name, m.room.member...
// I'm not implementing text fallbacks for arbitrary room events. this should cover most cases // I'm not implementing text fallbacks for arbitrary room events. this should cover most cases
// this has never ever happened in the wild anyway // this has never ever happened in the wild anyway
@ -788,6 +788,7 @@ async function eventToMessage(event, guild, channel, di) {
contentPreview = " (channel details edited)" contentPreview = " (channel details edited)"
} else { } else {
// Generate a reply preview for a standard message // Generate a reply preview for a standard message
let repliedToContent = originalRepliedToContent
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
repliedToContent = repliedToContent.replace(/(?:\n|<br ?\/?>)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/(?:\n|<br ?\/?>)+/g, " ") // Should all be on one line
@ -802,7 +803,18 @@ async function eventToMessage(event, guild, channel, di) {
const contentPreviewChunks = chunk(repliedToContent, 50) const contentPreviewChunks = chunk(repliedToContent, 50)
if (contentPreviewChunks.length) { if (contentPreviewChunks.length) {
contentPreview = ": " + contentPreviewChunks[0] contentPreview = ": " + contentPreviewChunks[0]
contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, "<$&>") contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, url => {
const originalUrlIndex = originalRepliedToContent.indexOf(url)
if (originalUrlIndex !== -1 && originalRepliedToContent[originalUrlIndex + url.length]?.match(/[^ )>"'`]/)) { // URL was truncated by chunking, replace it
try {
const u = new URL(url)
return `<via ${u.hostname}>`
} catch (e) {
return `<url>`
}
}
return `<${url}>` // Full URL is present, escape it
})
if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..."
} else { } else {
contentPreview = "" contentPreview = ""

View file

@ -2117,6 +2117,57 @@ test("event2message: should suppress embeds for links in reply preview", async t
) )
}) })
test("event2message: truncated links in reply preview should be unlinked to prevent broken links", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: `> <@_ooye_.wing.:cadence.moe> https://huggingface.co/blog/security-incident-july-2026\n\nthis is either written by claude or somebody who's too claude-brained and I just can't get through it`,
format: "org.matrix.custom.html",
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@_ooye_.wing.:cadence.moe">@_ooye_.wing.:cadence.moe</a><br>https://huggingface.co/blog/security-incident-july-2026</blockquote></mx-reply>this is either written by claude or somebody who's too claude-brained and I just can't get through it`,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
}
}
},
event_id: "$0Bs3rbsXaeZmSztGMx1NIsqvOrkXOpIWebN-dqr09i4",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
}, data.guild.general, data.channel.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU", {
"type": "m.room.message",
"sender": "@_ooye_.wing.:cadence.moe",
"content": {
"m.mentions": {},
"msgtype": "m.text",
"body": "<https://huggingface.co/blog/security-incident-july-2026>",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://huggingface.co/blog/security-incident-july-2026\">https://huggingface.co/blog/security-incident-july-2026</a>"
}
})
}
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 <@112890272819507200>:"
+ " <via huggingface.co>..."
+ `\nthis is either written by claude or somebody who's too claude-brained and I just can't get through it`,
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar",
allowed_mentions: {
parse: ["users", "roles"]
}
}]
}
)
})
test("event2message: should include a reply preview when message ends with a blockquote", async t => { test("event2message: should include a reply preview when message ends with a blockquote", async t => {
t.deepEqual( t.deepEqual(
await eventToMessage({ await eventToMessage({