From ab396bd581c2b6e2f372561df3e5a9bc047af33d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Jun 2025 21:52:28 +1200 Subject: [PATCH] Generate embeds for invites with events --- package-lock.json | 6 +- src/d2m/actions/send-message.js | 2 +- src/d2m/converters/message-to-event.js | 45 +++- src/d2m/converters/message-to-event.test.js | 128 ++++++++++ test/data.js | 245 ++++++++++++++++++++ 5 files changed, 421 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbb9d93..d6b690b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2950,9 +2950,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 15d749c..b01235a 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -42,7 +42,7 @@ async function sendMessage(message, channel, guild, row) { } } - const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) + const events = await messageToEvent.messageToEvent(message, guild, {}, {api, snow: discord.snow}) const eventIDs = [] if (events.length) { db.prepare("INSERT OR IGNORE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 88448b1..cdceeca 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -204,7 +204,7 @@ async function attachmentToEvent(mentions, attachment) { * - includeEditFallbackStar: false * - alwaysReturnFormattedBody: false - formatted_body will be skipped if it is the same as body because the message is plaintext. if you want the formatted_body to be returned anyway, for example to merge it with another message, then set this to true. * - scanTextForMentions: true - needs to be set to false when converting forwarded messages etc which may be from a different channel that can't be scanned. - * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API + * @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer}} di simple-as-nails dependency injection for the matrix API */ async function messageToEvent(message, guild, options = {}, di) { const events = [] @@ -608,6 +608,49 @@ async function messageToEvent(message, guild, options = {}, di) { await addTextEvent(body, html, msgtype) } + // Then scheduled events + if (message.content && di?.snow) { + for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old + const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) + const event = invite.guild_scheduled_event + if (!event) continue // the event ID provided was not valid + + const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric"}) // 9 June at 3:00 pm NZT + const rep = new mxUtils.MatrixStringBuilder() + + // Add time + if (event.scheduled_end_time) { + // @ts-ignore - no definition available for formatRange + rep.addParagraph(`Scheduled Event - ${formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time))}`) + } else { + rep.addParagraph(`Scheduled Event - ${formatter.format(new Date(event.scheduled_start_time))}`) + } + + // Add details + rep.addLine(`## ${event.name}`, tag`${event.name}`) + if (event.description) rep.addLine(event.description) + + // Add location + if (event.entity_metadata?.location) { + rep.addParagraph(`📍 ${event.entity_metadata.location}`) + } else if (invite.channel?.name) { + const roomID = select("channel_room", "room_id", {channel_id: invite.channel.id}).pluck().get() + if (roomID) { + const via = await getViaServersMemo(roomID) + rep.addParagraph(`🔊 ${invite.channel.name} - https://matrix.to/#/${roomID}?${via}`, tag`🔊 ${invite.channel.name} - ${invite.channel.name}`) + } else { + rep.addParagraph(`🔊 ${invite.channel.name}`) + } + } + + // Send like an embed + let {body, formatted_body: html} = rep.get() + body = body.split("\n").map(l => "| " + l).join("\n") + html = `
${html}
` + await addTextEvent(body, html, "m.notice") + } + } + // Then attachments if (message.attachments) { const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions))) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 3e7bcd4..cd7c3fe 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1165,3 +1165,131 @@ test("message2event: don't scan forwarded messages for mentions", async t => { } ]) }) + +test("message2event: invite no details embed if no event", async t => { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + snow: { + invite: { + getInvite: async () => ({...data.invite.irl, guild_scheduled_event: null}) + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "https://discord.gg/placeholder?event=1381190945646710824", + format: "org.matrix.custom.html", + formatted_body: "https://discord.gg/placeholder?event=1381190945646710824", + "m.mentions": {}, + msgtype: "m.text", + } + ]) +}) + +test("message2event: irl invite event renders embed", async t => { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + snow: { + invite: { + getInvite: async () => data.invite.irl + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "https://discord.gg/placeholder?event=1381190945646710824", + format: "org.matrix.custom.html", + formatted_body: "https://discord.gg/placeholder?event=1381190945646710824", + "m.mentions": {}, + msgtype: "m.text", + }, + { + $type: "m.room.message", + msgtype: "m.notice", + body: `| Scheduled Event - 8 June at 10:00 pm NZT – 9 June at 12:00 am NZT` + + `\n| ## forest exploration` + + `\n| ` + + `\n| 📍 the dark forest`, + format: "org.matrix.custom.html", + formatted_body: `

Scheduled Event - 8 June at 10:00 pm NZT – 9 June at 12:00 am NZT

` + + `forest exploration` + + `

📍 the dark forest

`, + "m.mentions": {} + } + ]) +}) + +test("message2event: vc invite event renders embed", async t => { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { + snow: { + invite: { + getInvite: async () => data.invite.vc + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "https://discord.gg/placeholder?event=1381174024801095751", + format: "org.matrix.custom.html", + formatted_body: "https://discord.gg/placeholder?event=1381174024801095751", + "m.mentions": {}, + msgtype: "m.text", + }, + { + $type: "m.room.message", + msgtype: "m.notice", + body: `| Scheduled Event - 9 June at 3:00 pm NZT` + + `\n| ## Cooking (Netrunners)` + + `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.` + + `\n| ` + + `\n| 🔊 Cooking`, + format: "org.matrix.custom.html", + formatted_body: `

Scheduled Event - 9 June at 3:00 pm NZT

` + + `Cooking (Netrunners)
Short circuited brain interfaces actually just means your brain is medium rare, yum.` + + `

🔊 Cooking

`, + "m.mentions": {} + } + ]) +}) + +test("message2event: vc invite event renders embed with room link", async t => { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { + api: { + getJoinedMembers: async () => ({ + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + } + }) + }, + snow: { + invite: { + getInvite: async () => data.invite.known_vc + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "https://discord.gg/placeholder?event=1381174024801095751", + format: "org.matrix.custom.html", + formatted_body: "https://discord.gg/placeholder?event=1381174024801095751", + "m.mentions": {}, + msgtype: "m.text", + }, + { + $type: "m.room.message", + msgtype: "m.notice", + body: `| Scheduled Event - 9 June at 3:00 pm NZT` + + `\n| ## Cooking (Netrunners)` + + `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.` + + `\n| ` + + `\n| 🔊 Hey. - https://matrix.to/#/!FuDZhlOAtqswlyxzeR:cadence.moe?via=cadence.moe`, + format: "org.matrix.custom.html", + formatted_body: `

Scheduled Event - 9 June at 3:00 pm NZT

` + + `Cooking (Netrunners)
Short circuited brain interfaces actually just means your brain is medium rare, yum.` + + `

🔊 Hey. - Hey.

`, + "m.mentions": {} + } + ]) +}) diff --git a/test/data.js b/test/data.js index 01e49a4..78fcb75 100644 --- a/test/data.js +++ b/test/data.js @@ -4858,5 +4858,250 @@ module.exports = { application_id: "1109360903096369153", guild_id: "497159726455455754" } + }, + invite: { + irl: { + type: 0, + code: 'placeholder', + inviter: { + id: '772659086046658620', + username: 'cadence.worm', + avatar: '466df0c98b1af1e1388f595b4c1ad1b9', + discriminator: '0', + public_flags: 0, + flags: 0, + banner: null, + accent_color: 4534897, + global_name: 'cadence', + avatar_decoration_data: null, + collectibles: null, + banner_color: '#453271', + clan: null, + primary_guild: null + }, + expires_at: '2025-06-15T08:39:43+00:00', + guild: { + id: '1338114140941586518', + name: 'self service', + splash: null, + banner: null, + description: null, + icon: null, + features: [], + verification_level: 0, + vanity_url_code: null, + nsfw_level: 0, + nsfw: false, + premium_subscription_count: 0, + premium_tier: 0 + }, + guild_id: '1338114140941586518', + channel: { id: '1338114141658939517', type: 0, name: 'general' }, + guild_scheduled_event: { + id: '1381190945646710824', + guild_id: '1338114140941586518', + name: 'forest exploration', + description: '', + channel_id: null, + creator_id: '772659086046658620', + image: null, + scheduled_start_time: '2025-06-08T10:00:00.161000+00:00', + scheduled_end_time: '2025-06-08T12:00:00.161000+00:00', + status: 1, + entity_type: 3, + entity_id: null, + recurrence_rule: null, + user_count: 1, + privacy_level: 2, + sku_ids: [], + user_rsvp: null, + guild_scheduled_event_exceptions: [], + entity_metadata: { location: 'the dark forest' } + }, + profile: { + id: '1338114140941586518', + name: 'self service', + icon_hash: null, + member_count: 2, + online_count: 1, + description: null, + banner_hash: null, + game_application_ids: [], + game_activity: {}, + tag: null, + badge: 0, + badge_color_primary: '#ff0000', + badge_color_secondary: '#800000', + badge_hash: null, + traits: [], + features: [], + visibility: 2, + custom_banner_hash: null, + premium_subscription_count: 0, + premium_tier: 0 + } + }, + vc: { + type: 0, + code: 'placeholder', + inviter: { + id: '1024720274928697384', + username: '1024720274928697384', + avatar: '040a0652f1c76af3b71bb2c58ee0057b', + discriminator: '0', + public_flags: 0, + flags: 0, + banner: null, + accent_color: 4259841, + global_name: 'Regalia, Goddess of OH GOD OH FU', + avatar_decoration_data: null, + collectibles: null, + banner_color: '#410001', + clan: null, + primary_guild: null + }, + expires_at: '2025-06-15T07:32:30+00:00', + guild: { + id: '1340545485542391879', + name: 'VRCooking', + splash: null, + banner: null, + description: null, + icon: '8e1948b83d79c11ccb32b9e54a5d85fd', + features: [ 'SOUNDBOARD', 'ACTIVITY_FEED_DISABLED_BY_USER' ], + verification_level: 0, + vanity_url_code: null, + nsfw_level: 0, + nsfw: false, + premium_subscription_count: 0, + premium_tier: 0 + }, + guild_id: '1340545485542391879', + channel: { id: '1368144987707019306', type: 2, name: 'Cooking' }, + guild_scheduled_event: { + id: '1381174024801095751', + guild_id: '1340545485542391879', + name: 'Cooking (Netrunners)', + description: 'Short circuited brain interfaces actually just means your brain is medium rare, yum.', + channel_id: '1368144987707019306', + creator_id: '1024720274928697384', + image: null, + scheduled_start_time: '2025-06-09T03:00:00+00:00', + scheduled_end_time: null, + status: 1, + entity_type: 2, + entity_id: null, + recurrence_rule: null, + user_count: 2, + privacy_level: 2, + sku_ids: [], + user_rsvp: null, + guild_scheduled_event_exceptions: [], + entity_metadata: {} + }, + profile: { + id: '1340545485542391879', + name: 'VRCooking', + icon_hash: '8e1948b83d79c11ccb32b9e54a5d85fd', + member_count: 18, + online_count: 13, + description: null, + banner_hash: null, + game_application_ids: [], + game_activity: {}, + tag: null, + badge: 0, + badge_color_primary: '#ff0000', + badge_color_secondary: '#800000', + badge_hash: null, + traits: [], + features: [], + visibility: 2, + custom_banner_hash: null, + premium_subscription_count: 0, + premium_tier: 0 + } + }, + known_vc: { + type: 0, + code: 'placeholder', + inviter: { + id: '1024720274928697384', + username: '1024720274928697384', + avatar: '040a0652f1c76af3b71bb2c58ee0057b', + discriminator: '0', + public_flags: 0, + flags: 0, + banner: null, + accent_color: 4259841, + global_name: 'Regalia, Goddess of OH GOD OH FU', + avatar_decoration_data: null, + collectibles: null, + banner_color: '#410001', + clan: null, + primary_guild: null + }, + expires_at: '2025-06-15T07:32:30+00:00', + guild: { + id: '112760669178241024', + name: 'Psychonauts 3', + splash: null, + banner: null, + description: null, + icon: '8e1948b83d79c11ccb32b9e54a5d85fd', + features: [ 'SOUNDBOARD', 'ACTIVITY_FEED_DISABLED_BY_USER' ], + verification_level: 0, + vanity_url_code: null, + nsfw_level: 0, + nsfw: false, + premium_subscription_count: 0, + premium_tier: 0 + }, + guild_id: '112760669178241024', + channel: { id: '1162005314908999790', type: 0, name: 'Hey.' }, + guild_scheduled_event: { + id: '1381174024801095751', + guild_id: '112760669178241024', + name: 'Cooking (Netrunners)', + description: 'Short circuited brain interfaces actually just means your brain is medium rare, yum.', + channel_id: '1162005314908999790', + creator_id: '1024720274928697384', + image: null, + scheduled_start_time: '2025-06-09T03:00:00+00:00', + scheduled_end_time: null, + status: 1, + entity_type: 2, + entity_id: null, + recurrence_rule: null, + user_count: 2, + privacy_level: 2, + sku_ids: [], + user_rsvp: null, + guild_scheduled_event_exceptions: [], + entity_metadata: {} + }, + profile: { + id: '112760669178241024', + name: 'Psychonauts 3', + icon_hash: '8e1948b83d79c11ccb32b9e54a5d85fd', + member_count: 18, + online_count: 13, + description: null, + banner_hash: null, + game_application_ids: [], + game_activity: {}, + tag: null, + badge: 0, + badge_color_primary: '#ff0000', + badge_color_secondary: '#800000', + badge_hash: null, + traits: [], + features: [], + visibility: 2, + custom_banner_hash: null, + premium_subscription_count: 0, + premium_tier: 0 + } + } } }