diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index cb1fb5a..842c24e 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -78,7 +78,7 @@ test("edit2changes: bot response", async t => { newContent: { $type: "m.room.message", msgtype: "m.text", - body: "* :ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", format: "org.matrix.custom.html", formatted_body: '* :ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', "m.mentions": { @@ -87,7 +87,7 @@ test("edit2changes: bot response", async t => { // *** Replaced With: *** "m.new_content": { msgtype: "m.text", - body: ":ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + body: ":ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", format: "org.matrix.custom.html", formatted_body: ':ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', "m.mentions": { diff --git a/src/d2m/converters/find-mentions.js b/src/d2m/converters/find-mentions.js index 8726830..8107459 100644 --- a/src/d2m/converters/find-mentions.js +++ b/src/d2m/converters/find-mentions.js @@ -146,10 +146,18 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) { // Highlight the relevant part of the message const start = baseOffset + best.scored.matchedInputTokens[0].index const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end - const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end) + const newNodes = [{ + type: "text", content: content.slice(0, start) + }, { + type: "link", target: `https://matrix.to/#/${best.mxid}`, content: [ + {type: "text", content: content.slice(start, end)} + ] + }, { + type: "text", content: content.slice(end) + }] return { mxid: best.mxid, - newContent + newNodes } } } diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 81c821f..05a376a 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -519,29 +519,60 @@ async function messageToEvent(message, guild, options = {}, di) { return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed })) - async function transformParsedVia(parsed) { - for (const node of parsed) { + async function transformParsedVia(parsed, scanTextForMentions) { + for (let n = 0; n < parsed.length; n++) { + const node = parsed[n] if (node.type === "discordChannel" || node.type === "discordChannelLink") { node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get() if (node.row?.room_id) { node.via = await getViaServersMemo(node.row.room_id) } } + else if (node.type === "text" && typeof node.content === "string") { + // Merge adjacent text nodes into this one + while (parsed[n+1]?.type === "text" && typeof parsed[n+1].content === "string") { + node.content += parsed[n+1].content + parsed.splice(n+1, 1) + } + // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. + if (scanTextForMentions) { + let content = node.content + const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)] + for (let i = matches.length; i--;) { + const m = matches[i] + const prefix = m[1] + const maximumWrittenSection = m[2].toLowerCase() + if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it + if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here + + var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() + assert(roomID) + var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name}))) + + const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content) + if (found) { + addMention(found.mxid) + parsed.splice(n, 1, ...found.newNodes) + content = found.newNodes[0].content + } + } + } + } for (const maybeChildNodesArray of [node, node.content, node.items]) { if (Array.isArray(maybeChildNodesArray)) { - await transformParsedVia(maybeChildNodesArray) + await transformParsedVia(maybeChildNodesArray, scanTextForMentions && ["blockQuote", "list", "paragraph", "em", "strong", "u", "del", "text"].includes(node.type)) } } } return parsed } - let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, { + let html = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, customOptions.isTheMessageContent && options.scanTextForMentions !== false), { discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers), ...customOptions }, customParser, customHtmlOutput) - let body = await markdown.toHtmlWithPostParser(content, transformParsedVia, { + let body = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, false), { // not scanning plaintext body for mentions as we don't parse whether they're in code discordCallback: getDiscordParseCallbacks(message, guild, false), discordOnly: true, escapeHTML: false, @@ -735,35 +766,12 @@ async function messageToEvent(message, guild, options = {}, di) { // Then text content if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) { - // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. - let content = message.content - if (options.scanTextForMentions !== false) { - const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)] - for (let i = matches.length; i--;) { - const m = matches[i] - const prefix = m[1] - const maximumWrittenSection = m[2].toLowerCase() - if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it - if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here - - var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() - assert(roomID) - var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name}))) - - const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content) - if (found) { - addMention(found.mxid) - content = found.newContent - } - } - } - // Scan the content for emojihax and replace them with real emojis - content = content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { + let content = message.content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { return `<:${name}:${id}>` }) - const {body, html} = await transformContent(content) + const {body, html} = await transformContent(content, {isTheMessageContent: true}) await addTextEvent(body, html, msgtype) } diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index f071417..4728c2b 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -789,7 +789,7 @@ test("message2event: simple written @mention for matrix user", async t => { ] }, msgtype: "m.text", - body: "[@ash](https://matrix.to/#/@she_who_brings_destruction:cadence.moe) do you need anything from the store btw as I'm heading there after gym", + body: "@ash do you need anything from the store btw as I'm heading there after gym", format: "org.matrix.custom.html", formatted_body: `@ash do you need anything from the store btw as I'm heading there after gym` }]) @@ -838,7 +838,7 @@ test("message2event: many written @mentions for matrix users", async t => { ] }, msgtype: "m.text", - body: "[@Cadence](https://matrix.to/#/@cadence:cadence.moe), tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and [@huck](https://matrix.to/#/@huckleton:cadence.moe)", + body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck", format: "org.matrix.custom.html", formatted_body: `@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck` }]) @@ -890,7 +890,7 @@ test("message2event: written @mentions may match part of the name", async t => { ] }, msgtype: "m.text", - body: "I wonder if [@cadence](https://matrix.to/#/@secret:cadence.moe) saw this?", + body: "I wonder if @cadence saw this?", format: "org.matrix.custom.html", formatted_body: `I wonder if @cadence saw this?` }]) @@ -941,7 +941,7 @@ test("message2event: written @mentions may match part of the mxid", async t => { ] }, msgtype: "m.text", - body: "I wonder if [@huck](https://matrix.to/#/@huckleton:cadence.moe) saw this?", + body: "I wonder if @huck saw this?", format: "org.matrix.custom.html", formatted_body: `I wonder if @huck saw this?` }]) @@ -962,6 +962,21 @@ test("message2event: written @mentions do not match in URLs", async t => { }]) }) +test("message2event: written @mentions do not match in inline code", async t => { + const events = await messageToEvent({ + ...data.message.advanced_written_at_mention_for_matrix, + content: "`public @Nullable EntityType`" + }, data.guild.general, {}, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "`public @Nullable EntityType`", + format: "org.matrix.custom.html", + formatted_body: `public @Nullable EntityType<?>` + }]) +}) + test("message2event: entire message may match elaborate display name", async t => { let called = 0 const events = await messageToEvent({ @@ -1007,7 +1022,7 @@ test("message2event: entire message may match elaborate display name", async t = ] }, msgtype: "m.text", - body: "[@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆](https://matrix.to/#/@wa:cadence.moe)", + body: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆", format: "org.matrix.custom.html", formatted_body: `@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆` }])