diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index 7972f13..75a4ffb 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -34,7 +34,68 @@ test("message2event embeds: nothing but a field", async t => { t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, - msgtype: "m.text", - body: "Amanda" + msgtype: "m.notice", + body: "**Amanda 🎵#2192 :online:" + + "\nwillow tree, branch 0**" + + "\n**❯ Uptime:**\n3m 55s\n**❯ Memory:**\n64.45MB", + format: "org.matrix.custom.html", + formatted_body: 'Amanda 🎵#2192 \":online:\"' + + '
willow tree, branch 0
' + + '
❯ Uptime:
3m 55s' + + '
❯ Memory:
64.45MB' + }]) +}) + +test("message2event embeds: reply with just an embed", async t => { + const events = await messageToEvent(data.message_with_embeds.reply_with_only_embed, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + "m.mentions": {}, + body: "[**⏺️ dynastic (@dynastic)**](https://twitter.com/i/user/719631291747078145)" + + "\n\n**https://twitter.com/i/status/1707484191963648161**" + + "\n\ndoes anyone know where to find that one video of the really mysterious yam-like object being held up to a bunch of random objects, like clocks, and they have unexplained impossible reactions to it?" + + "\n\n**Retweets**" + + "\n119" + + "\n\n**Likes**" + + "\n5581" + + "\n\n— Twitter", + format: "org.matrix.custom.html", + formatted_body: '⏺️ dynastic (@dynastic)' + + '

https://twitter.com/i/status/1707484191963648161' + + '

does anyone know where to find that one video of the really mysterious yam-like object being held up to a bunch of random objects, like clocks, and they have unexplained impossible reactions to it?' + + '

Retweets
119

Likes
5581

— Twitter' + }]) +}) + +test("message2event embeds: image embed and attachment", async t => { + const events = await messageToEvent(data.message_with_embeds.image_embed_and_attachment, data.guild.general, {}, { + api: { + async getJoinedMembers(roomID) { + return {joined: []} + } + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://tootsuite.net/Warp-Gate2.gif\ntanget: @ monster spawner", + format: "org.matrix.custom.html", + formatted_body: 'https://tootsuite.net/Warp-Gate2.gif
tanget: @ monster spawner', + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.image", + url: "mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR", + body: "Screenshot_20231001_034036.jpg", + external_url: "https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&", + filename: "Screenshot_20231001_034036.jpg", + info: { + h: 1170, + w: 1080, + size: 51981, + mimetype: "image/jpeg" + }, + "m.mentions": {} }]) }) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 69d1619..8850f45 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -136,16 +136,7 @@ async function messageToEvent(message, guild, options = {}, di) { addMention(repliedToEventSenderMxid) } - let msgtype = "m.text" - // Handle message type 4, channel name changed - if (message.type === DiscordTypes.MessageType.ChannelNameChange) { - msgtype = "m.emote" - message.content = "changed the channel name to **" + message.content + "**" - } - - // Text content appears first - if (message.content) { - let content = message.content + async function addTextEvent(content, msgtype, {scanMentions}) { content = content.replace(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)/, (whole, guildID, channelID, messageID) => { const eventID = select("event_message", "event_id", "WHERE message_id = ?").pluck().get(messageID) const roomID = select("channel_room", "room_id", "WHERE channel_id = ?").pluck().get(channelID) @@ -186,44 +177,21 @@ async function messageToEvent(message, guild, options = {}, di) { escapeHTML: false, }, null, null) - for (const embed of message.embeds || []) { - // Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once - let repParagraphs = [] - if (embed.author?.name) repParagraphs.push(`**${embed.author.name}**`) - if (embed.title && embed.url) repParagraphs.push(`[**${embed.title}**](${embed.url})`) - else if (embed.title) repParagraphs.push(`**${embed.title}**`) - else if (embed.url) repParagraphs.push(`**${embed.url}**`) - if (embed.description) repParagraphs.push(embed.description) - for (const field of embed.fields || []) { - repParagraphs.push(`**${field.name}**\n${field.value}`) - } - if (embed.footer?.text) repParagraphs.push(embed.footer.text) - const repContent = repParagraphs.join("\n\n") - - html += "
" + markdown.toHTML(repContent, { - discordCallback: getDiscordParseCallbacks(message, true) - }, null, null) + "
" - - body += "\n\n" + markdown.toHTML(repContent, { - discordCallback: getDiscordParseCallbacks(message, false), - discordOnly: true, - escapeHTML: false - }, null, null) - } - // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. - const matches = [...content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] - if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { - const writtenMentionsText = matches.map(m => m[1].toLowerCase()) - const roomID = select("channel_room", "room_id", "WHERE channel_id = ?").pluck().get(message.channel_id) - assert(roomID) - const {joined} = await di.api.getJoinedMembers(roomID) - for (const [mxid, member] of Object.entries(joined)) { - if (!userRegex.some(rx => mxid.match(rx))) { - const localpart = mxid.match(/@([^:]*)/) - assert(localpart) - const displayName = member.displayname || localpart[1] - if (writtenMentionsText.includes(localpart[1].toLowerCase()) || writtenMentionsText.includes(displayName.toLowerCase())) addMention(mxid) + if (scanMentions) { + const matches = [...content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] + if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { + const writtenMentionsText = matches.map(m => m[1].toLowerCase()) + const roomID = select("channel_room", "room_id", "WHERE channel_id = ?").pluck().get(message.channel_id) + assert(roomID) + const {joined} = await di.api.getJoinedMembers(roomID) + for (const [mxid, member] of Object.entries(joined)) { + if (!userRegex.some(rx => mxid.match(rx))) { + const localpart = mxid.match(/@([^:]*)/) + assert(localpart) + const displayName = member.displayname || localpart[1] + if (writtenMentionsText.includes(localpart[1].toLowerCase()) || writtenMentionsText.includes(displayName.toLowerCase())) addMention(mxid) + } } } } @@ -286,6 +254,19 @@ async function messageToEvent(message, guild, options = {}, di) { events.push(newTextMessageEvent) } + + let msgtype = "m.text" + // Handle message type 4, channel name changed + if (message.type === DiscordTypes.MessageType.ChannelNameChange) { + msgtype = "m.emote" + message.content = "changed the channel name to **" + message.content + "**" + } + + // Text content appears first + if (message.content) { + await addTextEvent(message.content, msgtype, {scanMentions: true}) + } + // Then attachments const attachmentEvents = await Promise.all(message.attachments.map(async attachment => { const emoji = @@ -381,6 +362,39 @@ async function messageToEvent(message, guild, options = {}, di) { })) events.push(...attachmentEvents) + // Then embeds + for (const embed of message.embeds || []) { + if (embed.type === "image") { + continue // Matrix already does a fine enough job of providing image embeds. + } + + // Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once + let repParagraphs = [] + const makeUrlTitle = (text, url) => + ( text && url ? `[**${text}**](${url})` + : text ? `**${text}**` + : url ? `**${url}**` + : "") + + let authorNameText = embed.author?.name || "" + if (authorNameText && embed.author?.icon_url) authorNameText = `⏺️ ${authorNameText}` // not using the real image + let authorTitle = makeUrlTitle(authorNameText, embed.author?.url) + if (authorTitle) repParagraphs.push(authorTitle) + + let title = makeUrlTitle(embed.title, embed.url) + if (title) repParagraphs.push(title) + + if (embed.description) repParagraphs.push(embed.description) + for (const field of embed.fields || []) { + repParagraphs.push(`**${field.name}**\n${field.value}`) + } + if (embed.footer?.text) repParagraphs.push(`— ${embed.footer.text}`) + const repContent = repParagraphs.join("\n\n") + + // Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person + await addTextEvent(repContent, "m.notice", {scanMentions: false}) + } + // Then stickers if (message.sticker_items) { const stickerEvents = await Promise.all(message.sticker_items.map(async stickerItem => { diff --git a/test/data.js b/test/data.js index f82b4ae..bb43c02 100644 --- a/test/data.js +++ b/test/data.js @@ -1167,7 +1167,7 @@ module.exports = { message_reference: { message_id: "1157413453921787924", guild_id: "1150201337112449045", - channel_id: "1150208267285434429" + channel_id: "1100319550446252084" }, mentions: [ { @@ -1212,7 +1212,7 @@ module.exports = { edited_timestamp: null, content: "https://twitter.com/dynastic/status/1707484191963648161", components: [], - channel_id: "1150208267285434429", + channel_id: "1100319550446252084", author: { username: "pokemongod", public_flags: 0, @@ -1228,7 +1228,7 @@ module.exports = { message_reference: { message_id: "1157417694728044624", guild_id: "1150201337112449045", - channel_id: "1150208267285434429" + channel_id: "1100319550446252084" }, mentions: [], mention_roles: [], @@ -1274,7 +1274,7 @@ module.exports = { edited_timestamp: null, content: "", components: [], - channel_id: "1150208267285434429", + channel_id: "1100319550446252084", author: { username: "Twitter Video Embeds", public_flags: 65536, @@ -1287,6 +1287,58 @@ module.exports = { }, attachments: [], guild_id: "1150201337112449045" + }, + image_embed_and_attachment: { + id: "1157854642810654821", + type: 0, + content: "https://tootsuite.net/Warp-Gate2.gif\ntanget: @ monster spawner", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [ + { + id: "1157854643037163610", + filename: "Screenshot_20231001_034036.jpg", + size: 51981, + url: "https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&", + proxy_url: "https://media.discordapp.net/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&", + width: 1080, + height: 1170, + content_type: "image/jpeg" + } + ], + embeds: [ + { + type: "image", + url: "https://tootsuite.net/Warp-Gate2.gif", + thumbnail: { + url: "https://tootsuite.net/Warp-Gate2.gif", + proxy_url: "https://images-ext-1.discordapp.net/external/Sy1ETGflxjW3iklbLgxP-Me2BXD7pMsAX2XrJ7ttaS4/https/tootsuite.net/Warp-Gate2.gif", + width: 258, + height: 213 + } + } + ], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-10-01T01:40:58.745000+00:00", + edited_timestamp: "2023-10-01T01:42:05.631000+00:00", + flags: 0, + components: [] } }, message_update: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 4724e30..953ec5c 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -68,14 +68,16 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'), ('https://cdn.discordapp.com/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024', 'mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL'), ('https://cdn.discordapp.com/emojis/230201364309868544.png', 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), -('https://cdn.discordapp.com/emojis/393635038903926784.gif', 'mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc'); +('https://cdn.discordapp.com/emojis/393635038903926784.gif', 'mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc'), +('https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg', 'mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR'); INSERT INTO emoji (id, name, animated, mxc_url) VALUES ('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), ('393635038903926784', 'hipposcope', 1, 'mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc'), ('362741439211503616', 'bn_re', 0, 'mxc://cadence.moe/OIpqpfxTnHKokcsYqDusxkBT'), ('551636841284108289', 'ae_botrac4r', 0, 'mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs'), -('975572106295259148', 'brillillillilliant_move', 0, 'mxc://cadence.moe/scfRIDOGKWFDEBjVXocWYQHik'); +('975572106295259148', 'brillillillilliant_move', 0, 'mxc://cadence.moe/scfRIDOGKWFDEBjVXocWYQHik'), +('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'); INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES ('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),