From 1e8066ca0ab9908eb5c5d1509375684e4d276729 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 6 Mar 2024 09:34:46 +1300 Subject: [PATCH 001/346] Embed URL should only appear when embed has title --- .../message-to-event.embeds.test.js | 52 +++- d2m/converters/message-to-event.js | 4 +- test/data.js | 240 ++++++++++++++++++ 3 files changed, 290 insertions(+), 6 deletions(-) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index dfc5679..0ac5018 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -27,7 +27,6 @@ test("message2event embeds: reply with just an embed", async t => { msgtype: "m.notice", "m.mentions": {}, body: "| ## ⏺️ dynastic (@dynastic) https://twitter.com/i/user/719631291747078145" - + "\n| \n| ## https://twitter.com/i/status/1707484191963648161" + "\n| \n| 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?" + "\n| \n| ### Retweets" + "\n| 119" @@ -35,8 +34,7 @@ test("message2event embeds: reply with just an embed", async t => { + "\n| 5581" + "\n| — Twitter", format: "org.matrix.custom.html", - formatted_body: '

⏺️ dynastic (@dynastic)

' - + '

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

⏺️ dynastic (@dynastic)' + '

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
' }]) @@ -141,3 +139,51 @@ test("message2event embeds: crazy html is all escaped", async t => { "m.mentions": {} }]) }) + +test("message2event embeds: title without url", async t => { + const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", + format: "org.matrix.custom.html", + formatted_body: `

Hi, I'm Amanda!

I condone pirating music!

`, + "m.mentions": {} + }]) +}) + +test("message2event embeds: url without title", async t => { + const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| I condone pirating music!", + format: "org.matrix.custom.html", + formatted_body: `

I condone pirating music!

`, + "m.mentions": {} + }]) +}) + +test("message2event embeds: author without url", async t => { + const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| ## Amanda\n| \n| I condone pirating music!", + format: "org.matrix.custom.html", + formatted_body: `

Amanda

I condone pirating music!

`, + "m.mentions": {} + }]) +}) + +test("message2event embeds: author url without name", async t => { + const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| I condone pirating music!", + format: "org.matrix.custom.html", + formatted_body: `

I condone pirating music!

`, + "m.mentions": {} + }]) +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 89edacc..69a4808 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -517,7 +517,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Author and URL into a paragraph let authorNameText = embed.author?.name || "" if (authorNameText && embed.author?.icon_url) authorNameText = `⏺️ ${authorNameText}` // using the emoji instead of an image - if (authorNameText || embed.author?.url) { + if (authorNameText) { if (embed.author?.url) { const authorURL = await transformContentMessageLinks(embed.author.url) rep.addParagraph(`## ${authorNameText} ${authorURL}`, tag`${authorNameText}`) @@ -534,8 +534,6 @@ async function messageToEvent(message, guild, options = {}, di) { } else { rep.addParagraph(`## ${body}`, `${html}`) } - } else if (embed.url) { - rep.addParagraph(`## ${embed.url}`, tag`${embed.url}`) } if (embed.description) { diff --git a/test/data.js b/test/data.js index 2d0f795..49b84e6 100644 --- a/test/data.js +++ b/test/data.js @@ -2282,6 +2282,246 @@ module.exports = { edited_timestamp: null, flags: 0, components: [] + }, + title_without_url: { + guild_id: "497159726455455754", + mentions: [], + id: "1141934888862351440", + type: 20, + content: "", + channel_id: "497161350934560778", + author: { + id: "1109360903096369153", + username: "Amanda 🎵", + avatar: "d56cd1b26e043ae512edae2214962faa", + discriminator: "2192", + public_flags: 524288, + flags: 524288, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "rich", + color: 3092790, + title: "Hi, I'm Amanda!", + description: "I condone pirating music!" + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-18T03:21:33.629000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + application_id: "1109360903096369153", + interaction: { + id: "1141934887608254475", + type: 2, + name: "stats", + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "47a19b0445069b826e136da4df4259bb", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: null, + accent_color: null, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: null + } + }, + webhook_id: "1109360903096369153" + }, + url_without_title: { + guild_id: "497159726455455754", + mentions: [], + id: "1141934888862351440", + type: 20, + content: "", + channel_id: "497161350934560778", + author: { + id: "1109360903096369153", + username: "Amanda 🎵", + avatar: "d56cd1b26e043ae512edae2214962faa", + discriminator: "2192", + public_flags: 524288, + flags: 524288, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "rich", + color: 3092790, + url: "https://amanda.moe", + description: "I condone pirating music!" + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-18T03:21:33.629000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + application_id: "1109360903096369153", + interaction: { + id: "1141934887608254475", + type: 2, + name: "stats", + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "47a19b0445069b826e136da4df4259bb", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: null, + accent_color: null, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: null + } + }, + webhook_id: "1109360903096369153" + }, + author_without_url: { + guild_id: "497159726455455754", + mentions: [], + id: "1141934888862351440", + type: 20, + content: "", + channel_id: "497161350934560778", + author: { + id: "1109360903096369153", + username: "Amanda 🎵", + avatar: "d56cd1b26e043ae512edae2214962faa", + discriminator: "2192", + public_flags: 524288, + flags: 524288, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "rich", + color: 3092790, + author: { + name: "Amanda" + }, + description: "I condone pirating music!" + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-18T03:21:33.629000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + application_id: "1109360903096369153", + interaction: { + id: "1141934887608254475", + type: 2, + name: "stats", + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "47a19b0445069b826e136da4df4259bb", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: null, + accent_color: null, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: null + } + }, + webhook_id: "1109360903096369153" + }, + author_url_without_name: { + guild_id: "497159726455455754", + mentions: [], + id: "1141934888862351440", + type: 20, + content: "", + channel_id: "497161350934560778", + author: { + id: "1109360903096369153", + username: "Amanda 🎵", + avatar: "d56cd1b26e043ae512edae2214962faa", + discriminator: "2192", + public_flags: 524288, + flags: 524288, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "rich", + color: 3092790, + author: { + url: "https://amanda.moe" + }, + description: "I condone pirating music!" + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-18T03:21:33.629000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + application_id: "1109360903096369153", + interaction: { + id: "1141934887608254475", + type: 2, + name: "stats", + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "47a19b0445069b826e136da4df4259bb", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: null, + accent_color: null, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: null + } + }, + webhook_id: "1109360903096369153" } }, message_update: { From e2d0ea41d54395e6d0049a9860e7e8675531fdad Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 6 Mar 2024 11:38:46 +1300 Subject: [PATCH 002/346] Improve video embed formatting --- .../message-to-event.embeds.test.js | 95 ++++++++++ d2m/converters/message-to-event.js | 19 +- test/data.js | 179 ++++++++++++++++++ 3 files changed, 291 insertions(+), 2 deletions(-) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index 0ac5018..fc02a3f 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -187,3 +187,98 @@ test("message2event embeds: author url without name", async t => { "m.mentions": {} }]) }) + +test("message2event embeds: vx image", async t => { + const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://vxtwitter.com/TomorrowCorp/status/1760330671074287875 we got a release date!!!", + format: "org.matrix.custom.html", + formatted_body: 'https://vxtwitter.com/TomorrowCorp/status/1760330671074287875 we got a release date!!!', + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix" + + "\n| " + + "\n| ## Twitter https://twitter.com/tomorrowcorp/status/1760330671074287875" + + "\n| " + + "\n| ## Tomorrow Corporation (@TomorrowCorp) https://vxtwitter.com/TomorrowCorp/status/1760330671074287875" + + "\n| " + + "\n| Mark your calendar with a wet black stain! World of Goo 2 releases on May 23, 2024 on Nintendo Switch, Epic Games Store (Win/Mac), and http://WorldOfGoo2.com (Win/Mac/Linux)." + + "\n| " + + "\n| https://tomorrowcorporation.com/posts/world-of-goo-2-now-with-100-more-release-dates-and-platforms" + + "\n| " + + "\n| 💖 123 🔁 36" + + "\n| " + + "\n| 📸 https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg", + format: "org.matrix.custom.html", + formatted_body: `

vxTwitter / fixvx` + + `

Twitter` + + `

Tomorrow Corporation (@TomorrowCorp)` + + `

Mark your calendar with a wet black stain! World of Goo 2 releases on May 23, 2024 on Nintendo Switch, Epic Games Store (Win/Mac), and http://WorldOfGoo2.com (Win/Mac/Linux).` + + `

https://tomorrowcorporation.com/posts/world-of-goo-2-now-with-100-more-release-dates-and-platforms` + + `

💖 123 🔁 36` + + `

📸 https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg

`, + "m.mentions": {} + }]) +}) + +test("message2event embeds: vx video", async t => { + const events = await messageToEvent(data.message_with_embeds.vx_video, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://vxtwitter.com/McDonalds/status/1759971752254341417", + format: "org.matrix.custom.html", + formatted_body: 'https://vxtwitter.com/McDonalds/status/1759971752254341417', + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix" + + "\n| \n| ## McDonald’s🤝@studiopierrot" + + "\n| \n| 💖 89 🔁 21 https://twitter.com/McDonalds/status/1759971752254341417" + + "\n| \n| ## McDonald's (@McDonalds) https://vxtwitter.com/McDonalds/status/1759971752254341417" + + "\n| \n| 🎞️ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12", + format: "org.matrix.custom.html", + formatted_body: `

vxTwitter / fixvx` + + `

McDonald’s🤝@studiopierrot\n\n💖 89 🔁 21` + + `

McDonald's (@McDonalds)` + + `

🎞️ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12

`, + "m.mentions": {} + }]) +}) + +test("message2event embeds: youtube video", async t => { + const events = await messageToEvent(data.message_with_embeds.youtube_video, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E\n\n\nJutomi I'm gonna make these sounds in your walls tonight", + format: "org.matrix.custom.html", + formatted_body: `https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E


Jutomi I'm gonna make these sounds in your walls tonight`, + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "| via YouTube https://www.youtube.com" + + "\n| \n| ## Happy O Funny https://www.youtube.com/channel/UCEpQ9aEb1NafpvWp5Aoizrg" + + "\n| \n| ## Shoebill stork clattering sounds like machine guun~!! (Japan Matsue... https://www.youtube.com/watch?v=kDMHHw8JqLE" + + "\n| \n| twitter" + + "\n| https://twitter.com/matsuevogelpark" + + "\n| \n| The shoebill (Balaeniceps rex) also known as whalehead, whale-headed stork, or shoe-billed stork, is a very large stork-like bird. It derives its name from its enormous shoe-shaped bill" + + "\n| some people also called them the living dinosaur~~" + + "\n| \n| #shoebill #livingdinosaur #happyofunny #weirdcreature #weirdsoun..." + + "\n| \n| 🎞️ https://www.youtube.com/embed/kDMHHw8JqLE", + format: "org.matrix.custom.html", + formatted_body: `

YouTube

` + + `

Happy O Funny` + + `

Shoebill stork clattering sounds like machine guun~!! (Japan Matsue...` + + `

twitter
https://twitter.com/matsuevogelpark

The shoebill (Balaeniceps rex) also known as whalehead, whale-headed stork, or shoe-billed stork, is a very large stork-like bird. It derives its name from its enormous shoe-shaped bill
some people also called them the living dinosaur~~

#shoebill #livingdinosaur #happyofunny #weirdcreature #weirdsoun...` + + `

🎞️ https://www.youtube.com/embed/kDMHHw8JqLE` + + `

`, + "m.mentions": {} + }]) +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 69a4808..054b12c 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -514,6 +514,15 @@ async function messageToEvent(message, guild, options = {}, di) { // 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 const rep = new mxUtils.MatrixStringBuilder() + // Provider + if (embed.provider?.name) { + if (embed.provider.url) { + rep.addParagraph(`via ${embed.provider.name} ${embed.provider.url}`, tag`${embed.provider.name}`) + } else { + rep.addParagraph(`via ${embed.provider.name}`, tag`${embed.provider.name}`) + } + } + // Author and URL into a paragraph let authorNameText = embed.author?.name || "" if (authorNameText && embed.author?.icon_url) authorNameText = `⏺️ ${authorNameText}` // using the emoji instead of an image @@ -536,7 +545,9 @@ async function messageToEvent(message, guild, options = {}, di) { } } - if (embed.description) { + let embedTypeShouldShowDescription = embed.type !== "video" // Discord doesn't display descriptions for videos + if (embed.provider?.name === "YouTube") embedTypeShouldShowDescription = true // But I personally like showing the descriptions for YouTube videos specifically + if (embed.description && embedTypeShouldShowDescription) { const {body, html} = await transformContent(embed.description) rep.addParagraph(body, html) } @@ -550,7 +561,11 @@ async function messageToEvent(message, guild, options = {}, di) { rep.addParagraph(fieldRep.get().body, fieldRep.get().formatted_body) } - if (embed.image?.url) rep.addParagraph(`📸 ${embed.image.url}`) + let chosenImage = embed.image?.url + // the thumbnail seems to be used for "article" type but displayed big at the bottom by discord + if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url + if (chosenImage) rep.addParagraph(`📸 ${chosenImage}`) + if (embed.video?.url) rep.addParagraph(`🎞️ ${embed.video.url}`) if (embed.footer?.text) rep.addLine(`— ${embed.footer.text}`, tag`— ${embed.footer.text}`) diff --git a/test/data.js b/test/data.js index 49b84e6..b4f95b3 100644 --- a/test/data.js +++ b/test/data.js @@ -2123,6 +2123,185 @@ module.exports = { attachments: [], guild_id: "1150201337112449045" }, + vx_image: { + id: "1209926442981269544", + type: 0, + content: "https://vxtwitter.com/TomorrowCorp/status/1760330671074287875 we got a release date!!!", + channel_id: "288058913985789953", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + premium_type: 0, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "article", + url: "https://vxtwitter.com/TomorrowCorp/status/1760330671074287875", + title: "Tomorrow Corporation (@TomorrowCorp)", + description: "Mark your calendar with a wet black stain! World of Goo 2 releases on May 23, 2024 on Nintendo Switch, Epic Games Store (Win/Mac), and http://WorldOfGoo2.com (Win/Mac/Linux).\n" + + "\n" + + "https://tomorrowcorporation.com/posts/world-of-goo-2-now-with-100-more-release-dates-and-platforms\n" + + "\n" + + "💖 123 🔁 36", + color: 8388564, + author: { + name: "Twitter", + url: "https://twitter.com/tomorrowcorp/status/1760330671074287875" + }, + provider: { + name: "vxTwitter / fixvx", + url: "https://github.com/dylanpdx/BetterTwitFix" + }, + thumbnail: { + url: "https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg", + proxy_url: "https://images-ext-2.discordapp.net/external/eqA-NKoXzJ0Y_l-MlwN6shFDJibC0TbPxMNWSU5IpKY/https/pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg", + width: 1200, + height: 1200, + placeholder: "5SgKDwTIlqiPjIhzlspniIiNaN8It3AD", + placeholder_version: 1 + } + } + ], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-02-21T18:15:43.353000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + vx_video: { + id: "1209804622206599190", + type: 0, + content: "https://vxtwitter.com/McDonalds/status/1759971752254341417", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + premium_type: 0, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "video", + url: "https://vxtwitter.com/McDonalds/status/1759971752254341417", + title: "McDonald's (@McDonalds)", + description: "McDonald’s🤝@studiopierrot\n\n💖 89 🔁 21", + color: 8388564, + author: { + name: "McDonald’s🤝@studiopierrot\n\n💖 89 🔁 21", + url: "https://twitter.com/McDonalds/status/1759971752254341417" + }, + provider: { + name: "vxTwitter / fixvx", + url: "https://github.com/dylanpdx/BetterTwitFix" + }, + video: { + url: "https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12", + proxy_url: "https://images-ext-1.discordapp.net/external/TInoGDskHFBRSQR0ErWEmvmzi75EO28aSyiEXs3SB8E/%3Ftag%3D12/https/video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4", + width: 1280, + height: 720, + placeholder: "AggGBIAIp4iGeYdxjHgAAAAAAA==", + placeholder_version: 1 + } + } + ], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-02-21T10:11:39.017000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + youtube_video: { + id: "1214383754479534100", + type: 0, + content: "https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E\n\n\nJutomi I'm gonna make these sounds in your walls tonight", + channel_id: "112760669178241024", + author: { + id: "1060361805152669766", + username: "occimyy", + avatar: "3bf268de3eab1c5441da9585534d8aa5", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "Occimyy", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "video", + url: "https://www.youtube.com/watch?v=kDMHHw8JqLE", + title: "Shoebill stork clattering sounds like machine guun~!! (Japan Matsue...", + description: "twitter\n" + + "https://twitter.com/matsuevogelpark\n" + + "\n" + + "The shoebill (Balaeniceps rex) also known as whalehead, whale-headed stork, or shoe-billed stork, is a very large stork-like bird. It derives its name from its enormous shoe-shaped bill\n" + + "some people also called them the living dinosaur~~\n" + + "\n" + + "#shoebill #livingdinosaur #happyofunny #weirdcreature #weirdsoun...", + color: 16711680, + author: { + name: "Happy O Funny", + url: "https://www.youtube.com/channel/UCEpQ9aEb1NafpvWp5Aoizrg" + }, + provider: { name: "YouTube", url: "https://www.youtube.com" }, + thumbnail: { + url: "https://i.ytimg.com/vi/kDMHHw8JqLE/maxresdefault.jpg", + proxy_url: "https://images-ext-1.discordapp.net/external/eEPOxZQXfTHqvPQJBWqsgG3wxTQN20b8LXqw3jSqyRM/https/i.ytimg.com/vi/kDMHHw8JqLE/maxresdefault.jpg", + width: 1280, + height: 720, + placeholder: "WAgSDIIIdIprl4h4h4dNoEoEaQ==", + placeholder_version: 1 + }, + video: { + url: "https://www.youtube.com/embed/kDMHHw8JqLE", + width: 1280, + height: 720, + placeholder: "WAgSDIIIdIprl4h4h4dNoEoEaQ==", + placeholder_version: 1 + } + } + ], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-03-05T01:27:29.227000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, image_embed_and_attachment: { id: "1157854642810654821", type: 0, From 2fb68900c72c15bdfabf92c93f6d6ca1adda30c2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 6 Mar 2024 11:56:21 +1300 Subject: [PATCH 003/346] d->m: Support permissioned @everyone -> @room This only works if #9 is fixed in the future. --- d2m/converters/message-to-event.js | 4 +- d2m/converters/message-to-event.test.js | 60 +++++++++ test/data.js | 157 ++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 054b12c..3ede0a4 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -249,6 +249,8 @@ async function messageToEvent(message, guild, options = {}, di) { let repliedToEventRow = null let repliedToEventSenderMxid = null + if (message.mention_everyone) mentions.room = true + function addMention(mxid) { if (!mentions.user_ids) mentions.user_ids = [] if (!mentions.user_ids.includes(mxid)) mentions.user_ids.push(mxid) @@ -480,7 +482,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] - if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { + if (matches.length && matches.some(m => m[1].match(/[a-z]/i) && m[1] !== "everyone" && m[1] !== "here")) { const writtenMentionsText = matches.map(m => m[1].toLowerCase()) const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() assert(roomID) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index ce694fd..edcb44c 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -789,3 +789,63 @@ test("message2event: crossposted announcements say where they are crossposted fr formatted_body: "🔀 Chewey Bot Official Server #announcements
All text based commands are now inactive on Chewey Bot
To continue using commands you'll need to use them as slash commands" }]) }) + +test("message2event: @everyone", async t => { + const events = await messageToEvent(data.message_mention_everyone.at_everyone) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@room", + "m.mentions": { + room: true + } + }]) +}) + +test("message2event: @here", async t => { + const events = await messageToEvent(data.message_mention_everyone.at_here) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@room", + "m.mentions": { + room: true + } + }]) +}) + +test("message2event: @everyone without permission", async t => { + const events = await messageToEvent(data.message_mention_everyone.at_everyone_without_permission) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@everyone <-- this is testing that it DOESN'T mention. if this mentions everyone then my apologies.", + format: "org.matrix.custom.html", + formatted_body: "@everyone <-- this is testing that it DOESN'T mention. if this mentions everyone then my apologies.", + "m.mentions": {} + }]) +}) + +test("message2event: @here without permission", async t => { + const events = await messageToEvent(data.message_mention_everyone.at_here_without_permission) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@here <-- this is testing that it DOESN'T mention. if this mentions people then my apologies.", + format: "org.matrix.custom.html", + formatted_body: "@here <-- this is testing that it DOESN'T mention. if this mentions people then my apologies.", + "m.mentions": {} + }]) +}) + +test("message2event: @everyone within a link", async t => { + const events = await messageToEvent(data.message_mention_everyone.at_everyone_within_link) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://github.com/@everyone", + format: "org.matrix.custom.html", + formatted_body: `https://github.com/@everyone`, + "m.mentions": {} + }]) +}) diff --git a/test/data.js b/test/data.js index b4f95b3..61ee5d0 100644 --- a/test/data.js +++ b/test/data.js @@ -1925,6 +1925,163 @@ module.exports = { webhook_id: "1195662438662680720" } }, + message_mention_everyone: { + at_everyone: { + id: "1214510099058655252", + type: 0, + content: "@everyone", + channel_id: "1100319550446252084", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: true, + tts: false, + timestamp: "2024-03-05T09:49:32.122000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + at_here: { + id: "1214510192230797332", + type: 0, + content: "@here", + channel_id: "1100319550446252084", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: true, + tts: false, + timestamp: "2024-03-05T09:49:54.336000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + at_everyone_without_permission: { + id: "1214510346623258654", + type: 0, + content: "@everyone <-- this is testing that it DOESN'T mention. if this mentions everyone then my apologies.", + channel_id: "112760669178241024", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-03-05T09:50:31.146000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + at_here_without_permission: { + id: "1214510346623258654", + type: 0, + content: "@here <-- this is testing that it DOESN'T mention. if this mentions people then my apologies.", + channel_id: "112760669178241024", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-03-05T09:50:31.146000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + at_everyone_within_link: { + id: "1214510225885888563", + type: 0, + content: "https://github.com/@everyone", + channel_id: "1100319550446252084", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-03-05T09:50:02.360000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + }, message_with_embeds: { nothing_but_a_field: { guild_id: "497159726455455754", From bf3d219716b46daf51dc4907270b8bbbb7d1ef62 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 6 Mar 2024 17:37:16 +1300 Subject: [PATCH 004/346] Add helper for permission calculations --- d2m/converters/remove-reaction.js | 2 +- d2m/event-dispatcher.js | 6 ++-- discord/discord-command-handler.js | 2 +- discord/utils.js | 46 ++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/d2m/converters/remove-reaction.js b/d2m/converters/remove-reaction.js index a6c8ace..caa96d1 100644 --- a/d2m/converters/remove-reaction.js +++ b/d2m/converters/remove-reaction.js @@ -12,7 +12,7 @@ const utils = sync.require("../../m2d/converters/utils") * @typedef ReactionRemoveRequest * @prop {string} eventID * @prop {string | null} mxid - * @prop {BigInt} [hash] + * @prop {bigint} [hash] */ /** diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index e769053..3db1d45 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -115,8 +115,7 @@ module.exports = { if (!member) return if (!("permission_overwrites" in channel)) continue const permissions = dUtils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) - const wants = BigInt(1 << 10) | BigInt(1 << 16) // VIEW_CHANNEL + READ_MESSAGE_HISTORY - if ((permissions & wants) !== wants) continue // We don't have permission to look back in this channel + if (!dUtils.hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])) continue // We don't have permission to look back in this channel /** More recent messages come first. */ // console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`) @@ -164,8 +163,7 @@ module.exports = { // Permissions check const permissions = dUtils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) - const wants = BigInt(1 << 10) | BigInt(1 << 16) // VIEW_CHANNEL + READ_MESSAGE_HISTORY - if ((permissions & wants) !== wants) continue // We don't have permission to look up the pins in this channel + if (!dUtils.hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])) continue // We don't have permission to look up the pins in this channel const row = select("channel_room", ["room_id", "last_bridged_pin_timestamp"], {channel_id: channel.id}).get() if (!row) continue // Only care about already bridged channels diff --git a/discord/discord-command-handler.js b/discord/discord-command-handler.js index aad06e1..f69346f 100644 --- a/discord/discord-command-handler.js +++ b/discord/discord-command-handler.js @@ -137,7 +137,7 @@ const commands = [{ // Check CREATE_INSTANT_INVITE permission assert(message.member) const guildPermissions = utils.getPermissions(message.member.roles, guild.roles) - if (!(guildPermissions & BigInt(1))) { + if (!(guildPermissions & DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) { return discord.snow.channel.createMessage(channel.id, { ...ctx, content: "You don't have permission to invite people to this Discord server." diff --git a/discord/utils.js b/discord/utils.js index 6788bcf..ff1f215 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -1,6 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") +const assert = require("assert").strict const EPOCH = 1420070400000 @@ -49,6 +50,48 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { return allowed } +/** + * Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`. + * It is designed like this to avoid developer error with bit manipulations. + * + * @param {bigint} resolvedPermissions + * @param {bigint} permissionToCheckFor + * @returns {boolean} whether the user has the requested permission + * @example + * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites) + * hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) + */ +function hasPermission(resolvedPermissions, permissionToCheckFor) { + // Make sure permissionToCheckFor has exactly one permission in it + assert.equal(permissionToCheckFor.toString(2).match(/1/g), 1) + // Do the actual calculation + return (resolvedPermissions & permissionToCheckFor) === permissionToCheckFor +} + +/** + * @param {bigint} resolvedPermissions + * @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor + * @returns {boolean} whether the user has any of the requested permissions + * @example + * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites) + * hasSomePermissions(permissions, ["ViewChannel", "ReadMessageHistory"]) + */ +function hasSomePermissions(resolvedPermissions, permissionsToCheckFor) { + return permissionsToCheckFor.some(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x])) +} + +/** + * @param {bigint} resolvedPermissions + * @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor + * @returns {boolean} whether the user has all of the requested permissions + * @example + * const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites) + * hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"]) + */ +function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) { + return permissionsToCheckFor.every(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x])) +} + /** * Command interaction responses have a webhook_id for some reason, but still have real author info of a real bot user in the server. * @param {DiscordTypes.APIMessage} message @@ -69,6 +112,9 @@ function timestampToSnowflakeInexact(timestamp) { } module.exports.getPermissions = getPermissions +module.exports.hasPermission = hasPermission +module.exports.hasSomePermissions = hasSomePermissions +module.exports.hasAllPermissions = hasAllPermissions module.exports.isWebhookMessage = isWebhookMessage module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact From 043f178d1e424b0d61c6f912d8712fc3ac137687 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 6 Mar 2024 17:40:06 +1300 Subject: [PATCH 005/346] Map Discord member permissions to sim user PLs Including PL 20 for members who can mention everyone. --- d2m/actions/create-room.js | 3 ++ d2m/actions/register-pk-user.js | 2 +- d2m/actions/register-user.js | 90 ++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 6994b57..0dea283 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -135,6 +135,9 @@ async function channelToKState(channel, guild) { events: { "m.room.avatar": 0 }, + notifications: { + room: 20 // TODO: Matrix users should have the same abilities as unprivileged Discord members. So make this automatically configured based on the guild or channel's default mention everyone permissions. That way if unprivileged Discord members can mention everyone, Matrix users can too. + }, users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {}) }, "chat.schildi.hide_ui/read_receipts": { diff --git a/d2m/actions/register-pk-user.js b/d2m/actions/register-pk-user.js index ca47b7c..1e223c6 100644 --- a/d2m/actions/register-pk-user.js +++ b/d2m/actions/register-pk-user.js @@ -131,7 +131,7 @@ async function syncUser(author, pkMessage, roomID) { db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username) // Sync the member state const content = await memberToStateContent(pkMessage, author) - const currentHash = registerUser._hashProfileContent(content) + const currentHash = registerUser._hashProfileContent(content, 0) const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get() // only do the actual sync if the hash has changed since we last looked if (existingHash !== currentHash) { diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 8244fe2..7e1d9f9 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -2,6 +2,8 @@ const assert = require("assert") const reg = require("../../matrix/read-registration") +const DiscordTypes = require("discord-api-types/v10") +const mixin = require("mixin-deep") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough @@ -9,6 +11,8 @@ const {discord, sync, db, select} = passthrough const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +/** @type {import("../../discord/utils")} */ +const utils = sync.require("../../discord/utils") /** @type {import("../converters/user-to-mxid")} */ const userToMxid = sync.require("../converters/user-to-mxid") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore @@ -18,7 +22,7 @@ require("xxhash-wasm")().then(h => hasher = h) /** * A sim is an account that is being simulated by the bridge to copy events from the other side. - * @param {import("discord-api-types/v10").APIUser} user + * @param {DiscordTypes.APIUser} user * @returns mxid */ async function createSim(user) { @@ -46,7 +50,7 @@ async function createSim(user) { /** * Ensure a sim is registered for the user. * If there is already a sim, use that one. If there isn't one yet, register a new sim. - * @param {import("discord-api-types/v10").APIUser} user + * @param {DiscordTypes.APIUser} user * @returns {Promise} mxid */ async function ensureSim(user) { @@ -62,7 +66,7 @@ async function ensureSim(user) { /** * Ensure a sim is registered for the user and is joined to the room. - * @param {import("discord-api-types/v10").APIUser} user + * @param {DiscordTypes.APIUser} user * @param {string} roomID * @returns {Promise} mxid */ @@ -92,8 +96,8 @@ async function ensureSimJoined(user, roomID) { } /** - * @param {import("discord-api-types/v10").APIUser} user - * @param {Omit} member + * @param {DiscordTypes.APIUser} user + * @param {Omit} member */ async function memberToStateContent(user, member, guildID) { let displayname = user.username @@ -123,8 +127,46 @@ async function memberToStateContent(user, member, guildID) { return content } -function _hashProfileContent(content) { - const unsignedHash = hasher.h64(`${content.displayname}\u0000${content.avatar_url}`) +/** + * https://gitdab.com/cadence/out-of-your-element/issues/9 + * @param {DiscordTypes.APIUser} user + * @param {Omit} member + * @param {DiscordTypes.APIGuild} guild + * @param {DiscordTypes.APIGuildChannel} channel + * @returns {number} 0 to 100 + */ +function memberToPowerLevel(user, member, guild, channel) { + const permissions = utils.getPermissions(member.roles, guild.roles, user.id, channel.permission_overwrites) + /* + * PL 100 = Administrator = People who can brick the room. RATIONALE: + * - Administrator. + * - Manage Webhooks: People who remove the webhook can break the room. + * - Manage Guild: People who can manage guild can add bots. + * - Manage Channels: People who can manage the channel can delete it. + * (Setting sim users to PL 100 is safe because even though we can't demote the sims we can use code to make the sims demote themselves.) + */ + if (guild.owner_id === user.id || utils.hasSomePermissions(permissions, ["Administrator", "ManageWebhooks", "ManageGuild", "ManageChannels"])) return 100 + /* + * PL 50 = Moderator = People who can manage people and messages in many ways. RATIONALE: + * - Manage Messages: Can moderate by pinning or deleting the conversation. + * - Manage Nicknames: Can moderate by removing inappropriate nicknames. + * - Manage Threads: Can moderate by deleting conversations. + * - Kick Members & Ban Members: Can moderate by removing disruptive people. + * - Mute Members & Deafen Members: Can moderate by silencing disruptive people in ways they can't undo. + * - Moderate Members. + */ + if (utils.hasSomePermissions(permissions, ["ManageMessages", "ManageNicknames", "ManageThreads", "KickMembers", "BanMembers", "MuteMembers", "DeafenMembers", "ModerateMembers"])) return 50 + /* PL 20 = Mention Everyone for technical reasons. */ + if (utils.hasSomePermissions(permissions, ["MentionEveryone"])) return 20 + return 0 +} + +/** + * @param {any} content + * @param {number} powerLevel + */ +function _hashProfileContent(content, powerLevel) { + const unsignedHash = hasher.h64(`${content.displayname}\u0000${content.avatar_url}\u0000${powerLevel}`) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range return signedHash } @@ -133,20 +175,30 @@ function _hashProfileContent(content) { * Sync profile data for a sim user. This function follows the following process: * 1. Join the sim to the room if needed * 2. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before - * 3. Compare against the previously known state content, which is helpfully stored in the database - * 4. If the state content has changed, send it to Matrix and update it in the database for next time - * @param {import("discord-api-types/v10").APIUser} user - * @param {Omit} member + * 3. Calculate the power level the user should get based on their Discord permissions + * 4. Compare against the previously known state content, which is helpfully stored in the database + * 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time + * @param {DiscordTypes.APIUser} user + * @param {Omit} member + * @param {DiscordTypes.APIGuild} guild + * @param {DiscordTypes.APIGuildChannel} channel * @returns {Promise} mxid of the updated sim */ -async function syncUser(user, member, guildID, roomID) { +async function syncUser(user, member, guild, channel, roomID) { const mxid = await ensureSimJoined(user, roomID) - const content = await memberToStateContent(user, member, guildID) - const currentHash = _hashProfileContent(content) + const content = await memberToStateContent(user, member, guild.id) + const powerLevel = memberToPowerLevel(user, member, guild, channel) + const currentHash = _hashProfileContent(content, powerLevel) const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get() // only do the actual sync if the hash has changed since we last looked if (existingHash !== currentHash) { + // Update room member state await api.sendState(roomID, "m.room.member", mxid, content, mxid) + // Update power levels + const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "") + mixin(powerLevelsStateContent, {users: {[mxid]: powerLevel}}) + api.sendState(roomID, "m.room.power_levels", "", powerLevelsStateContent) + // Update cached hash db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) } return mxid @@ -158,23 +210,25 @@ async function syncAllUsersInRoom(roomID) { const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get() assert.ok(typeof channelID === "string") - /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ + /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) const guildID = channel.guild_id assert.ok(typeof guildID === "string") + /** @ts-ignore @type {DiscordTypes.APIGuild} */ + const guild = discord.guilds.get(guildID) for (const mxid of mxids) { const userID = select("sim", "user_id", {mxid}).pluck().get() assert.ok(typeof userID === "string") - /** @ts-ignore @type {Required} */ + /** @ts-ignore @type {Required} */ const member = await discord.snow.guild.getGuildMember(guildID, userID) - /** @ts-ignore @type {Required} user */ + /** @ts-ignore @type {Required} user */ const user = member.user assert.ok(user) console.log(`[user sync] to matrix: ${user.username} in ${channel.name}`) - await syncUser(user, member, guildID, roomID) + await syncUser(user, member, guild, channel, roomID) } } From 0f1cf7a20c9b22ef5801146e852f8e3b3483701f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 09:13:25 +1300 Subject: [PATCH 006/346] Fix calls to syncUser/registerUser --- d2m/actions/register-user.js | 12 ++++++++---- d2m/actions/send-message.js | 12 +++++++----- d2m/event-dispatcher.js | 2 +- discord/utils.js | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 7e1d9f9..68b1a88 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const reg = require("../../matrix/read-registration") const DiscordTypes = require("discord-api-types/v10") const mixin = require("mixin-deep") @@ -180,11 +180,12 @@ function _hashProfileContent(content, powerLevel) { * 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time * @param {DiscordTypes.APIUser} user * @param {Omit} member - * @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuildChannel} channel + * @param {DiscordTypes.APIGuild} guild + * @param {string} roomID * @returns {Promise} mxid of the updated sim */ -async function syncUser(user, member, guild, channel, roomID) { +async function syncUser(user, member, channel, guild, roomID) { const mxid = await ensureSimJoined(user, roomID) const content = await memberToStateContent(user, member, guild.id) const powerLevel = memberToPowerLevel(user, member, guild, channel) @@ -204,6 +205,9 @@ async function syncUser(user, member, guild, channel, roomID) { return mxid } +/** + * @param {string} roomID + */ async function syncAllUsersInRoom(roomID) { const mxids = select("sim_member", "mxid", {room_id: roomID}).pluck().all() @@ -228,7 +232,7 @@ async function syncAllUsersInRoom(roomID) { assert.ok(user) console.log(`[user sync] to matrix: ${user.username} in ${channel.name}`) - await syncUser(user, member, guild, channel, roomID) + await syncUser(user, member, channel, guild, roomID) } } diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 8c26f07..a1290e0 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,6 +1,7 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough @@ -18,17 +19,18 @@ const createRoom = sync.require("../actions/create-room") const dUtils = sync.require("../../discord/utils") /** - * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message - * @param {import("discord-api-types/v10").APIGuild} guild + * @param {DiscordTypes.GatewayMessageCreateDispatchData} message + * @param {DiscordTypes.APIGuildChannel} channel + * @param {DiscordTypes.APIGuild} guild * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel */ -async function sendMessage(message, guild, row) { +async function sendMessage(message, channel, guild, row) { const roomID = await createRoom.ensureRoom(message.channel_id) let senderMxid = null if (!dUtils.isWebhookMessage(message)) { if (message.member) { // available on a gateway message create event - senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID) } else { // well, good enough... senderMxid = await registerUser.ensureSimJoined(message.author, roomID) } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 3db1d45..2c4dcd5 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -248,7 +248,7 @@ module.exports = { if (affected) return // @ts-ignore - await sendMessage.sendMessage(message, guild, row), + await sendMessage.sendMessage(message, channel, guild, row), await discordCommandHandler.execute(message, channel, guild) }, diff --git a/discord/utils.js b/discord/utils.js index ff1f215..6b6b602 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -63,7 +63,7 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { */ function hasPermission(resolvedPermissions, permissionToCheckFor) { // Make sure permissionToCheckFor has exactly one permission in it - assert.equal(permissionToCheckFor.toString(2).match(/1/g), 1) + assert.equal(permissionToCheckFor.toString(2).match(/1/g)?.length, 1) // Do the actual calculation return (resolvedPermissions & permissionToCheckFor) === permissionToCheckFor } From 12d85c982e20adba182f73b7576a02d62b0b9ca4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 10:17:39 +1300 Subject: [PATCH 007/346] Allow Matrixers to @room if Discorders can too --- d2m/actions/create-room.js | 7 ++++++- d2m/actions/create-room.test.js | 11 +++++++++++ discord/utils.js | 2 +- test/data.js | 22 +++++++++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 0dea283..a8d2846 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -12,6 +12,8 @@ const file = sync.require("../../matrix/file") const api = sync.require("../../matrix/api") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") +/** @type {import("../../discord/utils")} */ +const utils = sync.require("../../discord/utils") /** @type {import("./create-space")}) */ const createSpace = sync.require("./create-space") // watch out for the require loop @@ -119,6 +121,9 @@ async function channelToKState(channel, guild) { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } + const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) + const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, @@ -136,7 +141,7 @@ async function channelToKState(channel, guild) { "m.room.avatar": 0 }, notifications: { - room: 20 // TODO: Matrix users should have the same abilities as unprivileged Discord members. So make this automatically configured based on the guild or channel's default mention everyone permissions. That way if unprivileged Discord members can mention everyone, Matrix users can too. + room: everyoneCanMentionEveryone ? 0 : 20 }, users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {}) }, diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index 93f9203..be0febb 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,5 +1,6 @@ // @ts-check +const mixin = require("mixin-deep") const {channelToKState, _convertNameAndTopic} = require("./create-room") const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") @@ -39,6 +40,16 @@ test("channel2room: invite-only privacy room", async t => { ) }) +test("channel2room: room where limited people can mention everyone", async t => { + const limitedGuild = mixin({}, testData.guild.general) + limitedGuild.roles[0].permissions = (BigInt(limitedGuild.roles[0].permissions) - 131072n).toString() + const limitedRoom = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}}}) + t.deepEqual( + kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild).then(x => x.channelKState)), + limitedRoom + ) +}) + test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), diff --git a/discord/utils.js b/discord/utils.js index 6b6b602..2726a1a 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -26,7 +26,7 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { } if (channelOverwrites) { - /** @type {((overwrite: Required["permission_overwrites"][0]) => any)[]} */ + /** @type {((overwrite: Required) => any)[]} */ const actions = [ // Channel @everyone deny overwrite => overwrite.id === everyoneID && (allowed &= ~BigInt(overwrite.deny)), diff --git a/test/data.js b/test/data.js index 61ee5d0..c165322 100644 --- a/test/data.js +++ b/test/data.js @@ -47,6 +47,9 @@ module.exports = { }, users: { "@test_auto_invite:example.org": 100 + }, + notifications: { + room: 0 } }, "chat.schildi.hide_ui/read_receipts": {hidden: true}, @@ -98,7 +101,6 @@ module.exports = { icon: "a_f83622e09ead74f0c5c527fe241f8f8c", emojis: [ { - version: 0, roles: [], require_colons: true, name: "hippo", @@ -108,7 +110,6 @@ module.exports = { animated: false }, { - version: 0, roles: [], require_colons: true, name: "hipposcope", @@ -121,7 +122,20 @@ module.exports = { premium_subscription_count: 14, roles: [ { - version: 1696964862461, + unicode_emoji: null, + tags: {}, + position: 0, + permissions: '559623605575360', + name: '@everyone', + mentionable: false, + managed: false, + id: '112760669178241024', + icon: null, + hoist: false, + flags: 0, + color: 0 + }, + { unicode_emoji: null, tags: {}, position: 22, @@ -135,7 +149,6 @@ module.exports = { flags: 0, color: 0 }, { - version: 1696964862776, unicode_emoji: null, tags: {}, position: 131, @@ -149,7 +162,6 @@ module.exports = { flags: 0, color: 11076095 }, { - version: 1696964862698, unicode_emoji: '🍂', tags: {}, position: 102, From a190e690b1b30e9be12f27ab1b1a6a0c52e9df22 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 10:22:49 +1300 Subject: [PATCH 008/346] Add tests for somePermissions/allPermissions check --- discord/utils.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/discord/utils.test.js b/discord/utils.test.js index 1f3783f..815c0a1 100644 --- a/discord/utils.test.js +++ b/discord/utils.test.js @@ -1,3 +1,4 @@ +const DiscordTypes = require("discord-api-types/v10") const {test} = require("supertape") const data = require("../test/data") const utils = require("./utils") @@ -82,3 +83,27 @@ test("getPermissions: channel overwrite to allow role works", t => { const want = BigInt(1 << 10 | 1 << 16) t.equal((permissions & want), want) }) + +test("hasSomePermissions: detects the permission", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers + const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, true) +}) + +test("hasSomePermissions: doesn't detect not the permission", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages + const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, false) +}) + +test("hasAllPermissions: detects the permissions", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.BanMembers | DiscordTypes.PermissionFlagsBits.MentionEveryone + const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, true) +}) + +test("hasAllPermissions: doesn't detect not the permissions", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages | DiscordTypes.PermissionFlagsBits.KickMembers + const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, false) +}) From e03155817750553719bc05ad6967190b39879f29 Mon Sep 17 00:00:00 2001 From: RNLFoof Date: Wed, 6 Mar 2024 17:08:43 -0500 Subject: [PATCH 009/346] Added a comment explaining sims to db\orm-defs.d.ts --- db/orm-defs.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index 622e1a0..3e68639 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -50,6 +50,7 @@ export type Models = { channel_id: string } + // A sim is an account that is being simulated by the bridge to copy events from the other side. See d2m\actions\register-user.js for relevant behavior and comments. sim: { user_id: string sim_name: string From cc9e1de49ee1c5689cd1e32507c71fba2762293e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 12:19:07 +1300 Subject: [PATCH 010/346] Remove deep-equal dependency --- d2m/actions/create-space.js | 4 +- db/orm-defs.d.ts | 2 +- db/orm.test.js | 2 +- matrix/kstate.js | 6 +- package-lock.json | 283 ++++++++++++++++++++++-------------- package.json | 1 - readme.md | 1 - 7 files changed, 179 insertions(+), 120 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 7d50199..a7390ed 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,8 +1,8 @@ // @ts-check const assert = require("assert").strict +const {isDeepStrictEqual} = require("util") const DiscordTypes = require("discord-api-types/v10") -const deepEqual = require("deep-equal") const reg = require("../../matrix/read-registration") const passthrough = require("../../passthrough") @@ -226,7 +226,7 @@ async function syncSpaceExpressions(data, checkBeforeSync) { // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake. existing = fn([]) } - if (deepEqual(existing, content, {strict: true})) return + if (isDeepStrictEqual(existing, content)) return } api.sendState(spaceID, "im.ponies.room_emotes", eventKey, content) } diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index 622e1a0..0223058 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -100,7 +100,7 @@ export type Prepared = { safeIntegers: () => Prepared<{[K in keyof Row]: Row[K] extends number ? BigInt : Row[K]}> raw: () => Prepared all: (..._: any[]) => Row[] - get: (..._: any[]) => Row | null + get: (..._: any[]) => Row | null | undefined } export type AllKeys = U extends any ? keyof U : never diff --git a/db/orm.test.js b/db/orm.test.js index 066eabb..3d4e8ef 100644 --- a/db/orm.test.js +++ b/db/orm.test.js @@ -51,5 +51,5 @@ test("orm: from: join direction works", t => { const hasNoOwner = from("sim").join("sim_proxy", "user_id", "left").select("user_id", "proxy_owner_id").where({sim_name: "crunch_god"}).get() t.deepEqual(hasNoOwner, {user_id: "820865262526005258", proxy_owner_id: null}) const hasNoOwnerInner = from("sim").join("sim_proxy", "user_id", "inner").select("user_id", "proxy_owner_id").where({sim_name: "crunch_god"}).get() - t.deepEqual(hasNoOwnerInner, null) + t.deepEqual(hasNoOwnerInner, undefined) }) diff --git a/matrix/kstate.js b/matrix/kstate.js index 6358974..3c4189d 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -2,7 +2,7 @@ const assert = require("assert").strict const mixin = require("mixin-deep") -const deepEqual = require("deep-equal") +const {isDeepStrictEqual} = require("util") /** Mutates the input. */ function kstateStripConditionals(kstate) { @@ -51,14 +51,14 @@ function diffKState(actual, target) { // Special handling for power levels, we want to deep merge the actual and target into the final state. if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`) const temp = mixin({}, actual[key], target[key]) - if (!deepEqual(actual[key], temp, {strict: true})) { + if (!isDeepStrictEqual(actual[key], temp)) { // they differ. use the newly prepared object as the diff. diff[key] = temp } } else if (key in actual) { // diff - if (!deepEqual(actual[key], target[key], {strict: true})) { + if (!isDeepStrictEqual(actual[key], target[key])) { // they differ. use the target as the diff. diff[key] = target[key] } diff --git a/package-lock.json b/package-lock.json index 880c150..2e45796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", - "deep-equal": "^2.2.3", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", @@ -549,12 +548,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -581,9 +584,13 @@ "dev": true }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -797,13 +804,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1084,6 +1096,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -1120,23 +1133,28 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -1252,10 +1270,30 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -1469,6 +1507,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -1548,6 +1587,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1562,15 +1602,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1667,21 +1711,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1696,11 +1730,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1729,11 +1763,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -1846,12 +1881,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -1870,6 +1906,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -1882,13 +1919,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1903,6 +1943,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -1914,6 +1955,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -1929,6 +1971,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -1952,6 +1995,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -1975,6 +2019,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1983,6 +2028,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -1997,6 +2043,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -2012,16 +2059,21 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2031,6 +2083,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -2045,6 +2098,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -2055,28 +2109,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2085,6 +2122,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -2096,7 +2134,8 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/isexe": { "version": "2.0.0", @@ -2449,12 +2488,13 @@ } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -2467,17 +2507,19 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -2636,6 +2678,15 @@ "node": ">=14.19.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -2840,13 +2891,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -3027,27 +3080,31 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3254,6 +3311,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -3820,6 +3878,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -3835,6 +3894,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -3846,15 +3906,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 3a9a12c..bff4724 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", - "deep-equal": "^2.2.3", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", diff --git a/readme.md b/readme.md index 49c2e66..b64f780 100644 --- a/readme.md +++ b/readme.md @@ -167,7 +167,6 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. * (8) snowtransfer: Discord API library with bring-your-own-caching that I trust. -* (0) deep-equal: It's already pulled in by supertape. * (1) discord-markdown: This is my fork! * (0) get-stream: Only needed if content_length_workaround is true. * (0) giframe: This is my fork! From 25cd8cb289f8238c622bb5d4364e6d26098bec72 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 13:07:10 +1300 Subject: [PATCH 011/346] Use allowed_mentions instead of disableEveryone --- m2d/converters/event-to-message.js | 11 +- m2d/converters/event-to-message.test.js | 405 +++++++++++++++++++----- 2 files changed, 330 insertions(+), 86 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index d589277..8da3004 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -425,9 +425,6 @@ const attachmentEmojis = new Map([ * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"], mxcDownloader: (mxc: string) => Promise}} di simple-as-nails dependency injection for the matrix API */ async function eventToMessage(event, guild, di) { - /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ - let messages = [] - let displayName = event.sender let avatarURL = undefined /** @type {string[]} */ @@ -786,11 +783,15 @@ async function eventToMessage(event, guild, di) { // Split into 2000 character chunks const chunks = chunk(content, 2000) - messages = messages.concat(chunks.map(content => ({ + /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ + const messages = chunks.map(content => ({ content, + allowed_mentions: { + parse: ["users", "roles"] + }, username: displayNameShortened, avatar_url: avatarURL - }))) + })) if (attachments.length) { // If content is empty (should be the case when uploading a file) then chunk-text will create 0 messages. diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 7471f6a..3366d18 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -90,7 +90,10 @@ test("event2message: body is used when there is no formatted_body", async t => { messagesToSend: [{ username: "cadence [they]", content: "testing plaintext", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -125,7 +128,10 @@ test("event2message: any markdown in body is escaped, except strikethrough", asy messagesToSend: [{ username: "cadence [they]", content: "testing \\*\\*special\\*\\* ~~things~~ which \\_should\\_ \\*not\\* \\`trigger\\` @any , except strikethrough", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -157,7 +163,10 @@ test("event2message: links in formatted body are not broken", async t => { messagesToSend: [{ username: "cadence [they]", content: "<@111604486476181504> I wonder what the midjourney text description of this photo is https://upload.wikimedia.org/wikipedia/commons/f/f3/After_gay_pride%2C_rainbow_flags_flying_along_Beach_Street_%2814853144744%29.jpg", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -187,7 +196,10 @@ test("event2message: links in plaintext body are not broken", async t => { messagesToSend: [{ username: "cadence [they]", content: "I wonder what the midjourney text description of this photo is https://upload.wikimedia.org/wikipedia/commons/f/f3/After_gay_pride%2C_rainbow_flags_flying_along_Beach_Street_%2814853144744%29.jpg", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -214,7 +226,10 @@ test("event2message: links in formatted body where the text & href are the same, messagesToSend: [{ username: "cadence [they]", content: "https://privatebin.net/?9111cb16f28da21b#62CKkEr6WvXZ1gQv2M6agazsA7tGYX8ZP8drETYujYZr", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -245,7 +260,10 @@ test("event2message: basic html is converted to markdown", async t => { messagesToSend: [{ username: "cadence [they]", content: "this **is** a _**test** __of___ ~~_formatting_~~", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -276,7 +294,10 @@ test("event2message: spoilers work", async t => { messagesToSend: [{ username: "cadence [they]", content: "this **is** a ||_test_|| of ||spoilers||", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -307,7 +328,10 @@ test("event2message: spoiler reasons work", async t => { messagesToSend: [{ username: "cadence [they]", content: "\\(cw crossword spoilers you'll never believe. don't tell anybody\\) ||zoe kills a 5 letter noun at the end||", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -338,7 +362,10 @@ test("event2message: markdown syntax is escaped", async t => { messagesToSend: [{ username: "cadence [they]", content: "this \\*\\*is\\*\\* an **_extreme_** \\\\\\*test\\\\\\* of", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -369,7 +396,10 @@ test("event2message: html lines are bridged correctly", async t => { messagesToSend: [{ username: "cadence [they]", content: "paragraph one\nline _two_\nline three\n\nparagraph two\nline _two_\nline three\n\nparagraph three\n\nparagraph four\nline two\nline three\nline four\n\nparagraph five", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -400,7 +430,10 @@ test("event2message: html lines are bridged correctly", async t => { messagesToSend: [{ username: "cadence [they]", content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\n line five", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -432,7 +465,10 @@ test("event2message: whitespace is collapsed", async t => { messagesToSend: [{ username: "cadence [they]", content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\nline five", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -465,7 +501,10 @@ test("event2message: lists are bridged correctly", async t => { messagesToSend: [{ username: "cadence [they]", content: "* line one\n* line two\n* line three\n * nested one\n * nested two\n* line four", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -494,11 +533,17 @@ test("event2message: long messages are split", async t => { messagesToSend: [{ username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(15)).slice(0, -1), - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }, { username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(4)).slice(0, -1), - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -529,7 +574,10 @@ test("event2message: code blocks work", async t => { messagesToSend: [{ username: "cadence [they]", content: "preceding\n\n```\ncode block\n```\n\nfollowing `code` is inline", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -556,7 +604,10 @@ test("event2message: code block contents are formatted correctly and not escaped messagesToSend: [{ username: "cadence [they]", content: "```\ninput = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n```\n\n`input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,`", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -585,7 +636,10 @@ test("event2message: code blocks are uploaded as attachments instead if they con content: "So if you run code like this `[inline_code.java]` it should print a markdown formatted code block", attachments: [{id: "0", filename: "inline_code.java"}], pendingFiles: [{name: "inline_code.java", buffer: Buffer.from('System.out.println("```");')}], - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -625,7 +679,10 @@ test("event2message: characters are encoded properly in code blocks", async t => + '\n .map(|c| c.get(1).unwrap().as_str())' + '\n .collect::();' )}], - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -656,7 +713,10 @@ test("event2message: quotes have an appropriate amount of whitespace", async t = messagesToSend: [{ username: "cadence [they]", content: "> Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands\n🤨", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -694,7 +754,10 @@ test("event2message: lists have appropriate line breaks", async t => { messagesToSend: [{ username: "Milan", content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`, - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -732,7 +795,10 @@ test("event2message: ordered list start attribute works", async t => { messagesToSend: [{ username: "Milan", content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`, - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -761,7 +827,10 @@ test("event2message: m.emote plaintext works", async t => { messagesToSend: [{ username: "cadence [they]", content: "\\* cadence \\[they\\] tests an m.emote message", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -792,7 +861,10 @@ test("event2message: m.emote markdown syntax is escaped", async t => { messagesToSend: [{ username: "cadence [they]", content: "\\* cadence \\[they\\] shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -842,7 +914,10 @@ test("event2message: rich reply to a sim user", async t => { content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + "\n> Slow news day." + "\nTesting this reply, ignore", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -911,7 +986,10 @@ test("event2message: rich reply to a rich reply to a multi-line message should c content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> I just checked in a fix that will probably work..." + "\nwill try later (tomorrow if I don't forgor)", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -988,7 +1066,10 @@ test("event2message: rich reply to an already-edited message will quote the new content: "> <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" + "\n> this is the new content. heya!" + "\nhiiiii....", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1038,7 +1119,10 @@ test("event2message: rich reply to a missing event will quote from formatted_bod username: "cadence [they]", content: "> But who sees the seashells she sells sitting..." + "\nWhat a tongue-bender...", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1084,7 +1168,10 @@ test("event2message: rich reply to a missing event without formatted_body will u messagesToSend: [{ username: "cadence [they]", content: "Testing this reply, ignore", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1132,7 +1219,10 @@ test("event2message: rich reply to a missing event and no reply fallback will no messagesToSend: [{ username: "cadence [they]", content: "Testing this reply, ignore.", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1181,7 +1271,10 @@ test("event2message: should avoid using blockquote contents as reply preview in content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + "\n> that can't be true! there's no way :o" + "\nI agree!", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1267,7 +1360,10 @@ test("event2message: should include a reply preview when message ends with a blo content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**_ooye_cookie**:" + "\n> https://tootsuite.net/Warp-Gate2.gif tanget: @..." + "\naichmophobia", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1348,7 +1444,10 @@ test("event2message: should include a reply preview when replying to a descripti content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" + "\n> It looks like this queue has ended." + `\nso you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?`, - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1398,7 +1497,10 @@ test("event2message: entities are not escaped in main message or reply preview", content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> Testing? \"':.`[]&things" + "\n_Testing?_ \"':.\\`\\[\\]&things", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1445,7 +1547,10 @@ test("event2message: reply preview converts emoji formatting when replying to a content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> <:hippo:230201364309868544>" + "\nreply", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1492,7 +1597,10 @@ test("event2message: reply preview can guess custom emoji based on the name if i content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> <:hippo:230201364309868544>" + "\nreply", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1539,7 +1647,10 @@ test("event2message: reply preview uses emoji title text when replying to an unk content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> :svkftngur_gkdne:" + "\nreply", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1586,7 +1697,10 @@ test("event2message: reply preview ignores garbage image", async t => { content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + "\n> I am having a nice day" + "\nreply", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1632,7 +1746,10 @@ test("event2message: reply to empty message doesn't show an extra line or anythi username: "cadence [they]", content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**" + "\nreply", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1712,7 +1829,10 @@ test("event2message: editing a rich reply to a sim user", async t => { content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + "\n> Slow news day." + "\nEditing this reply, which is also a test", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } } }], messagesToSend: [] @@ -1765,7 +1885,10 @@ test("event2message: editing a plaintext body message", async t => { 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" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } } }], messagesToSend: [] @@ -1817,13 +1940,19 @@ test("event2message: editing a plaintext message to be longer", async t => { message: { content: "aaaaaaaaa ".repeat(198) + "well, I guess it's", username: "cadence [they]", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } } }], messagesToSend: [{ content: "no longer brand new... it's existed for mere seconds..." + ("aaaaaaaaa ".repeat(20)).slice(0, -1), username: "cadence [they]", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -1873,7 +2002,10 @@ test("event2message: editing a plaintext message to be shorter", async t => { 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" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } } }], messagesToSend: [] @@ -1931,7 +2063,10 @@ test("event2message: editing a formatted body message", async t => { 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" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } } }], messagesToSend: [] @@ -1985,7 +2120,10 @@ test("event2message: rich reply to a matrix user's long message with formatting" content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + "\n> i should have a little happy test list bold em..." + "\n**no you can't!!!**", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2044,7 +2182,10 @@ test("event2message: rich reply to an image", async t => { username: "cadence [they]", content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + "\nCaught in 8K UHD VR QLED Epic Edition", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2097,7 +2238,10 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + "\n> [spoiler] cw crossword spoilers you'll never..." + "\nomg NO WAY!!", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2149,7 +2293,10 @@ test("event2message: with layered rich replies, the preview should only be the r content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + "\n> two" + "\nthree", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2203,7 +2350,10 @@ test("event2message: if event is a reply and starts with a quote, they should be + "\n> i have a feeling that clients are meant to strip..." + "\n" + "\n> To strip the fallback on the `body`, the client should iterate over each line of the string, removing any lines that start with the fallback prefix (\"> “, including the space, without quotes) and stopping when a line is encountered without the prefix. This prefix is known as the “fallback prefix sequence”.", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2258,7 +2408,10 @@ test("event2message: rich reply to a deleted event", async t => { username: "Ampflower 🌺", content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Ampflower 🌺** (in reply to a deleted message)" + "\nHuh it did the same thing here too", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2287,7 +2440,10 @@ test("event2message: raw mentioning discord users in plaintext body works", asyn messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> what do you think?", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2318,7 +2474,10 @@ test("event2message: raw mentioning discord users in formatted body works", asyn messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> what do you think?", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2349,7 +2508,10 @@ test("event2message: mentioning discord users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just <@114147806469554185> testing mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2380,7 +2542,10 @@ test("event2message: mentioning discord users works when URL encoded", async t = messagesToSend: [{ username: "cadence [they]", content: "<@771520384671416320> a sample message", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2411,7 +2576,10 @@ test("event2message: mentioning PK discord users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just **@Azalea &flwr; 🌺** (<@196188877885538304>) testing mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2442,7 +2610,10 @@ test("event2message: mentioning matrix users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just [@▲]() testing mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2469,7 +2640,10 @@ test("event2message: mentioning matrix users works even when Element disambiguat messagesToSend: [{ username: "cadence [they]", content: "[@unascribed]() if you want to run some experimental software, `11864f80cf` branch of OOYE has _vastly_ improved handling of PluralKit users. feel free to try it out, if you find bugs I'd appreciate you letting me know (just tag me at the place in chat where something went wrong)", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2500,7 +2674,10 @@ test("event2message: mentioning bridged rooms works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2529,7 +2706,10 @@ test("event2message: mentioning bridged rooms works (plaintext body)", async t = messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2558,7 +2738,10 @@ test("event2message: mentioning known bridged events works (plaintext body)", as messagesToSend: [{ username: "cadence [they]", content: "it was uploaded earlier in https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020, take a look!", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2589,7 +2772,10 @@ test("event2message: mentioning known bridged events works (partially formatted messagesToSend: [{ username: "cadence [they]", content: "it was uploaded earlier in https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2620,7 +2806,10 @@ test("event2message: mentioning known bridged events works (formatted body)", as messagesToSend: [{ username: "cadence [they]", content: "it was uploaded earlier in https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2663,7 +2852,10 @@ test("event2message: mentioning unknown bridged events can approximate with time messagesToSend: [{ username: "cadence [they]", content: "it was uploaded years ago in https://discord.com/channels/497159726455455754/497161350934560778/753895613661184000", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2705,7 +2897,10 @@ test("event2message: mentioning events falls back to original link when server d messagesToSend: [{ username: "cadence [they]", content: "it was uploaded years ago in [amanda-spam]()", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2746,7 +2941,10 @@ test("event2message: mentioning events falls back to original link when the chan messagesToSend: [{ username: "cadence [they]", content: "it was uploaded years ago in [ex-room-doesnt-exist-any-more]()", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2773,7 +2971,10 @@ test("event2message: link to event in an unknown room (href link)", async t => { messagesToSend: [{ username: "cadence [they]", content: "ah yeah, here's where the bug was reported: ", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2800,7 +3001,10 @@ test("event2message: link to event in an unknown room (bare link)", async t => { messagesToSend: [{ username: "cadence [they]", content: "PK API failure, tho idk how you'd handle that ", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2825,7 +3029,10 @@ test("event2message: link to event in an unknown room (plaintext)", async t => { messagesToSend: [{ username: "cadence [they]", content: "ah yeah, here's where the bug was reported: ", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2856,7 +3063,10 @@ test("event2message: colon after mentions is stripped", async t => { messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> hey, I'm just [@▲]() testing mentions", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2898,7 +3108,10 @@ test("event2message: caches the member if the member is not known", async t => { messagesToSend: [{ username: "should_be_newly_cached", content: "testing the member state cache", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/this_is_the_avatar" + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/this_is_the_avatar", + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2943,7 +3156,10 @@ test("event2message: skips caching the member if the member does not exist, some messagesToSend: [{ username: "not_real", content: "should honestly never happen", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -2987,7 +3203,10 @@ test("event2message: overly long usernames are shifted into the message content" messagesToSend: [{ username: "I am BLACK I am WHITE I am SHORT I am LONG I am EVERYTHING YOU THINK IS", content: "**IMPORTANT and I DON'T MATTER**\ntesting the member state cache", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -3020,7 +3239,10 @@ test("event2message: overly long usernames are not treated specially when the ms messagesToSend: [{ username: "I am BLACK I am WHITE I am SHORT I am LONG I am EVERYTHING YOU THINK IS", content: "\\* I am BLACK I am WHITE I am SHORT I am LONG I am EVERYTHING YOU THINK IS IMPORTANT and I DON'T MATTER looks at the start of the message", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -3319,7 +3541,10 @@ test("event2message: static emojis work", async t => { messagesToSend: [{ username: "cadence [they]", content: "<:hippo:230201364309868544>", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -3346,7 +3571,10 @@ test("event2message: animated emojis work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -3373,7 +3601,10 @@ test("event2message: unknown emojis in the middle are linked", async t => { messagesToSend: [{ username: "cadence [they]", content: "a [:ms_robot_grin:](https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy) b", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) @@ -3419,7 +3650,10 @@ test("event2message: guessed @mentions in plaintext may join members to mention" messagesToSend: [{ username: "cadence [they]", content: "hey <@321876634777218072>, what food would you like to order?", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }], ensureJoined: [subtext.user] } @@ -3469,7 +3703,10 @@ test("event2message: guessed @mentions in formatted body may join members to men messagesToSend: [{ username: "cadence [they]", content: "**_HEY <@321876634777218072>, WHAT FOOD WOULD YOU LIKE TO ORDER??_**", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }], ensureJoined: [subtext.user] } @@ -3509,7 +3746,10 @@ test("event2message: guessed @mentions feature will not activate on links or cod messagesToSend: [{ username: "cadence [they]", content: "in link [view timeline](https://example.com/social/@subtext) in autolink https://example.com/social/@subtext in pre-code```\n@subtext\n```", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }], ensureJoined: [] } @@ -3536,7 +3776,10 @@ test("event2message: guessed @mentions work with other matrix bridge old users", messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> <@176943908762006200> back me up on this sentiment, if not necessarily the phrasing", - avatar_url: undefined + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } }], ensureJoined: [] // we already think it worked on Matrix side due to the pill, so no need for the OOYE sim user to join the room to indicate success. } From f5ffc09fab8f589234d30d70cf204393fc59d692 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 16:23:23 +1300 Subject: [PATCH 012/346] Convert @room to @everyone using permissions --- m2d/actions/channel-webhook.js | 2 +- m2d/converters/event-to-message.js | 45 +++++-- m2d/converters/event-to-message.test.js | 168 ++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 14 deletions(-) diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 52b4095..d52f174 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -57,7 +57,7 @@ async function withWebhook(channelID, callback) { */ async function sendMessageWithWebhook(channelID, data, threadID) { const result = await withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID}) }) return result } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 8da3004..87fadc8 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -384,19 +384,35 @@ async function handleRoomOrMessageLinks(input, di) { /** * @param {string} content + * @param {string} senderMxid + * @param {string} roomID * @param {DiscordTypes.APIGuild} guild * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di */ -async function checkWrittenMentions(content, guild, di) { +async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ if (writtenMentionMatch) { - const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]}) - if (results[0]) { - assert(results[0].user) - return { - // @ts-ignore - typescript doesn't know about indices yet - content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), - ensureJoined: results[0].user + if (writtenMentionMatch[1] === "room") { // convert @room to @everyone + const powerLevels = await di.api.getStateEvent(roomID, "m.room.power_levels", "") + const userPower = powerLevels.users?.[senderMxid] || 0 + if (userPower >= powerLevels.notifications?.room) { + return { + // @ts-ignore - typescript doesn't know about indices yet + content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]), + ensureJoined: [], + allowedMentionsParse: ["everyone"] + } + } + } else { + const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]}) + if (results[0]) { + assert(results[0].user) + return { + // @ts-ignore - typescript doesn't know about indices yet + content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), + ensureJoined: [results[0].user], + allowedMentionsParse: [] + } } } } @@ -427,6 +443,7 @@ const attachmentEmojis = new Map([ async function eventToMessage(event, guild, di) { let displayName = event.sender let avatarURL = undefined + const allowedMentionsParse = ["users", "roles"] /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" @@ -656,10 +673,11 @@ async function eventToMessage(event, guild, di) { for (; node; node = node.nextSibling) { // Check written mentions if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) { - const result = await checkWrittenMentions(node.nodeValue, guild, di) + const result = await checkWrittenMentions(node.nodeValue, event.sender, event.room_id, guild, di) if (result) { node.nodeValue = result.content - ensureJoined.push(result.ensureJoined) + ensureJoined.push(...result.ensureJoined) + allowedMentionsParse.push(...result.allowedMentionsParse) } } // Check for incompatible backticks in code blocks @@ -727,10 +745,11 @@ async function eventToMessage(event, guild, di) { content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews - const result = await checkWrittenMentions(content, guild, di) + const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di) if (result) { content = result.content - ensureJoined.push(result.ensureJoined) + ensureJoined.push(...result.ensureJoined) + allowedMentionsParse.push(...result.allowedMentionsParse) } // Markdown needs to be escaped, though take care not to escape the middle of links @@ -787,7 +806,7 @@ async function eventToMessage(event, guild, di) { const messages = chunks.map(content => ({ content, allowed_mentions: { - parse: ["users", "roles"] + parse: allowedMentionsParse }, username: displayNameShortened, avatar_url: avatarURL diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 3366d18..90da51c 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -3786,6 +3786,174 @@ test("event2message: guessed @mentions work with other matrix bridge old users", ) }) +test("event2message: @room converts to @everyone and is allowed when the room doesn't restrict who can use it (plaintext body)", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "@room dinner's ready", + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {}, + notifications: { + room: 0 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@everyone dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles", "everyone"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room converts to @everyone but is not allowed when the room restricts who can use it", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "@room dinner's ready" + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {}, + notifications: { + room: 20 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@room dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room converts to @everyone and is allowed if the user has sufficient power to use it", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "@room dinner's ready" + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 20 + }, + notifications: { + room: 20 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@everyone dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles", "everyone"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room in the middle of a link is not converted", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `https://github.com/@room/repositories https://github.com/@room/repositories` + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "https://github.com/@room/repositories https://github.com/@room/repositories", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }], + ensureJoined: [] + } + ) +}) + slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet", async t => { const messages = await eventToMessage({ type: "m.room.message", From 08c01e866419a48096f8ccfe41b00bc92fd06fe2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 8 Mar 2024 12:56:51 +1300 Subject: [PATCH 013/346] Update dependencies --- package-lock.json | 527 +--------------------------------------------- package.json | 4 +- 2 files changed, 9 insertions(+), 522 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e45796..49c9dc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", - "giframe": "github:cloudrac3r/giframe#v0.4.1", + "giframe": "github:cloudrac3r/giframe#v0.4.2", "heatsync": "^2.4.1", "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", @@ -40,7 +40,7 @@ "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^10.3.0", + "supertape": "^10.4.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" } }, @@ -547,22 +547,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -583,21 +567,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -1092,38 +1061,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -1148,23 +1085,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1289,26 +1209,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1503,15 +1403,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -1583,15 +1474,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1641,8 +1523,8 @@ } }, "node_modules/giframe": { - "version": "0.3.0", - "resolved": "git+ssh://git@github.com/cloudrac3r/giframe.git#1630f4d3b2bf5acd197409c85edd11e0da72d0a1", + "version": "0.4.2", + "resolved": "git+ssh://git@github.com/cloudrac3r/giframe.git#39b9d9af4184ea9df72c0ccd4db96da51bd1082c", "license": "MIT" }, "node_modules/github-from-package": { @@ -1711,15 +1593,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1762,21 +1635,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -1880,20 +1738,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1902,83 +1746,11 @@ "node": ">= 0.10" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -1991,21 +1763,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2015,128 +1772,6 @@ "node": ">=8" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2487,49 +2122,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2678,15 +2270,6 @@ "node": ">=14.19.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -2890,24 +2473,6 @@ "node": ">= 6" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3095,21 +2660,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3307,18 +2857,6 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-head": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-head/-/stream-head-2.0.2.tgz", @@ -3493,9 +3031,9 @@ } }, "node_modules/supertape": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.3.0.tgz", - "integrity": "sha512-z/HQbwB1+UMpmCQZXxxSU2zhzc+0yQq1Q9naCpyVDcRr2/cO/Bo3YhbSXJpEPVbvGy3BADl+vt5cfbXRJYFO0g==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.4.0.tgz", + "integrity": "sha512-mQbcPU3jyDQsbncbS2flHELhZ7qEIF9q4Swnn/0782aN+rMQBzcJR3yapVn+9k3pUoUi64IcTaGxs24WSLDCvQ==", "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0", @@ -3510,7 +3048,6 @@ "@supertape/formatter-time": "^1.0.0", "@supertape/operator-stub": "^3.0.0", "cli-progress": "^3.8.2", - "deep-equal": "^2.0.3", "fullstore": "^3.0.0", "glob": "^10.3.10", "jest-diff": "^29.0.1", @@ -3874,56 +3411,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", - "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index bff4724..be55cf4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", - "giframe": "github:cloudrac3r/giframe#v0.4.1", + "giframe": "github:cloudrac3r/giframe#v0.4.2", "heatsync": "^2.4.1", "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", @@ -46,7 +46,7 @@ "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^10.3.0", + "supertape": "^10.4.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" }, "scripts": { From 955310b759dbcd8b423d4d9290d74ba8e6917f56 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 15 Mar 2024 15:52:20 +1300 Subject: [PATCH 014/346] Set sim power a little bit better I should probably change this to use kstate. --- d2m/actions/register-user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 68b1a88..6b688d4 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -198,7 +198,8 @@ async function syncUser(user, member, channel, guild, roomID) { // Update power levels const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "") mixin(powerLevelsStateContent, {users: {[mxid]: powerLevel}}) - api.sendState(roomID, "m.room.power_levels", "", powerLevelsStateContent) + if (powerLevel === 0) delete powerLevelsStateContent.users[mxid] // keep the event compact + await api.sendState(roomID, "m.room.power_levels", "", powerLevelsStateContent) // Update cached hash db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) } From d01c888d02180a30ee8313122ac2f5744dd356ad Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 15 Mar 2024 15:54:13 +1300 Subject: [PATCH 015/346] Support embed generate MESSAGE_UPDATE events --- d2m/converters/edit-to-changes.js | 73 +++++++++++++++----------- d2m/converters/edit-to-changes.test.js | 25 +++++++++ d2m/converters/message-to-event.js | 39 +++++++------- test/data.js | 25 +++++++++ test/ooye-test-data.sql | 6 ++- 5 files changed, 117 insertions(+), 51 deletions(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 08f787c..ef170ca 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -3,13 +3,22 @@ const assert = require("assert").strict const passthrough = require("../../passthrough") -const {discord, sync, db, select, from} = passthrough +const {sync, select, from} = passthrough /** @type {import("./message-to-event")} */ const messageToEvent = sync.require("../converters/message-to-event") -/** @type {import("../actions/register-user")} */ -const registerUser = sync.require("../actions/register-user") -/** @type {import("../actions/create-room")} */ -const createRoom = sync.require("../actions/create-room") + +function eventCanBeEdited(ev) { + // Discord does not allow files, images, attachments, or videos to be edited. + if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") { + return false + } + // Discord does not allow stickers to be edited. + if (ev.old.event_type === "m.sticker") { + return false + } + // Anything else is fair game. + return true +} /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message @@ -19,12 +28,16 @@ const createRoom = sync.require("../actions/create-room") * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ async function editToChanges(message, guild, api) { + // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. + + const isGeneratedEmbed = !("content" in message) + // Figure out what events we will be replacing const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() assert(roomID) /** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */ - const senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null + const senderMxid = message.author && from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() @@ -48,7 +61,8 @@ async function editToChanges(message, guild, api) { let eventsToRedact = [] /** 3. Events that are present in the new version only, and should be sent as new, with references back to the context */ let eventsToSend = [] - // 4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. This is represented as nothing. + /** 4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. */ + let unchangedEvents = [] function shift() { newFallbackContent.shift() @@ -81,22 +95,35 @@ async function editToChanges(message, guild, api) { shift() } // Anything remaining in oldEventRows is present in the old version only and should be redacted. - eventsToRedact = oldEventRows + eventsToRedact = oldEventRows.map(e => ({old: e})) + + // If this is a generated embed update, only allow the embeds to be updated, since the system only sends data about events. Ignore changes to other things. + if (isGeneratedEmbed) { + unchangedEvents.push(...eventsToRedact.filter(e => e.old.event_subtype !== "m.notice")) // Move them from eventsToRedact to unchangedEvents. + eventsToRedact = eventsToRedact.filter(e => e.old.event_subtype === "m.notice") + } + + // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! + // (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) + // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed. + unchangedEvents.push(...eventsToReplace.filter(ev => !eventCanBeEdited(ev))) // Move them from eventsToRedact to unchangedEvents. + eventsToReplace = eventsToReplace.filter(eventCanBeEdited) // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. /** @type {({column: string, eventID: string} | {column: string, nextEvent: true})[]} */ const promotions = [] for (const column of ["part", "reaction_part"]) { + const candidatesForParts = unchangedEvents.concat(eventsToReplace) // If no events with part = 0 exist (or will exist), we need to do some management. - if (!eventsToReplace.some(e => e.old[column] === 0)) { - if (eventsToReplace.length) { + if (!candidatesForParts.some(e => e.old[column] === 0)) { + if (candidatesForParts.length) { // We can choose an existing event to promote. Bigger order is better. - const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.event_subtype === "m.text") - eventsToReplace.sort((a, b) => order(b) - order(a)) + const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.old.event_subtype === "m.text") + candidatesForParts.sort((a, b) => order(b) - order(a)) if (column === "part") { - promotions.push({column, eventID: eventsToReplace[0].old.event_id}) // part should be the first one + promotions.push({column, eventID: candidatesForParts[0].old.event_id}) // part should be the first one } else { - promotions.push({column, eventID: eventsToReplace[eventsToReplace.length - 1].old.event_id}) // reaction_part should be the last one + promotions.push({column, eventID: candidatesForParts[candidatesForParts.length - 1].old.event_id}) // reaction_part should be the last one } } else { // No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0. @@ -105,24 +132,8 @@ async function editToChanges(message, guild, api) { } } - // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! - // (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) - // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed. - eventsToReplace = eventsToReplace.filter(ev => { - // Discord does not allow files, images, attachments, or videos to be edited. - if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") { - return false - } - // Discord does not allow stickers to be edited. - if (ev.old.event_type === "m.sticker") { - return false - } - // Anything else is fair game. - return true - }) - // Removing unnecessary properties before returning - eventsToRedact = eventsToRedact.map(e => e.event_id) + eventsToRedact = eventsToRedact.map(e => e.old.event_id) eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promotions} diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 04f5568..1004c34 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -235,3 +235,28 @@ test("edit2changes: promotes the text event when multiple rows have part = 1 (sh } ]) }) + +test("edit2changes: generated embed", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToReplace, []) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| via hthrflwrs on cohost" + + "\n| \n| ## This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO https://cohost.org/jkap/post/4794219-empty" + + "\n| \n| 1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:" + + "\n| \n| * Both players draw eight cards" + + "\n| * Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand" + + "\n| * Both players present their best five-or-less-card pok...", + format: "org.matrix.custom.html", + formatted_body: `

hthrflwrs on cohost` + + `

This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO` + + `

1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:` + + `

  • Both players draw eight cards` + + `
  • Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand` + + `
  • Both players present their best five-or-less-card pok...

`, + "m.mentions": {} + }]) + t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet. +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 3ede0a4..518d041 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -480,32 +480,35 @@ async function messageToEvent(message, guild, options = {}, di) { message.content = "changed the channel name to **" + message.content + "**" } - // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. - const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] - if (matches.length && matches.some(m => m[1].match(/[a-z]/i) && m[1] !== "everyone" && m[1] !== "here")) { - const writtenMentionsText = matches.map(m => m[1].toLowerCase()) - const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() - 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.display_name || localpart[1] - if (writtenMentionsText.includes(localpart[1].toLowerCase()) || writtenMentionsText.includes(displayName.toLowerCase())) addMention(mxid) + + if (message.content) { + // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. + const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] + if (matches.length && matches.some(m => m[1].match(/[a-z]/i) && m[1] !== "everyone" && m[1] !== "here")) { + const writtenMentionsText = matches.map(m => m[1].toLowerCase()) + const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() + 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.display_name || localpart[1] + if (writtenMentionsText.includes(localpart[1].toLowerCase()) || writtenMentionsText.includes(displayName.toLowerCase())) addMention(mxid) + } } } - } - // Text content appears first - if (message.content) { + // Text content appears first const {body, html} = await transformContent(message.content) await addTextEvent(body, html, msgtype, {scanMentions: true}) } // Then attachments - const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions))) - events.push(...attachmentEvents) + if (message.attachments) { + const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions))) + events.push(...attachmentEvents) + } // Then embeds for (const embed of message.embeds || []) { diff --git a/test/data.js b/test/data.js index c165322..17b92cf 100644 --- a/test/data.js +++ b/test/data.js @@ -3469,6 +3469,31 @@ module.exports = { } ], guild_id: "112760669178241024" + }, + embed_generated_social_media_image: { + channel_id: "112760669178241024", + embeds: [ + { + color: 8594767, + description: "1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:\n\n * Both players draw eight cards\n * Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand\n * Both players present their best five-or-less-card pok...", + provider: { + name: "hthrflwrs on cohost" + }, + thumbnail: { + height: 1587, + placeholder: "GpoKP5BJZphshnhwmmmYlmh3l7+m+mwJ", + placeholder_version: 1, + proxy_url: "https://images-ext-2.discordapp.net/external/9vTXIzlXU4wyUZvWfmlmQkck8nGLUL-A090W4lWsZ48/https/staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png", + url: "https://staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png", + width: 1644 + }, + title: "This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO", + type: "link", + url: "https://cohost.org/jkap/post/4794219-empty" + } + ], + guild_id: "112760669178241024", + id: "1210387798297682020" } }, special_message: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 573d7a4..35368f4 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -53,7 +53,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1158842413025071135', '176333891320283136'), ('1197612733600895076', '112760669178241024'), ('1202543413652881428', '1160894080998461480'), -('1207486471489986620', '1160894080998461480'); +('1207486471489986620', '1160894080998461480'), +('1210387798297682020', '112760669178241024'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -87,7 +88,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo', 'm.room.message', 'm.text', '1158842413025071135', 0, 0, 1), ('$7tJoMw1h44n2gxgLUE1T_YinGrLbK0x-TDY1z6M7GBw', 'm.room.message', 'm.text', '1197612733600895076', 0, 0, 1), ('$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU', 'm.room.message', 'm.text', '1202543413652881428', 0, 0, 0), -('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0); +('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0), +('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), From 23d85547f34b7998a4eddeebb3051aff7de1125c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 17 Mar 2024 01:07:50 +1300 Subject: [PATCH 016/346] Send generated embeds as original user --- d2m/converters/edit-to-changes.js | 15 ++++++++++++++- d2m/converters/edit-to-changes.test.js | 12 +++++++++++- d2m/event-dispatcher.js | 3 ++- scripts/capture-message-update-events.js | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index ef170ca..dd8e39f 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -6,6 +6,8 @@ const passthrough = require("../../passthrough") const {sync, select, from} = passthrough /** @type {import("./message-to-event")} */ const messageToEvent = sync.require("../converters/message-to-event") +/** @type {import("../../m2d/converters/utils")} */ +const utils = sync.require("../../m2d/converters/utils") function eventCanBeEdited(ev) { // Discord does not allow files, images, attachments, or videos to be edited. @@ -37,7 +39,18 @@ async function editToChanges(message, guild, api) { const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() assert(roomID) /** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */ - const senderMxid = message.author && from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null + let senderMxid = null + if (message.author) { + senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null + } else { + // Should be a system generated embed. We want the embed to be sent by the same user who sent the message, so that the messages get grouped in most clients. + const eventID = select("event_message", "event_id", {message_id: message.id}).pluck().get() + assert(eventID) // this should have been checked earlier in a calling function + const event = await api.getEvent(roomID, eventID) + if (utils.eventSenderIsFromDiscord(event.sender)) { + senderMxid = event.sender + } + } const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 1004c34..b00a2c4 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -237,7 +237,15 @@ test("edit2changes: promotes the text event when multiple rows have part = 1 (sh }) test("edit2changes: generated embed", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, {}) + let called = 0 + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0") + return {sender: "@_ooye_cadence:cadence.moe"} + } + }) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToReplace, []) t.deepEqual(eventsToSend, [{ @@ -259,4 +267,6 @@ test("edit2changes: generated embed", async t => { "m.mentions": {} }]) t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet. + t.equal(senderMxid, "@_ooye_cadence:cadence.moe") + t.equal(called, 1) }) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 2c4dcd5..54d858a 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -268,7 +268,8 @@ module.exports = { // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. // If the message content is a string then it includes all interesting fields and is meaningful. - if (typeof data.content === "string") { + // Otherwise, if there are embeds, then the system generated URL preview embeds. + if (typeof data.content === "string" || "embeds" in data) { /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore const message = data diff --git a/scripts/capture-message-update-events.js b/scripts/capture-message-update-events.js index f345d3f..671b062 100644 --- a/scripts/capture-message-update-events.js +++ b/scripts/capture-message-update-events.js @@ -37,7 +37,7 @@ passthrough.discord = discord })() const events = new sqlite("scripts/events.db") -const sql = "INSERT INTO \"update\" (json, " + interestingFields.join(", ") + ") VALUES (" + "?".repeat(interestingFields.length + 1).split("").join(", ") + ")" +const sql = "INSERT INTO update_event (json, " + interestingFields.join(", ") + ") VALUES (" + "?".repeat(interestingFields.length + 1).split("").join(", ") + ")" console.log(sql) const prepared = events.prepare(sql) From c615ea1e6162a1f193f29e52d277d5a01f9fecdd Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 19 Mar 2024 15:06:31 +1300 Subject: [PATCH 017/346] Reflect immediately generated link embeds --- m2d/actions/send-event.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 6c0448f..d35c558 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -17,6 +17,8 @@ const eventToMessage = sync.require("../converters/event-to-message") const api = sync.require("../../matrix/api") /** @type {import("../../d2m/actions/register-user")} */ const registerUser = sync.require("../../d2m/actions/register-user") +/** @type {import("../../d2m/actions/edit-message")} */ +const editMessage = sync.require("../../d2m/actions/edit-message") /** @type {import("../actions/emoji-sheet")} */ const emojiSheet = sync.require("../actions/emoji-sheet") @@ -111,6 +113,22 @@ async function sendEvent(event) { eventPart = 1 messageResponses.push(messageResponse) + + /* + If the Discord system has a cached link preview embed for one of the links just sent, + it will be instantly added as part of `embeds` and there won't be a MESSAGE_UPDATE. + To reflect the generated embed back to Matrix, we pretend the message was updated right away. + */ + const sentEmbedsCount = message.embeds?.length || 0 + if (messageResponse.embeds.length > sentEmbedsCount) { + // @ts-ignore this is a valid message edit payload + editMessage.editMessage({ // not awaiting because requests to Matrix shouldn't block requests to Discord + id: messageResponse.channel_id, + channel_id: messageResponse.channel_id, + guild_id: guild.id, + embeds: messageResponse.embeds + }, guild, null) + } } for (const user of ensureJoined) { From bce3d0f2c9b622a9aa9d564ee69a5c833e70d51d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 19 Mar 2024 21:58:48 +1300 Subject: [PATCH 018/346] Fix reflecting generated embeds --- m2d/actions/send-event.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index d35c558..4422ad3 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -90,6 +90,7 @@ async function sendEvent(event) { })) let eventPart = 0 // 0 is primary, 1 is supporting + const pendingEdits = [] /** @type {DiscordTypes.APIMessage[]} */ const messageResponses = [] @@ -121,13 +122,16 @@ async function sendEvent(event) { */ const sentEmbedsCount = message.embeds?.length || 0 if (messageResponse.embeds.length > sentEmbedsCount) { - // @ts-ignore this is a valid message edit payload - editMessage.editMessage({ // not awaiting because requests to Matrix shouldn't block requests to Discord - id: messageResponse.channel_id, - channel_id: messageResponse.channel_id, - guild_id: guild.id, - embeds: messageResponse.embeds - }, guild, null) + // not awaiting here because requests to Matrix shouldn't block requests to Discord + pendingEdits.push(() => + // @ts-ignore this is a valid message edit payload + editMessage.editMessage({ + id: messageResponse.id, + channel_id: messageResponse.channel_id, + guild_id: guild.id, + embeds: messageResponse.embeds + }, guild, null) + ) } } @@ -135,6 +139,8 @@ async function sendEvent(event) { registerUser.ensureSimJoined(user, event.room_id) } + await Promise.all(pendingEdits.map(f => f())) // `await` will propagate any errors during editing + return messageResponses } From 0deb4155115e22646718b1cee69b866800bfdffe Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 19 Mar 2024 22:15:44 +1300 Subject: [PATCH 019/346] Don't update profile data of the bridge bot --- d2m/actions/send-message.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index a1290e0..346ac0d 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -29,7 +29,9 @@ async function sendMessage(message, channel, guild, row) { let senderMxid = null if (!dUtils.isWebhookMessage(message)) { - if (message.member) { // available on a gateway message create event + if (message.author.id === discord.application.id) { + // no need to sync the bot's own user + } else if (message.member) { // available on a gateway message create event senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID) } else { // well, good enough... senderMxid = await registerUser.ensureSimJoined(message.author, roomID) From 566b2a9d9e3ddfec02725c7713c74829a5827ecf Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 23 Mar 2024 18:39:37 +1300 Subject: [PATCH 020/346] Move bridge bot to its real ID in the database --- .../0011-move-bridge-bot-to-real-id.up.js | 16 ++++++++++++++++ scripts/seed.js | 12 +++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 db/migrations/0011-move-bridge-bot-to-real-id.up.js diff --git a/db/migrations/0011-move-bridge-bot-to-real-id.up.js b/db/migrations/0011-move-bridge-bot-to-real-id.up.js new file mode 100644 index 0000000..1808fcd --- /dev/null +++ b/db/migrations/0011-move-bridge-bot-to-real-id.up.js @@ -0,0 +1,16 @@ +/* + a. If the bridge bot sim already has the correct ID: + - No rows updated. + + b. If the bridge bot sim has the wrong ID but there's no duplicate: + - One row updated. + + c. If the bridge bot sim has the wrong ID and there's a duplicate: + - One row updated (replaces an existing row). +*/ + +module.exports = async function(db) { + const config = require("../../config") + const id = Buffer.from(config.discordToken.split(".")[0], "base64").toString() + db.prepare("UPDATE OR REPLACE sim SET user_id = ? WHERE user_id = '0'").run(id) +} diff --git a/scripts/seed.js b/scripts/seed.js index 1a21490..2ab3e02 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -53,17 +53,19 @@ async function uploadAutoEmoji(guild, name, filename) { const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}` // ensure registration is correctly set... - assert(reg.sender_localpart.startsWith(reg.ooye.namespace_prefix)) // appservice's localpart must be in the namespace it controls - assert(utils.eventSenderIsFromDiscord(mxid)) // appservice's mxid must be in the namespace it controls - assert(reg.ooye.server_origin.match(/^https?:\/\//)) // must start with http or https - assert.notEqual(reg.ooye.server_origin.slice(-1), "/") // must not end in slash + assert(reg.sender_localpart.startsWith(reg.ooye.namespace_prefix), "appservice's localpart must be in the namespace it controls") + assert(utils.eventSenderIsFromDiscord(mxid), "appservice's mxid must be in the namespace it controls") + assert(reg.ooye.server_origin.match(/^https?:\/\//), "server origin must start with http or https") + assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash") + const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString() + assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") console.log("✅ Configuration looks good...") // database ddl... await migrate.migrate(db) // add initial rows to database, like adding the bot to sim... - db.prepare("INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run("0", reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), reg.sender_localpart, mxid) + db.prepare("INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(botID, reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), reg.sender_localpart, mxid) console.log("✅ Database is ready...") From 7a00b95883a58bbe90f36748a28f9f4039a25fd1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 23 Mar 2024 21:26:42 +1300 Subject: [PATCH 021/346] Put < > around ALL the matrix.to links --- m2d/converters/event-to-message.js | 2 +- m2d/converters/event-to-message.test.js | 34 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 87fadc8..f9403f9 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -724,7 +724,7 @@ async function eventToMessage(event, guild, di) { content = turndownService.turndown(root) // Put < > around any surviving matrix.to links to hide the URL previews - content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/, "<$&>") + content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/g, "<$&>") // It's designed for commonmark, we need to replace the space-space-newline with just newline content = content.replace(/ \n/g, "\n") diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 90da51c..9035ddb 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -2619,6 +2619,40 @@ test("event2message: mentioning matrix users works", async t => { ) }) +test("event2message: multiple mentions are both escaped", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `@cadence:cadence.moe can you kick my old account over there @amyiscoolz:matrix.atiusamy.com` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "[@cadence:cadence.moe]() can you kick my old account over there [@amyiscoolz:matrix.atiusamy.com]()", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: mentioning matrix users works even when Element disambiguates the user", async t => { t.deepEqual( await eventToMessage({ From ff7af39802e34ac3972396cda67d0442dabf00c3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 25 Mar 2024 18:05:19 +1300 Subject: [PATCH 022/346] Exclude generated embeds for discord.com --- .../message-to-event.embeds.test.js | 34 ++++++++++++++++ d2m/converters/message-to-event.js | 4 ++ test/data.js | 40 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index fc02a3f..61a0822 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -282,3 +282,37 @@ test("message2event embeds: youtube video", async t => { "m.mentions": {} }]) }) + +test("message2event embeds: if discord creates an embed preview for a discord channel link, don't copy that embed", async t => { + const events = await messageToEvent(data.message_with_embeds.discord_server_included_punctuation_bad_discord, data.guild.general, {}, { + api: { + async getStateEvent(roomID, type, key) { + t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, + async getJoinedMembers(roomID) { + t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe") + return { + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} + } + } + } + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "(test https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU?via=cadence.moe&via=matrix.org)", + format: "org.matrix.custom.html", + formatted_body: `(test https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU?via=cadence.moe&via=matrix.org)`, + "m.mentions": {} + }]) +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 518d041..45e43dd 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -516,6 +516,10 @@ async function messageToEvent(message, guild, options = {}, di) { continue // Matrix's own URL previews are fine for images. } + if (embed.url?.startsWith("https://discord.com/")) { + continue // If discord creates an embed preview for a discord channel link, don't copy that embed + } + // 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 const rep = new mxUtils.MatrixStringBuilder() diff --git a/test/data.js b/test/data.js index 17b92cf..77b2ada 100644 --- a/test/data.js +++ b/test/data.js @@ -2870,6 +2870,46 @@ module.exports = { } }, webhook_id: "1109360903096369153" + }, + discord_server_included_punctuation_bad_discord: { + id: "1221672425792606349", + type: 0, + content: "(test https://discord.com/channels/1160894080998461480/1160894080998461480/1202543413652881428)", + channel_id: "1160894080998461480", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + premium_type: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "article", + url: "https://discord.com/channels/1160894080998461480/1160894080998461480/1202543413652881428)", + title: "Discord - A New Way to Chat with Friends & Communities", + description: "Discord is the easiest way to communicate over voice, video, and text. Chat, hang out, and stay close with your friends and communities.", + provider: { name: "Discord" }, + content_scan_version: 0 + } + ], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2024-03-25T04:10:03.885000+00:00", + edited_timestamp: null, + flags: 0, + components: [] } }, message_update: { From 642be26313bd69fbb563a3fbe2dabb4679e62f79 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 26 Mar 2024 01:10:29 +1300 Subject: [PATCH 023/346] Enumerate child rooms with hierarchy endpoint --- d2m/actions/create-space.js | 14 +++++++++++--- matrix/api.js | 14 ++++++++++++++ types.d.ts | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index a7390ed..3bad2a1 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -3,6 +3,7 @@ const assert = require("assert").strict const {isDeepStrictEqual} = require("util") const DiscordTypes = require("discord-api-types/v10") +const Ty = require("../../types") const reg = require("../../matrix/read-registration") const passthrough = require("../../passthrough") @@ -181,9 +182,16 @@ async function syncSpaceFully(guildID) { const spaceDiff = ks.diffKState(spaceKState, guildKState) await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) - const childRooms = ks.kstateToState(spaceKState).filter(({type, content}) => { - return type === "m.space.child" && "via" in content - }).map(({state_key}) => state_key) + /** @type {string[]} room IDs */ + let childRooms = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.HierarchyPagination} */ + const res = await api.getHierarchy(spaceID, {from: nextBatch}) + childRooms.push(...res.rooms.map(room => room.room_id)) + nextBatch = res.next_batch + } while (nextBatch) for (const roomID of childRooms) { const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get() diff --git a/matrix/api.js b/matrix/api.js index baa5d96..82b1c10 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -121,6 +121,19 @@ function getJoinedMembers(roomID) { return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`) } +/** + * @param {string} roomID + * @param {{from?: string, limit?: any}} pagination + * @returns {Promise>} + */ +function getHierarchy(roomID, pagination) { + let path = `/client/v1/rooms/${roomID}/hierarchy` + if (!pagination.from) delete pagination.from + if (!pagination.limit) pagination.limit = 50 + path += `?${new URLSearchParams(pagination)}` + return mreq.mreq("GET", path) +} + /** * @param {string} roomID * @param {string} eventID @@ -239,6 +252,7 @@ module.exports.getEventForTimestamp = getEventForTimestamp module.exports.getAllState = getAllState module.exports.getStateEvent = getStateEvent module.exports.getJoinedMembers = getJoinedMembers +module.exports.getHierarchy = getHierarchy module.exports.getRelations = getRelations module.exports.sendState = sendState module.exports.sendEvent = sendEvent diff --git a/types.d.ts b/types.d.ts index 2788f60..68430d9 100644 --- a/types.d.ts +++ b/types.d.ts @@ -257,6 +257,18 @@ export namespace R { export type EventRedacted = { event_id: string } + + export type Hierarchy = { + avatar_url?: string + canonical_alias?: string + children_state: {} + guest_can_join: boolean + join_rule?: string + name?: string + num_joined_members: number + room_id: string + room_type?: string + } } export type Pagination = { @@ -264,3 +276,8 @@ export type Pagination = { next_batch?: string prev_match?: string } + +export type HierarchyPagination = { + rooms: T[] + next_batch?: string +} From 5f0e765934bbd1dd3e5765e19d9624f0356ae9e6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 26 Mar 2024 01:11:13 +1300 Subject: [PATCH 024/346] Bridge forums as spaces --- d2m/actions/create-room.js | 40 +++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index a8d2846..4559ffa 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -59,13 +59,16 @@ function applyKStateDiffToRoom(roomID, kstate) { } /** - * @param {{id: string, name: string, topic?: string?, type: number}} channel + * @param {{id: string, name: string, topic?: string?, type: number, parent_id?: string?}} channel * @param {{id: string}} guild * @param {string | null | undefined} customName */ function convertNameAndTopic(channel, guild, customName) { + // @ts-ignore + const parentChannel = discord.channels.get(channel.parent_id) let channelPrefix = - ( channel.type === DiscordTypes.ChannelType.PublicThread ? "[⛓️] " + ( parentChannel?.type === DiscordTypes.ChannelType.GuildForum ? "" + : channel.type === DiscordTypes.ChannelType.PublicThread ? "[⛓️] " : channel.type === DiscordTypes.ChannelType.PrivateThread ? "[🔒⛓️] " : channel.type === DiscordTypes.ChannelType.GuildVoice ? "[🔊] " : "") @@ -88,9 +91,24 @@ function convertNameAndTopic(channel, guild, customName) { * @param {DiscordTypes.APIGuild} guild */ async function channelToKState(channel, guild) { - const spaceID = await createSpace.ensureSpace(guild) - assert(typeof spaceID === "string") - const privacyLevel = select("guild_space", "privacy_level", {space_id: spaceID}).pluck().get() + // @ts-ignore + const parentChannel = discord.channels.get(channel.parent_id) + /** Used for membership/permission checks. */ + let guildSpaceID + /** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */ + let parentSpaceID + let privacyLevel + if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) { // it's a forum channel's thread, so use a different space to group those threads + guildSpaceID = await createSpace.ensureSpace(guild) + parentSpaceID = await ensureRoom(channel.parent_id) + privacyLevel = select("guild_space", "privacy_level", {space_id: guildSpaceID}).pluck().get() + } else { // otherwise use the guild's space like usual + parentSpaceID = await createSpace.ensureSpace(guild) + guildSpaceID = parentSpaceID + privacyLevel = select("guild_space", "privacy_level", {space_id: parentSpaceID}).pluck().get() + } + assert(typeof parentSpaceID === "string") + assert(typeof guildSpaceID === "string") assert(typeof privacyLevel === "number") const row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get() @@ -114,7 +132,7 @@ async function channelToKState(channel, guild) { join_rule: "restricted", allow: [{ type: "m.room_membership", - room_id: spaceID + room_id: guildSpaceID }] } if (PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel] !== "restricted") { @@ -130,7 +148,7 @@ async function channelToKState(channel, guild) { "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.history_visibility/": {history_visibility}, - [`m.space.parent/${spaceID}`]: { + [`m.space.parent/${parentSpaceID}`]: { via: [reg.ooye.server_name], canonical: true }, @@ -167,7 +185,7 @@ async function channelToKState(channel, guild) { } } - return {spaceID, privacyLevel, channelKState} + return {spaceID: parentSpaceID, privacyLevel, channelKState} } /** @@ -183,6 +201,9 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) { let threadParent = null if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id + let spaceCreationContent = {} + if (channel.type === DiscordTypes.ChannelType.GuildForum) spaceCreationContent = {creation_content: {type: "m.space"}} + // Name and topic can be done earlier in room creation rather than in initial_state // https://spec.matrix.org/latest/client-server-api/#creation const name = kstate["m.room.name/"].name @@ -199,7 +220,8 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) { preset: PRIVACY_ENUMS.PRESET[privacyLevel], // This is closest to what we want, but properties from kstate override it anyway visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel], invite: [], - initial_state: ks.kstateToState(kstate) + initial_state: ks.kstateToState(kstate), + ...spaceCreationContent }) db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) From b6b65992f70622bda27de7d1984f3d0ee0a5f8c5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 14 May 2024 23:07:54 +1200 Subject: [PATCH 025/346] Forward redaction errors to error handler --- m2d/actions/redact.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2d/actions/redact.js b/m2d/actions/redact.js index 26a7142..7569df4 100644 --- a/m2d/actions/redact.js +++ b/m2d/actions/redact.js @@ -16,7 +16,7 @@ async function deleteMessage(event) { db.prepare("DELETE FROM event_message WHERE event_id = ?").run(event.event_id) for (const row of rows) { db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(row.message_id) - discord.snow.channel.deleteMessage(row.channel_id, row.message_id, event.content.reason) + await discord.snow.channel.deleteMessage(row.channel_id, row.message_id, event.content.reason) } } From 1f5865b0d8a14ba56b07cc1d4f94c5e57ed058c4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 14 May 2024 23:09:51 +1200 Subject: [PATCH 026/346] Avoid sending ephemeral messages from Discord --- d2m/event-dispatcher.js | 4 ++++ discord/utils.js | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 54d858a..ec04750 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -244,6 +244,8 @@ module.exports = { if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. } + if (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only! + const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id) if (affected) return @@ -262,6 +264,8 @@ module.exports = { if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. } + if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only! + // Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from. const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return diff --git a/discord/utils.js b/discord/utils.js index 2726a1a..c7045ec 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -101,6 +101,14 @@ function isWebhookMessage(message) { return message.webhook_id && !isInteractionResponse } +/** + * Ephemeral messages can be generated if a slash command is attached to the same bot that OOYE is running on + * @param {DiscordTypes.APIMessage} message + */ +function isEphemeralMessage(message) { + return message.flags & (1 << 6); +} + /** @param {string} snowflake */ function snowflakeToTimestampExact(snowflake) { return Number(BigInt(snowflake) >> 22n) + EPOCH @@ -116,5 +124,6 @@ module.exports.hasPermission = hasPermission module.exports.hasSomePermissions = hasSomePermissions module.exports.hasAllPermissions = hasAllPermissions module.exports.isWebhookMessage = isWebhookMessage +module.exports.isEphemeralMessage = isEphemeralMessage module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact From 07a133eba96871cb93acdb3f133859190e751ec0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 25 May 2024 22:46:28 +1200 Subject: [PATCH 027/346] Fix "...\nhttps://..." corrupting the link --- m2d/converters/event-to-message.js | 11 +++++----- m2d/converters/event-to-message.test.js | 28 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f9403f9..c22e1a8 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -54,16 +54,17 @@ const turndownService = new TurndownService({ */ // @ts-ignore bad type from turndown turndownService.escape = function (string) { - const escapedWords = string.split(" ").map(word => { - if (word.match(/^https?:\/\//)) { - return word + return string.replace(/\s+|\S+/g, part => { // match chunks of spaces or non-spaces + if (part.match(/\s/)) return part // don't process spaces + + if (part.match(/^https?:\/\//)) { + return part } else { return markdownEscapes.reduce(function (accumulator, escape) { return accumulator.replace(escape[0], escape[1]) - }, word) + }, part) } }) - return escapedWords.join(" ") } turndownService.remove("mx-reply") diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 9035ddb..db44393 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -205,6 +205,34 @@ test("event2message: links in plaintext body are not broken", async t => { ) }) +test("event2message: links in plaintext body are not broken when preceded by a newline", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "java redstoners will be like \"I hate bedrock edition redstone!!\" meanwhile java edition:\nhttps://youtu.be/g_ORb7bN3CM" + }, + event_id: "$b1c5gJZfh1gq3zz6UkhI1whJ61JVvgvvzbdSPEYnTbY", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "java redstoners will be like \"I hate bedrock edition redstone!!\" meanwhile java edition:\nhttps://youtu.be/g_ORb7bN3CM", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: links in formatted body where the text & href are the same, just post the link once", async t => { t.deepEqual( await eventToMessage({ From 24a3b9b0f49d5f252ee81104fb884512ebaca42e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 6 Jun 2024 12:12:48 +1200 Subject: [PATCH 028/346] Refactor kstate resource uploading --- d2m/actions/create-room.js | 6 +-- d2m/actions/create-space.js | 18 +++---- d2m/actions/create-space.test.js | 39 ++++++++++++++++ m2d/converters/event-to-message.test.js | 62 ++++++++++++++++++++++++- matrix/kstate.js | 34 +++++++++++++- matrix/kstate.test.js | 51 ++++++++++++++++++-- test/test.js | 4 ++ 7 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 d2m/actions/create-space.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 4559ffa..051e889 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -51,8 +51,8 @@ async function roomToKState(roomID) { * @param {string} roomID * @param {any} kstate */ -function applyKStateDiffToRoom(roomID, kstate) { - const events = ks.kstateToState(kstate) +async function applyKStateDiffToRoom(roomID, kstate) { + const events = await ks.kstateToState(kstate) return Promise.all(events.map(({type, state_key, content}) => api.sendState(roomID, type, state_key, content) )) @@ -220,7 +220,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) { preset: PRIVACY_ENUMS.PRESET[privacyLevel], // This is closest to what we want, but properties from kstate override it anyway visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel], invite: [], - initial_state: ks.kstateToState(kstate), + initial_state: await ks.kstateToState(kstate), ...spaceCreationContent }) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 3bad2a1..dbefd6d 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -45,7 +45,7 @@ async function createSpace(guild, kstate) { creation_content: { type: "m.space" }, - initial_state: ks.kstateToState(kstate) + initial_state: await ks.kstateToState(kstate) }) }) db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID) @@ -57,15 +57,14 @@ async function createSpace(guild, kstate) { * @param {number} privacyLevel */ async function guildToKState(guild, privacyLevel) { - const avatarEventContent = {} - if (guild.icon) { - avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API - } - + assert.equal(typeof privacyLevel, "number") const guildKState = { "m.room.name/": {name: guild.name}, - "m.room.avatar/": avatarEventContent, + "m.room.avatar/": { + $if: guild.icon, + discord_path: file.guildIcon(guild), + url: {$url: file.guildIcon(guild)} + }, "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]}, "m.room.join_rules/": {join_rule: createRoom.PRIVACY_ENUMS.SPACE_JOIN_RULES[privacyLevel]}, @@ -123,7 +122,8 @@ async function _syncSpace(guild, shouldActuallySync) { // don't try to update rooms with custom avatars though const roomsWithCustomAvatars = select("channel_room", "room_id", {}, "WHERE custom_avatar IS NOT NULL").pluck().all() - const childRooms = ks.kstateToState(spaceKState).filter(({type, state_key, content}) => { + const state = await ks.kstateToState(spaceKState) + const childRooms = state.filter(({type, state_key, content}) => { return type === "m.space.child" && "via" in content && !roomsWithCustomAvatars.includes(state_key) }).map(({state_key}) => state_key) diff --git a/d2m/actions/create-space.test.js b/d2m/actions/create-space.test.js new file mode 100644 index 0000000..368bb32 --- /dev/null +++ b/d2m/actions/create-space.test.js @@ -0,0 +1,39 @@ +// @ts-check + +const mixin = require("mixin-deep") +const {guildToKState, ensureSpace} = require("./create-space") +const {kstateStripConditionals, kstateUploadMxc} = require("../../matrix/kstate") +const {test} = require("supertape") +const testData = require("../../test/data") + +const passthrough = require("../../passthrough") +const {db} = passthrough + +test("guild2space: can generate kstate for a guild, passing privacy level 0", async t => { + t.deepEqual( + await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))), + { + "m.room.avatar/": { + discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", + url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + }, + "m.room.guest_access/": { + guest_access: "can_join" + }, + "m.room.history_visibility/": { + history_visibility: "invited" + }, + "m.room.join_rules/": { + join_rule: "invite" + }, + "m.room.name/": { + name: "Psychonauts 3" + }, + "m.room.power_levels/": { + users: { + "@test_auto_invite:example.org": 100 + }, + }, + } + ) +}) diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index db44393..6d750a5 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -584,7 +584,7 @@ test("event2message: code blocks work", async t => { msgtype: "m.text", body: "wrong body", format: "org.matrix.custom.html", - formatted_body: "

preceding

\n
code block\n
\n

following code is inline

\n" + formatted_body: "

preceding

\n
code block\n
\n

following code is inline

" }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", origin_server_ts: 1688301929913, @@ -641,6 +641,66 @@ test("event2message: code block contents are formatted correctly and not escaped ) }) +test("event2message: code blocks use double backtick as delimiter when necessary", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "backtick in ` the middle, backtick at the edge`" + }, + event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", + room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "``backtick in ` the middle``, `` backtick at the edge` ``", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: inline code is converted to code block if it contains both delimiters", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "` one two ``" + }, + event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", + room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "``` ` one two `` ```", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks", async t => { t.deepEqual( await eventToMessage({ diff --git a/matrix/kstate.js b/matrix/kstate.js index 3c4189d..eca9522 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -4,7 +4,12 @@ const assert = require("assert").strict const mixin = require("mixin-deep") const {isDeepStrictEqual} = require("util") -/** Mutates the input. */ +const passthrough = require("../passthrough") +const {sync} = passthrough +/** @type {import("./file")} */ +const file = sync.require("./file") + +/** Mutates the input. Not recursive - can only include or exclude entire state events. */ function kstateStripConditionals(kstate) { for (const [k, content] of Object.entries(kstate)) { // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) @@ -16,9 +21,33 @@ function kstateStripConditionals(kstate) { return kstate } -function kstateToState(kstate) { +/** Mutates the input. Works recursively through object tree. */ +async function kstateUploadMxc(obj) { + const promises = [] + function inner(obj) { + for (const [k, v] of Object.entries(obj)) { + if (v == null || typeof v !== "object") continue + + if (v.$url) { + promises.push( + file.uploadDiscordFileToMxc(v.$url) + .then(mxc => obj[k] = mxc) + ) + } + + inner(v) + } + } + inner(obj) + await Promise.all(promises) + return obj +} + +/** Automatically strips conditionals and uploads URLs to mxc. */ +async function kstateToState(kstate) { const events = [] kstateStripConditionals(kstate) + await kstateUploadMxc(kstate) for (const [k, content] of Object.entries(kstate)) { const slashIndex = k.indexOf("/") assert(slashIndex > 0) @@ -74,6 +103,7 @@ function diffKState(actual, target) { } module.exports.kstateStripConditionals = kstateStripConditionals +module.exports.kstateUploadMxc = kstateUploadMxc module.exports.kstateToState = kstateToState module.exports.stateToKState = stateToKState module.exports.diffKState = diffKState diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js index 239de75..0538450 100644 --- a/matrix/kstate.test.js +++ b/matrix/kstate.test.js @@ -1,5 +1,5 @@ const assert = require("assert") -const {kstateToState, stateToKState, diffKState, kstateStripConditionals} = require("./kstate") +const {kstateToState, stateToKState, diffKState, kstateStripConditionals, kstateUploadMxc} = require("./kstate") const {test} = require("supertape") test("kstate strip: strips false conditions", t => { @@ -21,8 +21,53 @@ test("kstate strip: keeps true conditions while removing $if", t => { }) }) -test("kstate2state: general", t => { - t.deepEqual(kstateToState({ +test("kstateUploadMxc: sets the mxc", async t => { + const input = { + "m.room.avatar/": { + url: {$url: "https://cdn.discordapp.com/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024"}, + test1: { + test2: { + test3: {$url: "https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg"} + } + } + } + } + await kstateUploadMxc(input) + t.deepEqual(input, { + "m.room.avatar/": { + url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", + test1: { + test2: { + test3: "mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR" + } + } + } + }) +}) + +test("kstateUploadMxc and strip: work together", async t => { + const input = { + "m.room.avatar/yes": { + $if: true, + url: {$url: "https://cdn.discordapp.com/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024"} + }, + "m.room.avatar/no": { + $if: false, + url: {$url: "https://cdn.discordapp.com/avatars/320067006521147393/5fc4ad85c1ea876709e9a7d3374a78a1.png?size=1024"} + }, + } + kstateStripConditionals(input) + await kstateUploadMxc(input) + t.deepEqual(input, { + "m.room.avatar/yes": { + url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl" + } + }) +}) + + +test("kstate2state: general", async t => { + t.deepEqual(await kstateToState({ "m.room.name/": {name: "test name"}, "m.room.member/@cadence:cadence.moe": {membership: "join"}, "uk.half-shot.bridge/org.matrix.appservice-irc://irc/epicord.net/#general": {creator: "@cadence:cadence.moe"} diff --git a/test/test.js b/test/test.js index 58459f6..396dea9 100644 --- a/test/test.js +++ b/test/test.js @@ -21,6 +21,9 @@ const reg = require("../matrix/read-registration") reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" reg.ooye.invite = ["@test_auto_invite:example.org"] +reg.id = "baby" // don't actually take authenticated actions on the server +reg.as_token = "baby" +reg.hs_token = "baby" const sync = new HeatSync({watchFS: false}) @@ -117,6 +120,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/read-registration.test") require("../matrix/txnid.test") require("../d2m/actions/create-room.test") + require("../d2m/actions/create-space.test") require("../d2m/actions/register-user.test") require("../d2m/converters/edit-to-changes.test") require("../d2m/converters/emoji-to-key.test") From c9aa5b4c64ac9f47dc75059950aee67e8244b575 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jun 2024 10:21:09 +1200 Subject: [PATCH 029/346] Update heatsync This fixes an issue where matrix spaces would be initially created without a default privacy level. --- package-lock.json | 88 +++++++++++------------------------------------ package.json | 2 +- 2 files changed, 21 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49c9dc3..5132540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "entities": "^4.5.0", "get-stream": "^6.0.1", "giframe": "github:cloudrac3r/giframe#v0.4.2", - "heatsync": "^2.4.1", + "heatsync": "^2.5.3", "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", @@ -155,14 +155,6 @@ "node": ">=8" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -573,9 +565,9 @@ "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" }, "node_modules/backtracker": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-3.3.2.tgz", - "integrity": "sha512-bXosLBp95xGE1kcWRnbG+e+Sw1xCKTT1GMdvaqEk9cGfBQoFPEvdI78fepKIJWFejV4NCl7OLUt8SNvYom/D/w==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-4.0.0.tgz", + "integrity": "sha512-XG2ldN+WDRq9niJMnoZDjLLUnhDOQGhFZc6qZQotN59xj8oOa4KXSCu6YyZQawPqi6gG3HilGFt91zT6Hbdh1w==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -984,9 +976,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -1258,16 +1250,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1298,43 +1290,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -1647,11 +1602,11 @@ } }, "node_modules/heatsync": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.2.tgz", - "integrity": "sha512-s+YzwGpCjsJLRCuz6Ur8JFKz7vXEMFAPLqDbaEvMR5Um/IPPJpmupBH7LKeiyfGkIScFz9iyBPa1TifcoU4D7A==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.3.tgz", + "integrity": "sha512-NvR6wZZquJqDO/Tl1JGMGdNph0TTfhgWdj3sMWOkdQa9rs4fspHV+kGiZEgZQ0j8AQiYUEPQgRI7vdNZCKH+uw==", "dependencies": { - "backtracker": "3.3.2" + "backtracker": "^4.0.0" } }, "node_modules/html-es6cape": { @@ -3329,14 +3284,11 @@ } }, "node_modules/undici": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz", - "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.2.tgz", + "integrity": "sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==", "engines": { - "node": ">=18.0" + "node": ">=18.17" } }, "node_modules/unpipe": { diff --git a/package.json b/package.json index be55cf4..5d6192c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "entities": "^4.5.0", "get-stream": "^6.0.1", "giframe": "github:cloudrac3r/giframe#v0.4.2", - "heatsync": "^2.4.1", + "heatsync": "^2.5.3", "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", From 49598c7af7ac491daf8e8b63ccfac8eec35c8566 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 20 Jun 2024 13:50:38 +1200 Subject: [PATCH 030/346] Fix bulk message deletes I have finally e2e tested this function --- d2m/actions/delete-message.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/d2m/actions/delete-message.js b/d2m/actions/delete-message.js index 440e123..bc8adfb 100644 --- a/d2m/actions/delete-message.js +++ b/d2m/actions/delete-message.js @@ -33,9 +33,9 @@ async function deleteMessageBulk(data) { if (!roomID) return const sids = JSON.stringify(data.ids) - const eventsToRedact = from("event_message").pluck("event_id").and("WHERE message_id IN (SELECT value FROM json_each(?)").all(sids) - db.prepare("DELETE FROM message_channel WHERE message_id IN (SELECT value FROM json_each(?)").run(sids) - db.prepare("DELETE FROM event_message WHERE message_id IN (SELECT value FROM json_each(?)").run(sids) + const eventsToRedact = from("event_message").pluck("event_id").and("WHERE message_id IN (SELECT value FROM json_each(?))").all(sids) + db.prepare("DELETE FROM message_channel WHERE message_id IN (SELECT value FROM json_each(?))").run(sids) + db.prepare("DELETE FROM event_message WHERE message_id IN (SELECT value FROM json_each(?))").run(sids) for (const eventID of eventsToRedact) { // Awaiting will make it go slower, but since this could be a long-running operation either way, we want to leave rate limit capacity for other operations await api.redactEvent(roomID, eventID) From 8c506ed27009e9c8e15186a9c1af0bae49acdefc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 15 Jul 2024 15:27:21 +1200 Subject: [PATCH 031/346] Drop admin permissions correctly --- d2m/actions/register-user.js | 4 +++- discord/utils.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 6b688d4..2ea0afc 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -197,9 +197,11 @@ async function syncUser(user, member, channel, guild, roomID) { await api.sendState(roomID, "m.room.member", mxid, content, mxid) // Update power levels const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "") + const oldPowerLevel = powerLevelsStateContent.users?.[mxid] || 0 mixin(powerLevelsStateContent, {users: {[mxid]: powerLevel}}) if (powerLevel === 0) delete powerLevelsStateContent.users[mxid] // keep the event compact - await api.sendState(roomID, "m.room.power_levels", "", powerLevelsStateContent) + const sendPowerLevelAs = powerLevel < oldPowerLevel ? mxid : undefined // bridge bot won't not have permission to demote equal power users, so do this action as themselves + await api.sendState(roomID, "m.room.power_levels", "", powerLevelsStateContent, sendPowerLevelAs) // Update cached hash db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) } diff --git a/discord/utils.js b/discord/utils.js index c7045ec..6e95d17 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -103,10 +103,10 @@ function isWebhookMessage(message) { /** * Ephemeral messages can be generated if a slash command is attached to the same bot that OOYE is running on - * @param {DiscordTypes.APIMessage} message + * @param {Pick} message */ function isEphemeralMessage(message) { - return message.flags & (1 << 6); + return message.flags && (message.flags & (1 << 6)); } /** @param {string} snowflake */ From 9e51482b60ce988e9c96e475be28b2dcdcd9d851 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 16 Jul 2024 16:51:00 +1200 Subject: [PATCH 032/346] Recover from webhooks being deleted --- m2d/actions/channel-webhook.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index d52f174..13c3ab1 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -44,8 +44,13 @@ async function ensureWebhook(channelID, forceCreate = false) { */ async function withWebhook(channelID, callback) { const webhook = await ensureWebhook(channelID, false) - return callback(webhook).catch(e => { - // TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook) + return callback(webhook).catch(async e => { + if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) { // pathetic error handling from SnowTransfer + // Our webhook is gone. Maybe somebody deleted it, or removed and re-added OOYE from the guild. + const newWebhook = await ensureWebhook(channelID, true) + return callback(newWebhook) // not caught; if the error happens again just throw it instead of looping + } + throw e }) } From 149cee845a3f93ff7c9f92c16c97a12ed1f66f3a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 16 Jul 2024 22:01:20 +1200 Subject: [PATCH 033/346] Retry PK API if it doesn't return a message --- d2m/actions/register-pk-user.js | 15 +++++++++++++-- d2m/actions/send-message.js | 11 ++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/d2m/actions/register-pk-user.js b/d2m/actions/register-pk-user.js index 1e223c6..2ead4ff 100644 --- a/d2m/actions/register-pk-user.js +++ b/d2m/actions/register-pk-user.js @@ -142,8 +142,19 @@ async function syncUser(author, pkMessage, roomID) { } /** @returns {Promise} */ -function fetchMessage(messageID) { - return fetch(`https://api.pluralkit.me/v2/messages/${messageID}`).then(res => res.json()) +async function fetchMessage(messageID) { + // Their backend is weird. Sometimes it says "message not found" (code 20006) on the first try, so we make multiple attempts. + let attempts = 0 + do { + var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) + if (res.ok) return res.json() + + // I think the backend needs some time to update. + await new Promise(resolve => setTimeout(resolve, 2000)) + } while (++attempts < 3) + + const errorMessage = await res.json() + throw new Error(`PK API returned an error after ${attempts} tries: ${JSON.stringify(errorMessage)}`) } module.exports._memberToStateContent = memberToStateContent diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 346ac0d..6d100d4 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -39,15 +39,8 @@ async function sendMessage(message, channel, guild, row) { } else if (row && row.speedbump_webhook_id === message.webhook_id) { // Handle the PluralKit public instance if (row.speedbump_id === "466378653216014359") { - const root = await registerPkUser.fetchMessage(message.id) - // Member is null if member was deleted. We just got this message, so member surely exists. - if (!root.member) { - const e = new Error("PK API did not return a member") - message["__pk_response__"] = root - console.error(root) - throw e - } - senderMxid = await registerPkUser.syncUser(message.author, root, roomID) + const pkMessage = await registerPkUser.fetchMessage(message.id) + senderMxid = await registerPkUser.syncUser(message.author, pkMessage, roomID) } } From a2f0bb79db93d89ecef07427f84cac40c2594e96 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 16 Jul 2024 23:48:03 +1200 Subject: [PATCH 034/346] Retrigger events that arrive too early --- d2m/actions/retrigger.js | 61 +++++++++++++++++++++++++++++++ d2m/converters/edit-to-changes.js | 10 ++--- d2m/event-dispatcher.js | 36 +++++++++++------- 3 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 d2m/actions/retrigger.js diff --git a/d2m/actions/retrigger.js b/d2m/actions/retrigger.js new file mode 100644 index 0000000..2475daf --- /dev/null +++ b/d2m/actions/retrigger.js @@ -0,0 +1,61 @@ +// @ts-check + +const {EventEmitter} = require("events") +const passthrough = require("../../passthrough") +const {select} = passthrough + +const DEBUG_RETRIGGER = false + +function debugRetrigger(message) { + if (DEBUG_RETRIGGER) { + console.log(message) + } +} + +const emitter = new EventEmitter() + +/** + * Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives + * (or before the it has finished being bridged to an event). + * In this case, wait until the original message has finished bridging, then retrigger the passed function. + * @template {(...args: any) => Promise} T + * @param {string} messageID + * @param {T} fn + * @param {Parameters} rest + * @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered + */ +function eventNotFoundThenRetrigger(messageID, fn, ...rest) { + const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() + if (eventID) { + debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) + return false // event was found so don't retrigger + } + + debugRetrigger(`[retrigger] WAIT mid <-> eid = ${messageID} <-> ${eventID}`) + emitter.addListener(messageID, () => { + debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`) + fn(...rest) + }) + // if the event never arrives, don't trigger the callback, just clean up + setTimeout(() => { + if (emitter.listeners(messageID).length) { + debugRetrigger(`[retrigger] EXPIRE mid = ${messageID}`) + } + emitter.removeAllListeners(messageID) + }, 60 * 1000) // 1 minute + return true // event was not found, then retrigger +} + +/** + * Triggers any pending operations that were waiting on the corresponding event ID. + * @param {string} messageID + */ +function messageFinishedBridging(messageID) { + if (emitter.listeners(messageID).length) { + debugRetrigger(`[retrigger] EMIT mid = ${messageID}`) + } + emitter.emit(messageID) +} + +module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger +module.exports.messageFinishedBridging = messageFinishedBridging diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index dd8e39f..c80bb49 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -30,7 +30,8 @@ function eventCanBeEdited(ev) { * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ async function editToChanges(message, guild, api) { - // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. + // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). + // If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. const isGeneratedEmbed = !("content" in message) @@ -38,22 +39,21 @@ async function editToChanges(message, guild, api) { const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() assert(roomID) + const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() + /** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */ let senderMxid = null if (message.author) { senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null } else { // Should be a system generated embed. We want the embed to be sent by the same user who sent the message, so that the messages get grouped in most clients. - const eventID = select("event_message", "event_id", {message_id: message.id}).pluck().get() - assert(eventID) // this should have been checked earlier in a calling function + const eventID = oldEventRows[0].event_id // a calling function should have already checked that there is at least one message to edit const event = await api.getEvent(roomID, eventID) if (utils.eventSenderIsFromDiscord(event.sender)) { senderMxid = event.sender } } - const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() - // Figure out what we will be replacing them with const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api}) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index ec04750..7f27b77 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -33,6 +33,8 @@ const discordCommandHandler = sync.require("../discord/discord-command-handler") const mxUtils = require("../m2d/converters/utils") /** @type {import("./actions/speedbump")} */ const speedbump = sync.require("./actions/speedbump") +/** @type {import("./actions/retrigger")} */ +const retrigger = sync.require("./actions/retrigger") /** @type {any} */ // @ts-ignore bad types from semaphore const Semaphore = require("@chriscdn/promise-semaphore") @@ -252,6 +254,8 @@ module.exports = { // @ts-ignore await sendMessage.sendMessage(message, channel, guild, row), await discordCommandHandler.execute(message, channel, guild) + + retrigger.messageFinishedBridging(message.id) }, /** @@ -259,6 +263,14 @@ module.exports = { * @param {DiscordTypes.GatewayMessageUpdateDispatchData} data */ async onMessageUpdate(client, data) { + // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. + // If the message content is a string then it includes all interesting fields and is meaningful. + // Otherwise, if there are embeds, then the system generated URL preview embeds. + if (!(typeof data.content === "string" || "embeds" in data)) return + + // Deal with Eventual Consistency(TM) + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return + if (data.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: data.webhook_id}).pluck().get() if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. @@ -270,21 +282,16 @@ module.exports = { const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return - // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. - // If the message content is a string then it includes all interesting fields and is meaningful. - // Otherwise, if there are embeds, then the system generated URL preview embeds. - if (typeof data.content === "string" || "embeds" in data) { - /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ - // @ts-ignore - const message = data + /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ + // @ts-ignore + const message = data - const channel = client.channels.get(message.channel_id) - if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. - const guild = client.guilds.get(channel.guild_id) - assert(guild) - // @ts-ignore - await editMessage.editMessage(message, guild, row) - } + const channel = client.channels.get(message.channel_id) + if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. + const guild = client.guilds.get(channel.guild_id) + assert(guild) + // @ts-ignore + await editMessage.editMessage(message, guild, row) }, /** @@ -311,6 +318,7 @@ module.exports = { */ async onMessageDelete(client, data) { speedbump.onMessageDelete(data.id) + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageDelete, client, data)) return await deleteMessage.deleteMessage(data) }, From 5a65e6f6da9b81f7031f26089f8010b46ed85b36 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 17 Jul 2024 12:08:04 +1200 Subject: [PATCH 035/346] Only retrigger once, just in case --- d2m/actions/retrigger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/d2m/actions/retrigger.js b/d2m/actions/retrigger.js index 2475daf..030ffbf 100644 --- a/d2m/actions/retrigger.js +++ b/d2m/actions/retrigger.js @@ -18,7 +18,7 @@ const emitter = new EventEmitter() * Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives * (or before the it has finished being bridged to an event). * In this case, wait until the original message has finished bridging, then retrigger the passed function. - * @template {(...args: any) => Promise} T + * @template {(...args: any[]) => Promise} T * @param {string} messageID * @param {T} fn * @param {Parameters} rest @@ -32,7 +32,7 @@ function eventNotFoundThenRetrigger(messageID, fn, ...rest) { } debugRetrigger(`[retrigger] WAIT mid <-> eid = ${messageID} <-> ${eventID}`) - emitter.addListener(messageID, () => { + emitter.once(messageID, () => { debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`) fn(...rest) }) From eb98af0ac3eda7405d9f7fb00e33b3a86f243a7d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 21 Jul 2024 01:30:07 +1200 Subject: [PATCH 036/346] Move packages to npm registry --- d2m/actions/create-room.test.js | 2 +- d2m/actions/create-space.test.js | 2 +- d2m/actions/register-user.js | 2 +- d2m/converters/lottie.js | 2 +- d2m/converters/message-to-event.js | 10 +- m2d/converters/emoji-sheet.js | 4 +- matrix/kstate.js | 2 +- matrix/mreq.js | 2 +- package-lock.json | 481 ++++++++++------------------- package.json | 14 +- passthrough.js | 2 +- 11 files changed, 177 insertions(+), 346 deletions(-) diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index be0febb..2a76e2e 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,6 +1,6 @@ // @ts-check -const mixin = require("mixin-deep") +const mixin = require("@cloudrac3r/mixin-deep") const {channelToKState, _convertNameAndTopic} = require("./create-room") const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") diff --git a/d2m/actions/create-space.test.js b/d2m/actions/create-space.test.js index 368bb32..b1c1f06 100644 --- a/d2m/actions/create-space.test.js +++ b/d2m/actions/create-space.test.js @@ -1,6 +1,6 @@ // @ts-check -const mixin = require("mixin-deep") +const mixin = require("@cloudrac3r/mixin-deep") const {guildToKState, ensureSpace} = require("./create-space") const {kstateStripConditionals, kstateUploadMxc} = require("../../matrix/kstate") const {test} = require("supertape") diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 2ea0afc..6bd4661 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -3,7 +3,7 @@ const assert = require("assert").strict const reg = require("../../matrix/read-registration") const DiscordTypes = require("discord-api-types/v10") -const mixin = require("mixin-deep") +const mixin = require("@cloudrac3r/mixin-deep") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough diff --git a/d2m/converters/lottie.js b/d2m/converters/lottie.js index 6d13f70..12a311a 100644 --- a/d2m/converters/lottie.js +++ b/d2m/converters/lottie.js @@ -2,7 +2,7 @@ const assert = require("assert") const stream = require("stream") -const {PNG} = require("pngjs") +const {PNG} = require("@cloudrac3r/pngjs") const SIZE = 160 // Discord's display size on 1x displays is 160 diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 45e43dd..45c4032 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -1,10 +1,10 @@ // @ts-check const assert = require("assert").strict -const markdown = require("discord-markdown") +const markdown = require("@cloudrac3r/discord-markdown") const pb = require("prettier-bytes") const DiscordTypes = require("discord-api-types/v10") -const {tag} = require("html-template-tag") +const {tag} = require("@cloudrac3r/html-template-tag") const passthrough = require("../../passthrough") const {sync, db, discord, select, from} = passthrough @@ -396,7 +396,7 @@ async function messageToEvent(message, guild, options = {}, di) { discordOnly: true, escapeHTML: false, ...customOptions - }, null, null) + }) return {body, html} } @@ -439,12 +439,12 @@ async function messageToEvent(message, guild, options = {}, di) { else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" const repliedToHtml = markdown.toHTML(repliedToContent, { discordCallback: getDiscordParseCallbacks(message, guild, true) - }, null, null) + }) const repliedToBody = markdown.toHTML(repliedToContent, { discordCallback: getDiscordParseCallbacks(message, guild, false), discordOnly: true, escapeHTML: false, - }, null, null) + }) html = `
In reply to ${repliedToUserHtml}` + `
${repliedToHtml}
` + html diff --git a/m2d/converters/emoji-sheet.js b/m2d/converters/emoji-sheet.js index 73c7e34..4d9cc07 100644 --- a/m2d/converters/emoji-sheet.js +++ b/m2d/converters/emoji-sheet.js @@ -3,8 +3,8 @@ const assert = require("assert").strict const {pipeline} = require("stream").promises const sharp = require("sharp") -const {GIFrame} = require("giframe") -const {PNG} = require("pngjs") +const {GIFrame} = require("@cloudrac3r/giframe") +const {PNG} = require("@cloudrac3r/pngjs") const streamMimeType = require("stream-mime-type") const SIZE = 48 diff --git a/matrix/kstate.js b/matrix/kstate.js index eca9522..67bb063 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert").strict -const mixin = require("mixin-deep") +const mixin = require("@cloudrac3r/mixin-deep") const {isDeepStrictEqual} = require("util") const passthrough = require("../passthrough") diff --git a/matrix/mreq.js b/matrix/mreq.js index d515846..d6a21ae 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -1,7 +1,7 @@ // @ts-check const fetch = require("node-fetch").default -const mixin = require("mixin-deep") +const mixin = require("@cloudrac3r/mixin-deep") const stream = require("stream") const getStream = require("get-stream") diff --git a/package-lock.json b/package-lock.json index 5132540..2d5284a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,21 +10,21 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", + "@cloudrac3r/discord-markdown": "^2.6.2", + "@cloudrac3r/giframe": "^0.4.3", + "@cloudrac3r/html-template-tag": "^5.0.1", + "@cloudrac3r/mixin-deep": "^3.0.0", + "@cloudrac3r/pngjs": "^7.0.3", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", - "giframe": "github:cloudrac3r/giframe#v0.4.2", "heatsync": "^2.5.3", - "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "minimist": "^1.2.8", - "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", - "pngjs": "github:cloudrac3r/pngjs#v7.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.32.6", "snowtransfer": "^0.10.5", @@ -34,14 +34,14 @@ "xxhash-wasm": "^1.0.2" }, "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.2", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^10.4.0", - "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" + "supertape": "^10.4.0" } }, "node_modules/@bcoe/v8-coverage": { @@ -59,7 +59,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.0.6", @@ -73,7 +72,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -82,7 +80,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -94,7 +91,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -103,7 +99,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", @@ -118,7 +113,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -127,7 +121,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -140,14 +133,12 @@ "node_modules/@cloudcmd/stub/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/@cloudcmd/stub/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -155,11 +146,77 @@ "node": ">=8" } }, + "node_modules/@cloudrac3r/discord-markdown": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.2.tgz", + "integrity": "sha512-nHk7HPRK6smrYP+BbEHdLEsv/utXdsylLmdqVgiB4U+PgWnILR7RlcKnKjS5P+K8guIzh+3HvTzWkF0PchO54g==", + "dependencies": { + "simple-markdown": "^0.7.3", + "supertape": "^10.7.2" + } + }, + "node_modules/@cloudrac3r/giframe": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/giframe/-/giframe-0.4.3.tgz", + "integrity": "sha512-LKuRfdHrhvgPP0heYdlVRecswk/kYaC3fI+X+GQmnkJE36uN1E2dg5l5QdLoukliH7g8S2hgDYk0jsR7sJf8Dg==" + }, + "node_modules/@cloudrac3r/html-template-tag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cloudrac3r/html-template-tag/-/html-template-tag-5.0.1.tgz", + "integrity": "sha512-aH+ZdWJf53E63bVb2FiSnpM81qtF2ZNVbrXjrHcfnofyV/GTYJjZHnmPYC2FgXxJ+I8+bZP3DiwYzj7zXYoekw==", + "dependencies": { + "html-es6cape": "^2.0.0" + } + }, + "node_modules/@cloudrac3r/mixin-deep": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/mixin-deep/-/mixin-deep-3.0.0.tgz", + "integrity": "sha512-yQz1wHSZbHfbKaGSjrV3wIG0e9MnElKlmekMKJPRdTn2jhF2Mt8wfMPX8U7v6rTyzR/7BTrX8CCUcrJMLgoQqw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@cloudrac3r/pngjs": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/pngjs/-/pngjs-7.0.3.tgz", + "integrity": "sha512-Aghuja9XAIqBPmY2jk8dKZSyK90gImxA4hJeEYYAWkZO34bf+zliUAvGBygoBZA0EgXSmfxewVchL+9y3w+rDw==", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/@cloudrac3r/tap-dot": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.2.tgz", + "integrity": "sha512-q2IzqZvFIIjy8dBnuC5+baZ6KHroxOjWzaaBd+FGPk2NvPpsRdf8dFv0rTTntco8Z62grmCuMPc4/Nblx+Ot3A==", + "dev": true, + "dependencies": { + "@cloudrac3r/tap-out": "^3.2.3", + "colorette": "^1.0.5" + }, + "bin": { + "tap-dot": "bin/dot" + } + }, + "node_modules/@cloudrac3r/tap-out": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-out/-/tap-out-3.2.3.tgz", + "integrity": "sha512-WS89ziuTMLZlOVTXArkok32HcpWWxejQmZAUOl6nbB2Y114M8LfSOQ7t/rmgVsYB4oc6Ehe/nzevUMt4GGbCsA==", + "dev": true, + "dependencies": { + "re-emitter": "1.1.4", + "split2": "^4.2.0" + }, + "bin": { + "tap-in": "bin/tap-in.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -176,7 +233,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -187,14 +243,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -211,7 +265,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -237,7 +290,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -274,7 +326,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -284,7 +335,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-2.0.0.tgz", "integrity": "sha512-EXJv2HaXM+5scjoxE6Tf+o4+pxwL1tYJZJBDMygrF7cocjirGcU05GgNr9WHOaUPaVOpVjVU98ugYD7XJLmMkw==", - "dev": true, "dependencies": { "ci-info": "^4.0.0", "fullstore": "^3.0.0" @@ -294,29 +344,26 @@ } }, "node_modules/@putout/cli-validate-args": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-1.1.1.tgz", - "integrity": "sha512-AczBS98YyvsDVxvvYjHGyIygFu3i/EJ0xsruU6MlytTuUiCFQIE/QQPDy1bcN5J2Y75BzSYncaYnVrEGcBjeeQ==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-2.0.0.tgz", + "integrity": "sha512-/tl1XiBog6XMb1T9kalFerYU86sYsl6EtrlvGI5RVtlHOQdEEJAIPRxmX4vnKG3uoY5aVEkJOWzbPM5tsncmFQ==", "dependencies": { "fastest-levenshtein": "^1.0.12", - "just-kebab-case": "^1.1.0" + "just-kebab-case": "^4.2.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@supertape/engine-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-2.0.0.tgz", "integrity": "sha512-1G2MmfZnSxx546omLPAVNgvG/iqOQZGiXHnjJ2JXKvuf2lpPdDRnNm5eLl81lvEG473zE9neX979TzeFcr3Dxw==", - "dev": true, "dependencies": { "try-catch": "^3.0.0" }, @@ -328,7 +375,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", - "dev": true, "dependencies": { "@supertape/formatter-tap": "^3.0.3", "fullstore": "^3.0.0" @@ -341,7 +387,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", - "dev": true, "dependencies": { "fullstore": "^3.0.0" }, @@ -350,10 +395,9 @@ } }, "node_modules/@supertape/formatter-progress-bar": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-5.0.2.tgz", - "integrity": "sha512-thJlWFQLVeZjK/YZktUNBU69LFaEjBNvRcg6otuXn0RHhorRrgNWHHkA7JLDHRExzc+xaGJJvrWwsslh47vN2g==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-6.1.0.tgz", + "integrity": "sha512-BVnLW08BMbF/Xf9DNxTtc5V5Ong4VCj0w46Ts2cc1EboX+RQGuxGO0/wrzTBTt4t30iUzFhG/t2g280MfLHutQ==", "dependencies": { "chalk": "^5.3.0", "ci-info": "^4.0.0", @@ -369,7 +413,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -381,7 +424,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==", - "dev": true, "engines": { "node": ">=16" } @@ -390,7 +432,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==", - "dev": true, "engines": { "node": ">=16" } @@ -399,7 +440,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-time/-/formatter-time-1.0.2.tgz", "integrity": "sha512-QihQWA/3LSNuODHrL8MGNHkdRunaEqNQkuMUDGNgEQO8MYBB0d83WGlNxDFGjn4kRlq47hovw3Skq7Btb2i2JA==", - "dev": true, "dependencies": { "chalk": "^5.3.0", "ci-info": "^4.0.0", @@ -416,7 +456,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -428,7 +467,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.1.0.tgz", "integrity": "sha512-jzC56u1k+3DLRo854+J6v/DP/4SjRV2mAqfR6qzsyaAocC9OFe7NHYQQMmlJ4cUJwgFjUh7AVnjFfC0Z0XuH+g==", - "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0" }, @@ -483,18 +521,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -511,7 +537,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -523,7 +548,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -548,7 +572,6 @@ "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, "dependencies": { "printable-characters": "^1.0.42" } @@ -572,8 +595,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -701,35 +723,10 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -786,7 +783,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -819,7 +815,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", - "dev": true, "funding": [ { "type": "github", @@ -834,7 +829,6 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dev": true, "dependencies": { "string-width": "^4.2.3" }, @@ -1010,7 +1004,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1028,8 +1021,7 @@ "node_modules/data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" }, "node_modules/debug": { "version": "2.6.9", @@ -1115,7 +1107,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -1125,14 +1116,6 @@ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.70.tgz", "integrity": "sha512-8EtfZR0KwOK+yP5q/llWILdUAPmGmF1LmcVUYf7+gtGigz2pu6WR38ZN+IWtMzohY1Ujl2u3KOdbFvrEz9EC8w==" }, - "node_modules/discord-markdown": { - "version": "2.6.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", - "license": "MIT", - "dependencies": { - "simple-markdown": "^0.7.2" - } - }, "node_modules/domino": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", @@ -1141,8 +1124,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ee-first": { "version": "1.1.1", @@ -1152,8 +1134,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -1223,24 +1204,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1299,7 +1262,6 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, "engines": { "node": ">= 4.9.1" } @@ -1358,6 +1320,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -1416,7 +1383,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==", - "dev": true, "engines": { "node": ">=4" } @@ -1460,7 +1426,6 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -1477,43 +1442,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/giframe": { - "version": "0.4.2", - "resolved": "git+ssh://git@github.com/cloudrac3r/giframe.git#39b9d9af4184ea9df72c0ccd4db96da51bd1082c", - "license": "MIT" - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob/node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1529,7 +1488,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -1552,7 +1510,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1620,14 +1577,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/html-template-tag": { - "version": "5.0.0", - "resolved": "git+ssh://git@github.com/cloudrac3r/html-template-tag.git#9b2ec9efd344119997495c7889c11527cc6a35ed", - "license": "MIT", - "dependencies": { - "html-es6cape": "^2.0.0" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1710,7 +1659,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -1722,7 +1670,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -1730,8 +1677,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -1770,15 +1716,14 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1791,7 +1736,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -1806,7 +1750,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -1823,10 +1766,9 @@ } }, "node_modules/just-kebab-case": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-1.1.0.tgz", - "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", + "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -1946,15 +1888,14 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1969,22 +1910,13 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mixin-deep": { - "version": "3.0.0", - "resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2134,6 +2066,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2164,7 +2101,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -2172,32 +2108,29 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/path-to-regexp": { @@ -2217,14 +2150,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/pngjs": { - "version": "7.0.2", - "resolved": "git+ssh://git@github.com/cloudrac3r/pngjs.git#0295be509ed56dcf2f1d11b3af0b3108ad699dfe", - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -2259,7 +2184,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -2273,7 +2197,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2284,17 +2207,7 @@ "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -2381,24 +2294,7 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", - "dev": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/readable-web-to-node-stream": { "version": "3.0.2", @@ -2441,7 +2337,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -2666,7 +2561,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2678,7 +2572,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -2777,28 +2670,23 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, - "dependencies": { - "through": "2" - }, "engines": { - "node": "*" + "node": ">= 10.x" } }, "node_modules/stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -2857,7 +2745,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2872,7 +2759,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2886,7 +2772,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2895,7 +2780,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2907,7 +2791,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2916,7 +2799,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2928,7 +2810,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2944,7 +2825,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2956,7 +2836,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2986,25 +2865,25 @@ } }, "node_modules/supertape": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.4.0.tgz", - "integrity": "sha512-mQbcPU3jyDQsbncbS2flHELhZ7qEIF9q4Swnn/0782aN+rMQBzcJR3yapVn+9k3pUoUi64IcTaGxs24WSLDCvQ==", - "dev": true, + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.7.2.tgz", + "integrity": "sha512-pBWvzBtefVgokjJRk2ks+6q8/Sy1fIIvMBL8nXYaXk3100cgcPsw4iYhFSW41tg/cEe4IOlV/G+FZFN4kSMtOg==", "dependencies": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^2.0.0", - "@putout/cli-validate-args": "^1.0.1", + "@putout/cli-validate-args": "^2.0.0", "@supertape/engine-loader": "^2.0.0", "@supertape/formatter-fail": "^3.0.0", "@supertape/formatter-json-lines": "^2.0.0", - "@supertape/formatter-progress-bar": "^5.0.0", + "@supertape/formatter-progress-bar": "^6.0.0", "@supertape/formatter-short": "^2.0.0", "@supertape/formatter-tap": "^3.0.0", "@supertape/formatter-time": "^1.0.0", "@supertape/operator-stub": "^3.0.0", "cli-progress": "^3.8.2", + "flatted": "^3.3.1", "fullstore": "^3.0.0", - "glob": "^10.3.10", + "glob": "^11.0.0", "jest-diff": "^29.0.1", "once": "^1.4.0", "resolve": "^1.17.0", @@ -3026,7 +2905,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3038,7 +2916,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3046,38 +2923,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tap-dot": { - "version": "2.0.0", - "resolved": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#9dd7750ececeae3a96afba91905be812b6b2cc2d", - "integrity": "sha512-SLg6KF3cSkKII+5hA/we9FjnMCrL5uk0wYap7RXD9KJziy7xqZolvEOamt3CJlm5LSzRXIGblm3nmhY/EBE3AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^1.0.5", - "tap-out": "github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771" - }, - "bin": { - "tap-dot": "bin/dot" - } - }, - "node_modules/tap-out": { - "version": "3.2.1", - "resolved": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", - "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "re-emitter": "1.1.4", - "readable-stream": "^4.3.0", - "split": "^1.0.1" - }, - "bin": { - "tap-in": "bin/tap-in.js" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -3173,12 +3018,6 @@ "node": "*" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3203,8 +3042,7 @@ "node_modules/timer-node": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.7.tgz", - "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==", - "dev": true + "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==" }, "node_modules/toidentifier": { "version": "1.0.1", @@ -3239,7 +3077,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==", - "dev": true, "engines": { "node": ">=6" } @@ -3284,9 +3121,9 @@ } }, "node_modules/undici": { - "version": "6.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.2.tgz", - "integrity": "sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", + "integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==", "engines": { "node": ">=18.17" } @@ -3352,7 +3189,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3385,7 +3221,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3402,7 +3237,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3411,7 +3245,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3448,8 +3281,7 @@ "node_modules/wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", - "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", - "dev": true + "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" }, "node_modules/xxhash-wasm": { "version": "1.0.2", @@ -3492,7 +3324,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index 5d6192c..806785c 100644 --- a/package.json +++ b/package.json @@ -16,21 +16,21 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", + "@cloudrac3r/discord-markdown": "^2.6.2", + "@cloudrac3r/giframe": "^0.4.3", + "@cloudrac3r/html-template-tag": "^5.0.1", + "@cloudrac3r/mixin-deep": "^3.0.0", + "@cloudrac3r/pngjs": "^7.0.3", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#2881b447954fcea10510f212fa4c1dbbdc0a57a3", "entities": "^4.5.0", "get-stream": "^6.0.1", - "giframe": "github:cloudrac3r/giframe#v0.4.2", "heatsync": "^2.5.3", - "html-template-tag": "github:cloudrac3r/html-template-tag#v5.0", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "minimist": "^1.2.8", - "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", - "pngjs": "github:cloudrac3r/pngjs#v7.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.32.6", "snowtransfer": "^0.10.5", @@ -40,14 +40,14 @@ "xxhash-wasm": "^1.0.2" }, "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.2", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", - "supertape": "^10.4.0", - "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" + "supertape": "^10.4.0" }, "scripts": { "addbot": "node addbot.js", diff --git a/passthrough.js b/passthrough.js index fd21381..ec80004 100644 --- a/passthrough.js +++ b/passthrough.js @@ -5,7 +5,7 @@ * @property {import("repl").REPLServer} repl * @property {typeof import("./config")} config * @property {import("./d2m/discord-client")} discord - * @property {import("heatsync")} sync + * @property {import("heatsync").default} sync * @property {import("better-sqlite3/lib/database")} db * @property {import("matrix-appservice").AppService} as * @property {import("./db/orm").from} from From 541c5e1eb524e50c4bbe319e995a9718df4a37a7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 21 Jul 2024 14:56:58 +1200 Subject: [PATCH 037/346] Add include_user_id_in_mxid setting --- d2m/converters/user-to-mxid.js | 5 +++++ d2m/converters/user-to-mxid.test.js | 8 ++++++++ registration.example.yaml | 1 + types.d.ts | 1 + 4 files changed, 15 insertions(+) diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 6b3bbbd..97b655e 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert") +const registration = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {select} = passthrough @@ -24,6 +25,10 @@ function downcaseUsername(user) { .replace(/[^a-z0-9._=/-]*/g, "") // remove leading and trailing dashes and underscores... .replace(/(?:^[_-]*|[_-]*$)/g, "") + // If requested, also make the Discord user ID part of the username + if (registration.ooye.include_user_id_in_mxid) { + downcased = user.id + "_" + downcased + } // The new length must be at least 2 characters (in other words, it should have some content) if (downcased.length < 2) { downcased = user.id diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index b080115..4576205 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -44,3 +44,11 @@ test("user2name: uses ID when name has only disallowed characters", t => { test("user2name: works on special user", t => { t.equal(userToSimName(data.user.clyde_ai), "clyde_ai") }) + +test("user2name: includes ID if requested in config", t => { + const reg = require("../../matrix/read-registration") + reg.ooye.include_user_id_in_mxid = true + t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles") + t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") + reg.ooye.include_user_id_in_mxid = false +}) diff --git a/registration.example.yaml b/registration.example.yaml index dd217db..d38f5ae 100644 --- a/registration.example.yaml +++ b/registration.example.yaml @@ -19,6 +19,7 @@ ooye: server_name: [the part after the colon in your matrix id, like cadence.moe] server_origin: [the full protocol and domain of your actual matrix server's location, with no trailing slash, like https://matrix.cadence.moe] content_length_workaround: false + include_user_id_in_mxid: false invite: # uncomment this to auto-invite the named user to newly created spaces and mark them as admin (PL 100) everywhere # - '@cadence:cadence.moe' diff --git a/types.d.ts b/types.d.ts index 68430d9..6036867 100644 --- a/types.d.ts +++ b/types.d.ts @@ -22,6 +22,7 @@ export type AppServiceRegistrationConfig = { server_name: string server_origin: string content_length_workaround: boolean + include_user_id_in_mxid: boolean invite: string[] } old_bridge?: { From a6251e850c75a938462c30db83efd5d31df451bb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 30 Jul 2024 23:45:26 +1200 Subject: [PATCH 038/346] Use small text for m->d replies --- m2d/converters/event-to-message.js | 6 +-- m2d/converters/event-to-message.test.js | 72 ++++++++++++------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index c22e1a8..7b88adb 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -541,7 +541,7 @@ async function eventToMessage(event, guild, di) { .replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) .replace(/<[^>]+>/g, "") // Completely strip all HTML tags and formatting. ), 50) - replyLine = "> " + contentPreviewChunks[0] + replyLine = "> -# " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) replyLine = replyLine.replace(/[,.']$/, "") + "..." replyLine += "\n" return @@ -595,14 +595,14 @@ async function eventToMessage(event, guild, di) { repliedToContent = entities.decodeHTML5Strict(repliedToContent) // Remove entities like & " const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { - contentPreview = ":\n> " + contentPreviewChunks[0] + contentPreview = ":\n> -# " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." } else { console.log("Unable to generate reply preview for this replied-to event because we stripped all of it:", repliedToEvent) contentPreview = "" } } - replyLine = `> ${replyLine}${contentPreview}\n` + replyLine = `> -# ${replyLine}${contentPreview}\n` })() if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 6d750a5..c054abc 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -999,8 +999,8 @@ test("event2message: rich reply to a sim user", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> Slow news day." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + "\n> -# Slow news day." + "\nTesting this reply, ignore", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1071,8 +1071,8 @@ test("event2message: rich reply to a rich reply to a multi-line message should c messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> I just checked in a fix that will probably work..." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# I just checked in a fix that will probably work..." + "\nwill try later (tomorrow if I don't forgor)", avatar_url: undefined, allowed_mentions: { @@ -1151,8 +1151,8 @@ test("event2message: rich reply to an already-edited message will quote the new messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" - + "\n> this is the new content. heya!" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" + + "\n> -# this is the new content. heya!" + "\nhiiiii....", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1205,7 +1205,7 @@ test("event2message: rich reply to a missing event will quote from formatted_bod messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> But who sees the seashells she sells sitting..." + content: "> -# But who sees the seashells she sells sitting..." + "\nWhat a tongue-bender...", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1356,8 +1356,8 @@ test("event2message: should avoid using blockquote contents as reply preview in messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> that can't be true! there's no way :o" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + "\n> -# that can't be true! there's no way :o" + "\nI agree!", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1445,8 +1445,8 @@ test("event2message: should include a reply preview when message ends with a blo messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**_ooye_cookie**:" - + "\n> https://tootsuite.net/Warp-Gate2.gif tanget: @..." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**_ooye_cookie**:" + + "\n> -# https://tootsuite.net/Warp-Gate2.gif tanget: @..." + "\naichmophobia", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1529,8 +1529,8 @@ test("event2message: should include a reply preview when replying to a descripti messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" - + "\n> It looks like this queue has ended." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" + + "\n> -# It looks like this queue has ended." + `\nso you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?`, avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1582,8 +1582,8 @@ test("event2message: entities are not escaped in main message or reply preview", messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> Testing? \"':.`[]&things" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# Testing? \"':.`[]&things" + "\n_Testing?_ \"':.\\`\\[\\]&things", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1632,8 +1632,8 @@ test("event2message: reply preview converts emoji formatting when replying to a messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> <:hippo:230201364309868544>" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# <:hippo:230201364309868544>" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1682,8 +1682,8 @@ test("event2message: reply preview can guess custom emoji based on the name if i messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> <:hippo:230201364309868544>" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# <:hippo:230201364309868544>" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1732,8 +1732,8 @@ test("event2message: reply preview uses emoji title text when replying to an unk messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> :svkftngur_gkdne:" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# :svkftngur_gkdne:" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1782,8 +1782,8 @@ test("event2message: reply preview ignores garbage image", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> I am having a nice day" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> -# I am having a nice day" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1832,7 +1832,7 @@ test("event2message: reply to empty message doesn't show an extra line or anythi messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1914,8 +1914,8 @@ test("event2message: editing a rich reply to a sim user", async t => { id: "1144874214311067708", message: { username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> Slow news day." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + "\n> -# Slow news day." + "\nEditing this reply, which is also a test", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2205,8 +2205,8 @@ test("event2message: rich reply to a matrix user's long message with formatting" messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" - + "\n> i should have a little happy test list bold em..." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + + "\n> -# i should have a little happy test list bold em..." + "\n**no you can't!!!**", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2268,7 +2268,7 @@ test("event2message: rich reply to an image", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + "\nCaught in 8K UHD VR QLED Epic Edition", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2323,8 +2323,8 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> [spoiler] cw crossword spoilers you'll never..." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + "\n> -# [spoiler] cw crossword spoilers you'll never..." + "\nomg NO WAY!!", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2378,8 +2378,8 @@ test("event2message: with layered rich replies, the preview should only be the r messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" - + "\n> two" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + + "\n> -# two" + "\nthree", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2434,8 +2434,8 @@ test("event2message: if event is a reply and starts with a quote, they should be messagesToEdit: [], messagesToSend: [{ username: "Rose", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Rose**:" - + "\n> i have a feeling that clients are meant to strip..." + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Rose**:" + + "\n> -# i have a feeling that clients are meant to strip..." + "\n" + "\n> To strip the fallback on the `body`, the client should iterate over each line of the string, removing any lines that start with the fallback prefix (\"> “, including the space, without quotes) and stopping when a line is encountered without the prefix. This prefix is known as the “fallback prefix sequence”.", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", @@ -2494,7 +2494,7 @@ test("event2message: rich reply to a deleted event", async t => { messagesToEdit: [], messagesToSend: [{ username: "Ampflower 🌺", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Ampflower 🌺** (in reply to a deleted message)" + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Ampflower 🌺** (in reply to a deleted message)" + "\nHuh it did the same thing here too", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", allowed_mentions: { From 742d34f15ee3401e65e132c6d333f5b2a253c16f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 30 Jul 2024 23:45:44 +1200 Subject: [PATCH 039/346] Fork turndown for Discord-like link escaping --- m2d/converters/event-to-message.js | 2 +- m2d/converters/event-to-message.test.js | 39 +++++++++++++++++++++++++ package-lock.json | 19 ++++++------ package.json | 3 +- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 7b88adb..a266f12 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -4,7 +4,7 @@ const Ty = require("../../types") const DiscordTypes = require("discord-api-types/v10") const {Readable} = require("stream") const chunk = require("chunk-text") -const TurndownService = require("turndown") +const TurndownService = require("@cloudrac3r/turndown") const domino = require("domino") const assert = require("assert").strict const entities = require("entities") diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index c054abc..7acad93 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -263,6 +263,45 @@ test("event2message: links in formatted body where the text & href are the same, ) }) +test("event2message: markdown in link text does not attempt to be escaped because that doesn't work", async t => { + t.deepEqual( + await eventToMessage({ + content: { + body: "hey mario sports mix [she/her], is it possible to listen on a unix socket?", + format: "org.matrix.custom.html", + formatted_body: "hey mario sports mix [she/her], is it possible to listen on a unix socket?", + "m.mentions": { + "user_ids": [ + "@cadence:cadence.moe" + ] + }, + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "hey [mario sports mix [she/her]](), is it possible to listen on a unix socket?", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: basic html is converted to markdown", async t => { t.deepEqual( await eventToMessage({ diff --git a/package-lock.json b/package-lock.json index 2d5284a..f7d6553 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", + "@cloudrac3r/turndown": "^7.1.4", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", + "domino": "^2.1.6", "entities": "^4.5.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", @@ -30,7 +32,6 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", - "turndown": "^7.1.2", "xxhash-wasm": "^1.0.2" }, "devDependencies": { @@ -213,6 +214,14 @@ "node": ">=8.0.0" } }, + "node_modules/@cloudrac3r/turndown": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz", + "integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==", + "dependencies": { + "domino": "^2.1.6" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3100,14 +3109,6 @@ "node": "*" } }, - "node_modules/turndown": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.1.2.tgz", - "integrity": "sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==", - "dependencies": { - "domino": "^2.1.6" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 806785c..1349673 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", + "@cloudrac3r/turndown": "^7.1.4", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", + "domino": "^2.1.6", "entities": "^4.5.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", @@ -36,7 +38,6 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", - "turndown": "^7.1.2", "xxhash-wasm": "^1.0.2" }, "devDependencies": { From f9eb043c10123432d86bb886221b32c90d0d6bbb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 31 Jul 2024 00:00:48 +1200 Subject: [PATCH 040/346] Update dependencies --- m2d/converters/event-to-message.test.js | 8 +- package-lock.json | 892 ++++++++++++++++-------- package.json | 8 +- 3 files changed, 621 insertions(+), 287 deletions(-) diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 7acad93..7d1f09d 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -4136,7 +4136,7 @@ slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet" t.deepEqual(testResult, { content: "a b", fileName: "emojis.png", - fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALkklEQVR4nM1ZeWyUxxV/azAGwn0JMJUppPhce++1Oc1i" + fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH" }) }) @@ -4161,7 +4161,7 @@ slow()("event2message: known emoji from an unreachable server at the end is reup t.deepEqual(testResult, { content: "a b", fileName: "emojis.png", - fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAOnUlEQVR4nM1aCXCb1Z3/kig+5NjWaeu+LOuWLFnHJ8l2" + fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAOoUlEQVR4nM1aCXBbx3l+Eu8bN0CAuO+TAHGTFAmAJHgT" }) }) @@ -4186,7 +4186,7 @@ slow()("event2message: known and unknown emojis in the end are reuploaded as a s t.deepEqual(testResult, { content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:", fileName: "emojis.png", - fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAT5UlEQVR4nOVbCXSVRZauR9gMsoYlvKwvARKSkPUlJOyL" + fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAeXRFWHRSYXcACklQVEMgcHJvZmlsZQogICAgICA0Ngoz" }) }) @@ -4211,6 +4211,6 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet t.deepEqual(testResult, { content: "testing", fileName: "emojis.png", - fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3xT1/W/UkImYKZtLdt4a0uWMaQkzS9t" + fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48" }) }) diff --git a/package-lock.json b/package-lock.json index f7d6553..9b97e23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", "domino": "^2.1.6", - "entities": "^4.5.0", + "entities": "^5.0.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", @@ -28,7 +28,7 @@ "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", - "sharp": "^0.32.6", + "sharp": "^0.33.4", "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", @@ -38,7 +38,7 @@ "@cloudrac3r/tap-dot": "^2.0.2", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", - "c8": "^8.0.1", + "c8": "^10.1.2", "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", @@ -52,9 +52,9 @@ "dev": true }, "node_modules/@chriscdn/promise-semaphore": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-2.0.7.tgz", - "integrity": "sha512-xsa5SAYSBnYjqvGnzmaLca4X/RFeOl+ziCsIHl5iHkFBgE4NgWupB4z3A1rVMBM2I8TEKaah+5iu9Cm7gQu9JQ==" + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-2.0.9.tgz", + "integrity": "sha512-kKXJcm5gM8FN8O8U20H19/85b8R33K0Q2u5cnm9mfblK/7QcNChlOhCTWgnrr8wYiuF1ZbYIZcioxW79QfjnmQ==" }, "node_modules/@cloudcmd/stub": { "version": "4.0.1", @@ -222,6 +222,446 @@ "domino": "^2.1.6" } }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", + "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", + "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", + "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", + "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", + "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.31", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", + "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", + "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", + "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", + "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.1.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", + "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", + "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -495,15 +935,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.18.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.5.tgz", - "integrity": "sha512-4slmbtwV59ZxitY4ixUZdy1uRLf9eSIvBWPQxNjhHYWEtn0FryfKpyS2cvADYXTayWdKEIsJengncrVvkI4I6A==", - "dev": true + "version": "18.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", + "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", "dev": true, "dependencies": { "@types/node": "*", @@ -591,11 +1034,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" - }, "node_modules/backtracker": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-4.0.0.tgz", @@ -642,9 +1080,9 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/better-sqlite3": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.0.0.tgz", - "integrity": "sha512-lDxQ9qg/XuUHZG6xzrQaMHkNWl37t35/LPB/VJGV8DdScSuGFNfFSqgscXEd8UIuyk/d9wU8iaMxQa4If5Wqog==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.1.2.tgz", + "integrity": "sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -745,20 +1183,19 @@ } }, "node_modules/c8": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz", - "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.2.tgz", + "integrity": "sha512-Qr6rj76eSshu5CgRYvktW0uM0CFY0yi4Fd5D0duDXO6sYinyopmftUiJVuzBQxQcwQLor7JWDVRP+dUfCmzgJw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", - "foreground-child": "^2.0.0", + "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", + "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" @@ -767,7 +1204,15 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, "node_modules/call-bind": { @@ -881,11 +1326,11 @@ } }, "node_modules/cloudstorm": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.8.tgz", - "integrity": "sha512-ZVy4HbEiNRyRO0p0d0DtIMoVCg8rxoRpA07hflq2MQpPpmFpostkF7nd+zOsQ+piAfhUdvPsgtQLHdGFmTZVPQ==", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.9.tgz", + "integrity": "sha512-LLWCDn8CORM9iCFyWq3sIRK/7zJU7Jw14wLit3p5zWlD9Ihixv2nqjBbnWqEuq97zWh/xA+abACz2CvTVBlIWQ==", "dependencies": { - "discord-api-types": "^0.37.70", + "discord-api-types": "^0.37.90", "snowtransfer": "^0.10.5" }, "engines": { @@ -947,12 +1392,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1105,9 +1544,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -1121,9 +1560,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.70", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.70.tgz", - "integrity": "sha512-8EtfZR0KwOK+yP5q/llWILdUAPmGmF1LmcVUYf7+gtGigz2pu6WR38ZN+IWtMzohY1Ujl2u3KOdbFvrEz9EC8w==" + "version": "0.37.93", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.93.tgz", + "integrity": "sha512-M5jn0x3bcXk8EI2c6F6V6LeOWq10B/cJf+YJSyqNmg7z4bdXK+Z7g9zGJwHS0h9Bfgs0nun2LQISFOzwck7G9A==" }, "node_modules/domino": { "version": "2.1.6", @@ -1162,9 +1601,9 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz", + "integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==", "engines": { "node": ">=0.12" }, @@ -1262,11 +1701,6 @@ "node": ">= 0.10.0" } }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1335,16 +1769,18 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dependencies": { "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/form-data": { @@ -1382,12 +1818,6 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fullstore": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", @@ -1478,32 +1908,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob/node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1568,9 +1972,9 @@ } }, "node_modules/heatsync": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.3.tgz", - "integrity": "sha512-NvR6wZZquJqDO/Tl1JGMGdNph0TTfhgWdj3sMWOkdQa9rs4fspHV+kGiZEgZQ0j8AQiYUEPQgRI7vdNZCKH+uw==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.4.tgz", + "integrity": "sha512-KzsM+wR0MIykD80kCHNZCpNvFY4uC1Yze8R37eehJyGIvEepJd+7ubczh6FVoBFtK0nVEszt5Hl8AbzUvb+vMQ==", "dependencies": { "backtracker": "^4.0.0" } @@ -1631,16 +2035,6 @@ } ] }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1794,17 +2188,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -1986,11 +2369,6 @@ "node": ">=10" } }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2097,15 +2475,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2253,11 +2622,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2358,63 +2722,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/runes": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/runes/-/runes-0.4.3.tgz", @@ -2448,12 +2755,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -2525,45 +2829,42 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", + "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" + "detect-libc": "^2.0.3", + "semver": "^7.6.0" }, "engines": { - "node": ">=14.15.0" + "libvips": ">=8.15.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/sharp/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.4", + "@img/sharp-darwin-x64": "0.33.4", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.4", + "@img/sharp-linux-arm64": "0.33.4", + "@img/sharp-linux-s390x": "0.33.4", + "@img/sharp-linux-x64": "0.33.4", + "@img/sharp-linuxmusl-arm64": "0.33.4", + "@img/sharp-linuxmusl-x64": "0.33.4", + "@img/sharp-wasm32": "0.33.4", + "@img/sharp-win32-ia32": "0.33.4", + "@img/sharp-win32-x64": "0.33.4" } }, "node_modules/shebang-command": { @@ -2599,10 +2900,15 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-concat": { "version": "1.0.1", @@ -2733,15 +3039,6 @@ "node": ">=10" } }, - "node_modules/streamx": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", - "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2972,59 +3269,89 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=18" } }, "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/test-exclude/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/through2": { @@ -3098,6 +3425,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "optional": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3129,6 +3462,12 @@ "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3298,11 +3637,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 1349673..10486ac 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.8", "domino": "^2.1.6", - "entities": "^4.5.0", + "entities": "^5.0.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", @@ -34,7 +34,7 @@ "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", - "sharp": "^0.32.6", + "sharp": "^0.33.4", "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", @@ -44,7 +44,7 @@ "@cloudrac3r/tap-dot": "^2.0.2", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", - "c8": "^8.0.1", + "c8": "^10.1.2", "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", From 2c3b653b540e6eea37e130104aece48c1f094d71 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 31 Jul 2024 00:06:32 +1200 Subject: [PATCH 041/346] Consistently add @ to m->d matrix mentions --- m2d/converters/event-to-message.js | 2 +- m2d/converters/event-to-message.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index a266f12..86975ab 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -129,7 +129,7 @@ turndownService.addRule("inlineLink", { const href = node.getAttribute("href") content = content.replace(/ @.*/, "") if (href === content) return href - if (href.startsWith("https://matrix.to/#/@") && content[0] !== "@") content = "@" + content + if (decodeURIComponent(href).startsWith("https://matrix.to/#/@") && content[0] !== "@") content = "@" + content return "[" + content + "](" + href + ")" } }) diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 7d1f09d..3a187c3 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -292,7 +292,7 @@ test("event2message: markdown in link text does not attempt to be escaped becaus messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "hey [mario sports mix [she/her]](), is it possible to listen on a unix socket?", + content: "hey [@mario sports mix [she/her]](), is it possible to listen on a unix socket?", avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] From 87cad6c61312fdcb4f4d0cefbb91a613d5ca4d4f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 3 Aug 2024 22:45:34 +1200 Subject: [PATCH 042/346] Update and unminify rlottie --- d2m/converters/rlottie-wasm.js | 2636 +++++++++++++++++++++++++++++- d2m/converters/rlottie-wasm.wasm | Bin 504120 -> 608159 bytes test/res/lottie-bee.png | Bin 5492 -> 5510 bytes 3 files changed, 2635 insertions(+), 1 deletion(-) diff --git a/d2m/converters/rlottie-wasm.js b/d2m/converters/rlottie-wasm.js index 4abf45d..986a299 100644 --- a/d2m/converters/rlottie-wasm.js +++ b/d2m/converters/rlottie-wasm.js @@ -1 +1,2635 @@ -var Module=typeof Module!="undefined"?Module:{};var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(ENVIRONMENT_IS_WORKER){scriptDirectory=nodePath.dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}process.on("uncaughtException",ex=>{if(ex!=="unwind"&&!(ex instanceof ExitStatus)&&!(ex.context instanceof ExitStatus)){throw ex}});quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow};Module["inspect"]=()=>"[Emscripten Module object]"}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=title=>document.title=title}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeKeepaliveCounter=0;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile="rlottie-wasm.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(binaryFile)){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{if(!response["ok"]){throw"failed to load wasm binary file at '"+binaryFile+"'"}return response["arrayBuffer"]()}).catch(()=>getBinarySync(binaryFile))}else if(readAsync){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),reject)})}}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(instance=>instance).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function createWasm(){var info={"env":wasmImports,"wasi_snapshot_preview1":wasmImports};function receiveInstance(instance,module){var exports=instance.exports;wasmExports=exports;wasmMemory=wasmExports["memory"];updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return exports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);return false}}instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult);return{}}var tempDouble;function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;var UTF8ArrayToString=(heapOrArray,idx,maxBytesToRead)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;this.set_type=function(type){HEAPU32[this.ptr+4>>2]=type};this.get_type=function(){return HEAPU32[this.ptr+4>>2]};this.set_destructor=function(destructor){HEAPU32[this.ptr+8>>2]=destructor};this.get_destructor=function(){return HEAPU32[this.ptr+8>>2]};this.set_caught=function(caught){caught=caught?1:0;HEAP8[this.ptr+12>>0]=caught};this.get_caught=function(){return HEAP8[this.ptr+12>>0]!=0};this.set_rethrown=function(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>0]=rethrown};this.get_rethrown=function(){return HEAP8[this.ptr+13>>0]!=0};this.init=function(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)};this.set_adjusted_ptr=function(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr};this.get_adjusted_ptr=function(){return HEAPU32[this.ptr+16>>2]};this.get_exception_ptr=function(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var SYSCALLS={varargs:undefined,get(){var ret=HEAP32[SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret},getp(){return SYSCALLS.get()},getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs}var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{};var embind_init_charCodes=()=>{var codes=new Array(256);for(var i=0;i<256;++i){codes[i]=String.fromCharCode(i)}embind_charCodes=codes};var embind_charCodes=undefined;var readLatin1String=ptr=>{var ret="";var c=ptr;while(HEAPU8[c]){ret+=embind_charCodes[HEAPU8[c++]]}return ret};var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var BindingError=undefined;var throwBindingError=message=>{throw new BindingError(message)};var InternalError=undefined;var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(function(type){typeDependencies[type]=dependentTypes});function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{if(registeredTypes.hasOwnProperty(dt)){typeConverters[i]=registeredTypes[dt]}else{unregisteredTypes.push(dt);if(!awaitingDependencies.hasOwnProperty(dt)){awaitingDependencies[dt]=[]}awaitingDependencies[dt].push(()=>{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}});if(0===unregisteredTypes.length){onComplete(typeConverters)}};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){if(!("argPackAdvance"in registeredInstance)){throw new TypeError("registerType registeredInstance requires argPackAdvance")}return sharedRegisterType(rawType,registeredInstance,options)}var GenericWireTypeSize=8;var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(wt){return!!wt},"toWireType":function(destructors,o){return o?trueValue:falseValue},"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":function(pointer){return this["fromWireType"](HEAPU8[pointer])},destructorFunction:null})};function ClassHandle_isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right}var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredPointers={};var getInheritedInstanceCount=()=>Object.keys(registeredInstances).length;var getLiveInheritedInstances=()=>{var rv=[];for(var k in registeredInstances){if(registeredInstances.hasOwnProperty(k)){rv.push(registeredInstances[k])}}return rv};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction=undefined;var setDelayFunction=fn=>{delayFunction=fn;if(deletionQueue.length&&delayFunction){delayFunction(flushPendingDeletes)}};var init_embind=()=>{Module["getInheritedInstanceCount"]=getInheritedInstanceCount;Module["getLiveInheritedInstances"]=getLiveInheritedInstances;Module["flushPendingDeletes"]=flushPendingDeletes;Module["setDelayFunction"]=setDelayFunction};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr:ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var attachFinalizer=handle=>{if("undefined"===typeof FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$:$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};function ClassHandle_clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}}function ClassHandle_delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}}function ClassHandle_isDeleted(){return!this.$$.ptr}function ClassHandle_deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}var init_ClassHandle=()=>{ClassHandle.prototype["isAliasOf"]=ClassHandle_isAliasOf;ClassHandle.prototype["clone"]=ClassHandle_clone;ClassHandle.prototype["delete"]=ClassHandle_delete;ClassHandle.prototype["isDeleted"]=ClassHandle_isDeleted;ClassHandle.prototype["deleteLater"]=ClassHandle_deleteLater};function ClassHandle(){}var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{if(undefined===name){return"_unknown"}name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function createNamedFunction(name,body){name=makeLegalFunctionName(name);return{[name]:function(){return body.apply(this,arguments)}}[name]}var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(){if(!proto[methodName].overloadTable.hasOwnProperty(arguments.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${arguments.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[arguments.length].apply(this,arguments)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;if(undefined!==numArguments){Module[name].numArguments=numArguments}}};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupporting sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function readPointer(pointer){return this["fromWireType"](HEAPU32[pointer>>2])}function RegisteredPointer_getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr}function RegisteredPointer_destructor(ptr){if(this.rawDestructor){this.rawDestructor(ptr)}}var RegisteredPointer_deleteObject=handle=>{if(handle!==null){handle["delete"]()}};var init_RegisteredPointer=()=>{RegisteredPointer.prototype.getPointee=RegisteredPointer_getPointee;RegisteredPointer.prototype.destructor=RegisteredPointer_destructor;RegisteredPointer.prototype["argPackAdvance"]=GenericWireTypeSize;RegisteredPointer.prototype["readValueFromPointer"]=readPointer;RegisteredPointer.prototype["deleteObject"]=RegisteredPointer_deleteObject;RegisteredPointer.prototype["fromWireType"]=RegisteredPointer_fromWireType};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this["toWireType"]=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this["toWireType"]=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this["toWireType"]=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistant public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var dynCallLegacy=(sig,ptr,args)=>{var f=Module["dynCall_"+sig];return args&&args.length?f.apply(null,[ptr].concat(args)):f.call(null,ptr)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var dynCall=(sig,ptr,args)=>{if(sig.includes("j")){return dynCallLegacy(sig,ptr,args)}var rtn=getWasmTableEntry(ptr).apply(null,args);return rtn};var getDynCaller=(sig,ptr)=>{var argCache=[];return function(){argCache.length=0;Object.assign(argCache,arguments);return dynCall(sig,ptr,argCache)}};var embind__requireFunction=(signature,rawFunction)=>{signature=readLatin1String(signature);function makeDynCaller(){if(signature.includes("j")){return getDynCaller(signature,rawFunction)}return getWasmTableEntry(rawFunction)}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};var extendError=(baseErrorType,errorName)=>{var errorClass=createNamedFunction(errorName,function(message){this.name=errorName;this.message=message;var stack=new Error(message).stack;if(stack!==undefined){this.stack=this.toString()+"\n"+stack.replace(/^Error(:[^\n]*)?\n/,"")}});errorClass.prototype=Object.create(baseErrorType.prototype);errorClass.prototype.constructor=errorClass;errorClass.prototype.toString=function(){if(this.message===undefined){return this.name}else{return`${this.name}: ${this.message}`}};return errorClass};var UnboundTypeError=undefined;var getTypeName=type=>{var ptr=___getTypeName(type);var rv=readLatin1String(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=readLatin1String(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);if(upcast){upcast=embind__requireFunction(upcastSignature,upcast)}if(downcast){downcast=embind__requireFunction(downcastSignature,downcast)}rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],function(base){base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(legalFunctionName,function(){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError("Use 'new' to construct "+name)}if(undefined===registeredClass.constructor_body){throw new BindingError(name+" has no accessible constructor")}var body=registeredClass.constructor_body[arguments.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${arguments.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,arguments)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){if(registeredClass.baseClass.__derivedClasses===undefined){registeredClass.baseClass.__derivedClasses=[]}registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};var heap32VectorToArray=(count,firstElement)=>{var array=[];for(var i=0;i>2])}return array};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function newFunc(constructor,argumentList){if(!(constructor instanceof Function)){throw new TypeError(`new_ called with constructor type ${typeof constructor} which is not a function`)}var dummy=createNamedFunction(constructor.name||"unknownFunctionName",function(){});dummy.prototype=constructor.prototype;var obj=new dummy;var r=constructor.apply(obj,argumentList);return r instanceof Object?r:obj}function craftInvokerFunction(humanName,argTypes,classType,cppInvokerFunc,cppTargetFunc,isAsync){var argCount=argTypes.length;if(argCount<2){throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!")}var isClassMethodFunc=argTypes[1]!==null&&classType!==null;var needsDestructorStack=false;for(var i=1;i0?", ":"")+argsListWired}invokerFnBody+=(returns||isAsync?"var rv = ":"")+"invoker(fn"+(argsListWired.length>0?", ":"")+argsListWired+");\n";if(needsDestructorStack){invokerFnBody+="runDestructors(destructors);\n"}else{for(var i=isClassMethodFunc?1:2;i{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],function(classType){classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=readLatin1String(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker);whenDependentTypesAreResolved([],[rawClassType],function(classType){classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,function(argTypes){var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};function handleAllocatorInit(){Object.assign(HandleAllocator.prototype,{get(id){return this.allocated[id]},has(id){return this.allocated[id]!==undefined},allocate(handle){var id=this.freelist.pop()||this.allocated.length;this.allocated[id]=handle;return id},free(id){this.allocated[id]=undefined;this.freelist.push(id)}})}function HandleAllocator(){this.allocated=[undefined];this.freelist=[]}var emval_handles=new HandleAllocator;var __emval_decref=handle=>{if(handle>=emval_handles.reserved&&0===--emval_handles.get(handle).refcount){emval_handles.free(handle)}};var count_emval_handles=()=>{var count=0;for(var i=emval_handles.reserved;i{emval_handles.allocated.push({value:undefined},{value:null},{value:true},{value:false});emval_handles.reserved=emval_handles.allocated.length;Module["count_emval_handles"]=count_emval_handles};var Emval={toValue:handle=>{if(!handle){throwBindingError("Cannot use deleted val. handle = "+handle)}return emval_handles.get(handle).value},toHandle:value=>{switch(value){case undefined:return 1;case null:return 2;case true:return 3;case false:return 4;default:{return emval_handles.allocate({refcount:1,value:value})}}}};function simpleReadValueFromPointer(pointer){return this["fromWireType"](HEAP32[pointer>>2])}var __embind_register_emval=(rawType,name)=>{name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},"toWireType":(destructors,value)=>Emval.toHandle(value),"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:null})};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this["fromWireType"](HEAPF32[pointer>>2])};case 8:return function(pointer){return this["fromWireType"](HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":value=>value,"toWireType":(destructors,value)=>value,"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":floatReadValueFromPointer(name,size),destructorFunction:null})};var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer>>0]:pointer=>HEAPU8[pointer>>0];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=readLatin1String(name);if(maxRange===-1){maxRange=4294967295}var fromWireType=value=>value;if(minRange===0){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift}var isUnsignedType=name.includes("unsigned");var checkAssertions=(value,toTypeName)=>{};var toWireType;if(isUnsignedType){toWireType=function(destructors,value){checkAssertions(value,this.name);return value>>>0}}else{toWireType=function(destructors,value){checkAssertions(value,this.name);return value}}registerType(primitiveType,{name:name,"fromWireType":fromWireType,"toWireType":toWireType,"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":decodeMemoryView,"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":decodeMemoryView},{ignoreDuplicateRegistrations:true})};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var __embind_register_std_string=(rawType,name)=>{name=readLatin1String(name);var stdStringIsUTF8=name==="std::string";registerType(rawType,{name:name,"fromWireType":value=>{var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){var decodeStartPtr=payload;for(var i=0;i<=length;++i){var currentBytePtr=payload+i;if(i==length||HEAPU8[currentBytePtr]==0){var maxRead=currentBytePtr-decodeStartPtr;var stringSegment=UTF8ToString(decodeStartPtr,maxRead);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+1}}}else{var a=new Array(length);for(var i=0;i{if(value instanceof ArrayBuffer){value=new Uint8Array(value)}var length;var valueIsOfTypeString=typeof value=="string";if(!(valueIsOfTypeString||value instanceof Uint8Array||value instanceof Uint8ClampedArray||value instanceof Int8Array)){throwBindingError("Cannot pass non-string to std::string")}if(stdStringIsUTF8&&valueIsOfTypeString){length=lengthBytesUTF8(value)}else{length=value.length}var base=_malloc(4+length+1);var ptr=base+4;HEAPU32[base>>2]=length;if(stdStringIsUTF8&&valueIsOfTypeString){stringToUTF8(value,ptr,length+1)}else{if(valueIsOfTypeString){for(var i=0;i255){_free(ptr);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}else{for(var i=0;i_free(ptr)})};var UTF16Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead)=>{var endPtr=ptr;var idx=endPtr>>1;var maxIdx=idx+maxBytesToRead/2;while(!(idx>=maxIdx)&&HEAPU16[idx])++idx;endPtr=idx<<1;if(endPtr-ptr>32&&UTF16Decoder)return UTF16Decoder.decode(HEAPU8.subarray(ptr,endPtr));var str="";for(var i=0;!(i>=maxBytesToRead/2);++i){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)break;str+=String.fromCharCode(codeUnit)}return str};var stringToUTF16=(str,outPtr,maxBytesToWrite)=>{if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead)=>{var i=0;var str="";while(!(i>=maxBytesToRead/4)){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)break;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i=55296&&codeUnit<=57343)++i;len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=readLatin1String(name);var decodeString,encodeString,getHeap,lengthBytesUTF,shift;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16;getHeap=()=>HEAPU16;shift=1}else if(charSize===4){decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32;getHeap=()=>HEAPU32;shift=2}registerType(rawType,{name:name,"fromWireType":value=>{var length=HEAPU32[value>>2];var HEAP=getHeap();var str;var decodeStartPtr=value+4;for(var i=0;i<=length;++i){var currentBytePtr=value+4+i*charSize;if(i==length||HEAP[currentBytePtr>>shift]==0){var maxReadBytes=currentBytePtr-decodeStartPtr;var stringSegment=decodeString(decodeStartPtr,maxReadBytes);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+charSize}}_free(value);return str},"toWireType":(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length>>shift;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},"argPackAdvance":GenericWireTypeSize,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:ptr=>_free(ptr)})};var __embind_register_void=(rawType,name)=>{name=readLatin1String(name);registerType(rawType,{isVoid:true,name:name,"argPackAdvance":0,"fromWireType":()=>undefined,"toWireType":(destructors,o)=>undefined})};var __emscripten_throw_longjmp=()=>{throw Infinity};var __emval_incref=handle=>{if(handle>4){emval_handles.get(handle).refcount+=1}};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(humanName+" has unknown type "+getTypeName(rawType))}return impl};var __emval_take_value=(type,arg)=>{type=requireRegisteredType(type,"_emval_take_value");var v=type["readValueFromPointer"](arg);return Emval.toHandle(v)};var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function __localtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var __mktime_js=function(tmPtr){var ret=(()=>{var date=new Date(HEAP32[tmPtr+20>>2]+1900,HEAP32[tmPtr+16>>2],HEAP32[tmPtr+12>>2],HEAP32[tmPtr+8>>2],HEAP32[tmPtr+4>>2],HEAP32[tmPtr>>2],0);var dst=HEAP32[tmPtr+32>>2];var guessedOffset=date.getTimezoneOffset();var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dstOffset=Math.min(winterOffset,summerOffset);if(dst<0){HEAP32[tmPtr+32>>2]=Number(summerOffset!=winterOffset&&dstOffset==guessedOffset)}else if(dst>0!=(dstOffset==guessedOffset)){var nonDstOffset=Math.max(winterOffset,summerOffset);var trueOffset=dst>0?dstOffset:nonDstOffset;date.setTime(date.getTime()+(trueOffset-guessedOffset)*6e4)}HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getYear();return date.getTime()/1e3})();return setTempRet0((tempDouble=ret,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)),ret>>>0};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var __tzset_js=(timezone,daylight,tzname)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);function extractZone(date){var match=date.toTimeString().match(/\(([A-Za-z ]+)\)$/);return match?match[1]:"GMT"}var winterName=extractZone(winter);var summerName=extractZone(summer);var winterNamePtr=stringToNewUTF8(winterName);var summerNamePtr=stringToNewUTF8(summerName);if(summerOffset>2]=winterNamePtr;HEAPU32[tzname+4>>2]=summerNamePtr}else{HEAPU32[tzname>>2]=summerNamePtr;HEAPU32[tzname+4>>2]=winterNamePtr}};var _abort=()=>{abort("")};var _emscripten_memcpy_big=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={"USER":"web_user","LOGNAME":"web_user","PATH":"/","PWD":"/","HOME":"/home/web_user","LANG":lang,"_":getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i>0]=str.charCodeAt(i)}HEAP8[buffer>>0]=0};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var _fd_close=fd=>52;var _fd_read=(fd,iov,iovcnt,pnum)=>52;function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);return 70}var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};var arraySum=(array,index)=>{var sum=0;for(var i=0;i<=index;sum+=array[i++]){}return sum};var MONTH_DAYS_LEAP=[31,29,31,30,31,30,31,31,30,31,30,31];var MONTH_DAYS_REGULAR=[31,28,31,30,31,30,31,31,30,31,30,31];var addDays=(date,days)=>{var newDate=new Date(date.getTime());while(days>0){var leap=isLeapYear(newDate.getFullYear());var currentMonth=newDate.getMonth();var daysInCurrentMonth=(leap?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR)[currentMonth];if(days>daysInCurrentMonth-newDate.getDate()){days-=daysInCurrentMonth-newDate.getDate()+1;newDate.setDate(1);if(currentMonth<11){newDate.setMonth(currentMonth+1)}else{newDate.setMonth(0);newDate.setFullYear(newDate.getFullYear()+1)}}else{newDate.setDate(newDate.getDate()+days);return newDate}}return newDate};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var _strftime=(s,maxsize,format,tm)=>{var tm_zone=HEAPU32[tm+40>>2];var date={tm_sec:HEAP32[tm>>2],tm_min:HEAP32[tm+4>>2],tm_hour:HEAP32[tm+8>>2],tm_mday:HEAP32[tm+12>>2],tm_mon:HEAP32[tm+16>>2],tm_year:HEAP32[tm+20>>2],tm_wday:HEAP32[tm+24>>2],tm_yday:HEAP32[tm+28>>2],tm_isdst:HEAP32[tm+32>>2],tm_gmtoff:HEAP32[tm+36>>2],tm_zone:tm_zone?UTF8ToString(tm_zone):""};var pattern=UTF8ToString(format);var EXPANSION_RULES_1={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var rule in EXPANSION_RULES_1){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_1[rule])}var WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];function leadingSomething(value,digits,character){var str=typeof value=="number"?value.toString():value||"";while(str.length0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={"%a":date=>WEEKDAYS[date.tm_wday].substring(0,3),"%A":date=>WEEKDAYS[date.tm_wday],"%b":date=>MONTHS[date.tm_mon].substring(0,3),"%B":date=>MONTHS[date.tm_mon],"%C":date=>{var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},"%d":date=>leadingNulls(date.tm_mday,2),"%e":date=>leadingSomething(date.tm_mday,2," "),"%g":date=>getWeekBasedYear(date).toString().substring(2),"%G":date=>getWeekBasedYear(date),"%H":date=>leadingNulls(date.tm_hour,2),"%I":date=>{var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},"%j":date=>leadingNulls(date.tm_mday+arraySum(isLeapYear(date.tm_year+1900)?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR,date.tm_mon-1),3),"%m":date=>leadingNulls(date.tm_mon+1,2),"%M":date=>leadingNulls(date.tm_min,2),"%n":()=>"\n","%p":date=>{if(date.tm_hour>=0&&date.tm_hour<12){return"AM"}return"PM"},"%S":date=>leadingNulls(date.tm_sec,2),"%t":()=>"\t","%u":date=>date.tm_wday||7,"%U":date=>{var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},"%V":date=>{var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},"%w":date=>date.tm_wday,"%W":date=>{var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},"%y":date=>(date.tm_year+1900).toString().substring(2),"%Y":date=>date.tm_year+1900,"%z":date=>{var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?"+":"-")+String("0000"+off).slice(-4)},"%Z":date=>date.tm_zone,"%%":()=>"%"};pattern=pattern.replace(/%%/g,"\0\0");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\0\0/g,"%");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1};var _strftime_l=(s,maxsize,format,tm,loc)=>_strftime(s,maxsize,format,tm);embind_init_charCodes();BindingError=Module["BindingError"]=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};InternalError=Module["InternalError"]=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};init_ClassHandle();init_embind();init_RegisteredPointer();UnboundTypeError=Module["UnboundTypeError"]=extendError(Error,"UnboundTypeError");handleAllocatorInit();init_emval();var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,__syscall_ioctl:___syscall_ioctl,__syscall_openat:___syscall_openat,_embind_register_bigint:__embind_register_bigint,_embind_register_bool:__embind_register_bool,_embind_register_class:__embind_register_class,_embind_register_class_constructor:__embind_register_class_constructor,_embind_register_class_function:__embind_register_class_function,_embind_register_emval:__embind_register_emval,_embind_register_float:__embind_register_float,_embind_register_integer:__embind_register_integer,_embind_register_memory_view:__embind_register_memory_view,_embind_register_std_string:__embind_register_std_string,_embind_register_std_wstring:__embind_register_std_wstring,_embind_register_void:__embind_register_void,_emscripten_throw_longjmp:__emscripten_throw_longjmp,_emval_decref:__emval_decref,_emval_incref:__emval_incref,_emval_take_value:__emval_take_value,_localtime_js:__localtime_js,_mktime_js:__mktime_js,_tzset_js:__tzset_js,abort:_abort,emscripten_memcpy_big:_emscripten_memcpy_big,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,invoke_vi:invoke_vi,invoke_viii:invoke_viii,invoke_viiii:invoke_viiii,invoke_viiiii:invoke_viiiii,strftime_l:_strftime_l};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["__wasm_call_ctors"])();var _free=a0=>(_free=wasmExports["free"])(a0);var _malloc=a0=>(_malloc=wasmExports["malloc"])(a0);var ___getTypeName=a0=>(___getTypeName=wasmExports["__getTypeName"])(a0);var __embind_initialize_bindings=Module["__embind_initialize_bindings"]=()=>(__embind_initialize_bindings=Module["__embind_initialize_bindings"]=wasmExports["_embind_initialize_bindings"])();var ___errno_location=()=>(___errno_location=wasmExports["__errno_location"])();var _setThrew=(a0,a1)=>(_setThrew=wasmExports["setThrew"])(a0,a1);var setTempRet0=a0=>(setTempRet0=wasmExports["setTempRet0"])(a0);var stackSave=()=>(stackSave=wasmExports["stackSave"])();var stackRestore=a0=>(stackRestore=wasmExports["stackRestore"])(a0);var stackAlloc=a0=>(stackAlloc=wasmExports["stackAlloc"])(a0);var ___cxa_increment_exception_refcount=a0=>(___cxa_increment_exception_refcount=wasmExports["__cxa_increment_exception_refcount"])(a0);var ___cxa_is_pointer_type=a0=>(___cxa_is_pointer_type=wasmExports["__cxa_is_pointer_type"])(a0);var dynCall_iijii=Module["dynCall_iijii"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_iijii=Module["dynCall_iijii"]=wasmExports["dynCall_iijii"])(a0,a1,a2,a3,a4,a5);var dynCall_jiji=Module["dynCall_jiji"]=(a0,a1,a2,a3,a4)=>(dynCall_jiji=Module["dynCall_jiji"]=wasmExports["dynCall_jiji"])(a0,a1,a2,a3,a4);var dynCall_viijii=Module["dynCall_viijii"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_viijii=Module["dynCall_viijii"]=wasmExports["dynCall_viijii"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_iiiiij=Module["dynCall_iiiiij"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_iiiiij=Module["dynCall_iiiiij"]=wasmExports["dynCall_iiiiij"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8)=>(dynCall_iiiiijj=Module["dynCall_iiiiijj"]=wasmExports["dynCall_iiiiijj"])(a0,a1,a2,a3,a4,a5,a6,a7,a8);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9)=>(dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=wasmExports["dynCall_iiiiiijj"])(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9);function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); +var Module = typeof Module != "undefined" ? Module : {}; + +var moduleOverrides = Object.assign({}, Module); + +var arguments_ = []; + +var thisProgram = "./this.program"; + +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +var ENVIRONMENT_IS_WEB = false; + +var ENVIRONMENT_IS_WORKER = false; + +var ENVIRONMENT_IS_NODE = true; + +var scriptDirectory = ""; + +function locateFile(path) { + if (Module["locateFile"]) { + return Module["locateFile"](path, scriptDirectory); + } + return scriptDirectory + path; +} + +var read_, readAsync, readBinary; + +if (ENVIRONMENT_IS_NODE) { + var fs = require("fs"); + var nodePath = require("path"); + if (ENVIRONMENT_IS_WORKER) { + scriptDirectory = nodePath.dirname(scriptDirectory) + "/"; + } else { + scriptDirectory = __dirname + "/"; + } + read_ = (filename, binary) => { + filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); + return fs.readFileSync(filename, binary ? undefined : "utf8"); + }; + readBinary = filename => { + var ret = read_(filename, true); + if (!ret.buffer) { + ret = new Uint8Array(ret); + } + return ret; + }; + readAsync = (filename, onload, onerror, binary = true) => { + filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); + fs.readFile(filename, binary ? undefined : "utf8", (err, data) => { + if (err) onerror(err); else onload(binary ? data.buffer : data); + }); + }; + if (!Module["thisProgram"] && process.argv.length > 1) { + thisProgram = process.argv[1].replace(/\\/g, "/"); + } + arguments_ = process.argv.slice(2); + if (typeof module != "undefined") { + module["exports"] = Module; + } + quit_ = (status, toThrow) => { + process.exitCode = status; + throw toThrow; + }; + Module["inspect"] = () => "[Emscripten Module object]"; +} else {} + +var out = Module["print"] || console.log.bind(console); + +var err = Module["printErr"] || console.error.bind(console); + +Object.assign(Module, moduleOverrides); + +moduleOverrides = null; + +if (Module["arguments"]) arguments_ = Module["arguments"]; + +if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; + +if (Module["quit"]) quit_ = Module["quit"]; + +var wasmBinary; + +if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; + +var noExitRuntime = Module["noExitRuntime"] || true; + +if (typeof WebAssembly != "object") { + abort("no native wasm support detected"); +} + +var wasmMemory; + +var ABORT = false; + +var EXITSTATUS; + +var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; + +function updateMemoryViews() { + var b = wasmMemory.buffer; + Module["HEAP8"] = HEAP8 = new Int8Array(b); + Module["HEAP16"] = HEAP16 = new Int16Array(b); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(b); + Module["HEAPU16"] = HEAPU16 = new Uint16Array(b); + Module["HEAP32"] = HEAP32 = new Int32Array(b); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(b); + Module["HEAPF32"] = HEAPF32 = new Float32Array(b); + Module["HEAPF64"] = HEAPF64 = new Float64Array(b); +} + +var wasmTable; + +var __ATPRERUN__ = []; + +var __ATINIT__ = []; + +var __ATPOSTRUN__ = []; + +var runtimeInitialized = false; + +var runtimeKeepaliveCounter = 0; + +function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()); + } + } + callRuntimeCallbacks(__ATPRERUN__); +} + +function initRuntime() { + runtimeInitialized = true; + callRuntimeCallbacks(__ATINIT__); +} + +function postRun() { + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()); + } + } + callRuntimeCallbacks(__ATPOSTRUN__); +} + +function addOnPreRun(cb) { + __ATPRERUN__.unshift(cb); +} + +function addOnInit(cb) { + __ATINIT__.unshift(cb); +} + +function addOnPostRun(cb) { + __ATPOSTRUN__.unshift(cb); +} + +var runDependencies = 0; + +var runDependencyWatcher = null; + +var dependenciesFulfilled = null; + +function addRunDependency(id) { + runDependencies++; + if (Module["monitorRunDependencies"]) { + Module["monitorRunDependencies"](runDependencies); + } +} + +function removeRunDependency(id) { + runDependencies--; + if (Module["monitorRunDependencies"]) { + Module["monitorRunDependencies"](runDependencies); + } + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); + } + } +} + +function abort(what) { + if (Module["onAbort"]) { + Module["onAbort"](what); + } + what = "Aborted(" + what + ")"; + err(what); + ABORT = true; + EXITSTATUS = 1; + what += ". Build with -sASSERTIONS for more info."; + var e = new WebAssembly.RuntimeError(what); + throw e; +} + +var dataURIPrefix = "data:application/octet-stream;base64,"; + +function isDataURI(filename) { + return filename.startsWith(dataURIPrefix); +} + +function isFileURI(filename) { + return filename.startsWith("file://"); +} + +var wasmBinaryFile; + +wasmBinaryFile = "rlottie-wasm.wasm"; + +if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile); +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + throw "both async and sync fetching of the wasm failed"; +} + +function getBinaryPromise(binaryFile) { + if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) { + if (typeof fetch == "function") { + return fetch(binaryFile, { + credentials: "same-origin" + }).then(response => { + if (!response["ok"]) { + throw "failed to load wasm binary file at '" + binaryFile + "'"; + } + return response["arrayBuffer"](); + }).catch(() => getBinarySync(binaryFile)); + } + } + return Promise.resolve().then(() => getBinarySync(binaryFile)); +} + +function instantiateArrayBuffer(binaryFile, imports, receiver) { + return getBinaryPromise(binaryFile).then(binary => WebAssembly.instantiate(binary, imports)).then(instance => instance).then(receiver, reason => { + err(`failed to asynchronously prepare wasm: ${reason}`); + abort(reason); + }); +} + +function instantiateAsync(binary, binaryFile, imports, callback) { + if (!binary && typeof WebAssembly.instantiateStreaming == "function" && !isDataURI(binaryFile) && !ENVIRONMENT_IS_NODE && typeof fetch == "function") { + return fetch(binaryFile, { + credentials: "same-origin" + }).then(response => { + var result = WebAssembly.instantiateStreaming(response, imports); + return result.then(callback, function(reason) { + err(`wasm streaming compile failed: ${reason}`); + err("falling back to ArrayBuffer instantiation"); + return instantiateArrayBuffer(binaryFile, imports, callback); + }); + }); + } + return instantiateArrayBuffer(binaryFile, imports, callback); +} + +function createWasm() { + var info = { + "env": wasmImports, + "wasi_snapshot_preview1": wasmImports + }; + function receiveInstance(instance, module) { + var exports = instance.exports; + wasmExports = exports; + wasmMemory = wasmExports["memory"]; + updateMemoryViews(); + wasmTable = wasmExports["__indirect_function_table"]; + addOnInit(wasmExports["__wasm_call_ctors"]); + removeRunDependency("wasm-instantiate"); + return exports; + } + addRunDependency("wasm-instantiate"); + function receiveInstantiationResult(result) { + receiveInstance(result["instance"]); + } + if (Module["instantiateWasm"]) { + try { + return Module["instantiateWasm"](info, receiveInstance); + } catch (e) { + err(`Module.instantiateWasm callback failed with error: ${e}`); + return false; + } + } + instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult); + return {}; +} + +var tempDouble; + +function ExitStatus(status) { + this.name = "ExitStatus"; + this.message = `Program terminated with exit(${status})`; + this.status = status; +} + +var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + callbacks.shift()(Module); + } +}; + +var UTF8Decoder = typeof TextDecoder != "undefined" ? new TextDecoder("utf8") : undefined; + +var UTF8ArrayToString = (heapOrArray, idx, maxBytesToRead) => { + var endIdx = idx + maxBytesToRead; + var endPtr = idx; + while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; + if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { + return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); + } + var str = ""; + while (idx < endPtr) { + var u0 = heapOrArray[idx++]; + if (!(u0 & 128)) { + str += String.fromCharCode(u0); + continue; + } + var u1 = heapOrArray[idx++] & 63; + if ((u0 & 224) == 192) { + str += String.fromCharCode((u0 & 31) << 6 | u1); + continue; + } + var u2 = heapOrArray[idx++] & 63; + if ((u0 & 240) == 224) { + u0 = (u0 & 15) << 12 | u1 << 6 | u2; + } else { + u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63; + } + if (u0 < 65536) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 65536; + str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023); + } + } + return str; +}; + +var UTF8ToString = (ptr, maxBytesToRead) => ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ""; + +var ___assert_fail = (condition, filename, line, func) => { + abort(`Assertion failed: ${UTF8ToString(condition)}, at: ` + [ filename ? UTF8ToString(filename) : "unknown filename", line, func ? UTF8ToString(func) : "unknown function" ]); +}; + +function ExceptionInfo(excPtr) { + this.excPtr = excPtr; + this.ptr = excPtr - 24; + this.set_type = function(type) { + HEAPU32[this.ptr + 4 >> 2] = type; + }; + this.get_type = function() { + return HEAPU32[this.ptr + 4 >> 2]; + }; + this.set_destructor = function(destructor) { + HEAPU32[this.ptr + 8 >> 2] = destructor; + }; + this.get_destructor = function() { + return HEAPU32[this.ptr + 8 >> 2]; + }; + this.set_caught = function(caught) { + caught = caught ? 1 : 0; + HEAP8[this.ptr + 12 >> 0] = caught; + }; + this.get_caught = function() { + return HEAP8[this.ptr + 12 >> 0] != 0; + }; + this.set_rethrown = function(rethrown) { + rethrown = rethrown ? 1 : 0; + HEAP8[this.ptr + 13 >> 0] = rethrown; + }; + this.get_rethrown = function() { + return HEAP8[this.ptr + 13 >> 0] != 0; + }; + this.init = function(type, destructor) { + this.set_adjusted_ptr(0); + this.set_type(type); + this.set_destructor(destructor); + }; + this.set_adjusted_ptr = function(adjustedPtr) { + HEAPU32[this.ptr + 16 >> 2] = adjustedPtr; + }; + this.get_adjusted_ptr = function() { + return HEAPU32[this.ptr + 16 >> 2]; + }; + this.get_exception_ptr = function() { + var isPointer = ___cxa_is_pointer_type(this.get_type()); + if (isPointer) { + return HEAPU32[this.excPtr >> 2]; + } + var adjusted = this.get_adjusted_ptr(); + if (adjusted !== 0) return adjusted; + return this.excPtr; + }; +} + +var exceptionLast = 0; + +var uncaughtExceptionCount = 0; + +var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + info.init(type, destructor); + exceptionLast = ptr; + uncaughtExceptionCount++; + throw exceptionLast; +}; + +var SYSCALLS = { + varargs: undefined, + get() { + var ret = HEAP32[SYSCALLS.varargs >> 2]; + SYSCALLS.varargs += 4; + return ret; + }, + getp() { + return SYSCALLS.get(); + }, + getStr(ptr) { + var ret = UTF8ToString(ptr); + return ret; + } +}; + +function ___syscall_ioctl(fd, op, varargs) { + SYSCALLS.varargs = varargs; + return 0; +} + +function ___syscall_openat(dirfd, path, flags, varargs) { + SYSCALLS.varargs = varargs; +} + +var __embind_register_bigint = (primitiveType, name, size, minRange, maxRange) => {}; + +var embind_init_charCodes = () => { + var codes = new Array(256); + for (var i = 0; i < 256; ++i) { + codes[i] = String.fromCharCode(i); + } + embind_charCodes = codes; +}; + +var embind_charCodes = undefined; + +var readLatin1String = ptr => { + var ret = ""; + var c = ptr; + while (HEAPU8[c]) { + ret += embind_charCodes[HEAPU8[c++]]; + } + return ret; +}; + +var awaitingDependencies = {}; + +var registeredTypes = {}; + +var typeDependencies = {}; + +var BindingError = undefined; + +var throwBindingError = message => { + throw new BindingError(message); +}; + +var InternalError = undefined; + +var throwInternalError = message => { + throw new InternalError(message); +}; + +var whenDependentTypesAreResolved = (myTypes, dependentTypes, getTypeConverters) => { + myTypes.forEach(function(type) { + typeDependencies[type] = dependentTypes; + }); + function onComplete(typeConverters) { + var myTypeConverters = getTypeConverters(typeConverters); + if (myTypeConverters.length !== myTypes.length) { + throwInternalError("Mismatched type converter count"); + } + for (var i = 0; i < myTypes.length; ++i) { + registerType(myTypes[i], myTypeConverters[i]); + } + } + var typeConverters = new Array(dependentTypes.length); + var unregisteredTypes = []; + var registered = 0; + dependentTypes.forEach((dt, i) => { + if (registeredTypes.hasOwnProperty(dt)) { + typeConverters[i] = registeredTypes[dt]; + } else { + unregisteredTypes.push(dt); + if (!awaitingDependencies.hasOwnProperty(dt)) { + awaitingDependencies[dt] = []; + } + awaitingDependencies[dt].push(() => { + typeConverters[i] = registeredTypes[dt]; + ++registered; + if (registered === unregisteredTypes.length) { + onComplete(typeConverters); + } + }); + } + }); + if (0 === unregisteredTypes.length) { + onComplete(typeConverters); + } +}; + +function sharedRegisterType(rawType, registeredInstance, options = {}) { + var name = registeredInstance.name; + if (!rawType) { + throwBindingError(`type "${name}" must have a positive integer typeid pointer`); + } + if (registeredTypes.hasOwnProperty(rawType)) { + if (options.ignoreDuplicateRegistrations) { + return; + } else { + throwBindingError(`Cannot register type '${name}' twice`); + } + } + registeredTypes[rawType] = registeredInstance; + delete typeDependencies[rawType]; + if (awaitingDependencies.hasOwnProperty(rawType)) { + var callbacks = awaitingDependencies[rawType]; + delete awaitingDependencies[rawType]; + callbacks.forEach(cb => cb()); + } +} + +function registerType(rawType, registeredInstance, options = {}) { + if (!("argPackAdvance" in registeredInstance)) { + throw new TypeError("registerType registeredInstance requires argPackAdvance"); + } + return sharedRegisterType(rawType, registeredInstance, options); +} + +var GenericWireTypeSize = 8; + +var __embind_register_bool = (rawType, name, trueValue, falseValue) => { + name = readLatin1String(name); + registerType(rawType, { + name: name, + "fromWireType": function(wt) { + return !!wt; + }, + "toWireType": function(destructors, o) { + return o ? trueValue : falseValue; + }, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": function(pointer) { + return this["fromWireType"](HEAPU8[pointer]); + }, + destructorFunction: null + }); +}; + +function ClassHandle_isAliasOf(other) { + if (!(this instanceof ClassHandle)) { + return false; + } + if (!(other instanceof ClassHandle)) { + return false; + } + var leftClass = this.$$.ptrType.registeredClass; + var left = this.$$.ptr; + var rightClass = other.$$.ptrType.registeredClass; + var right = other.$$.ptr; + while (leftClass.baseClass) { + left = leftClass.upcast(left); + leftClass = leftClass.baseClass; + } + while (rightClass.baseClass) { + right = rightClass.upcast(right); + rightClass = rightClass.baseClass; + } + return leftClass === rightClass && left === right; +} + +var shallowCopyInternalPointer = o => ({ + count: o.count, + deleteScheduled: o.deleteScheduled, + preservePointerOnDelete: o.preservePointerOnDelete, + ptr: o.ptr, + ptrType: o.ptrType, + smartPtr: o.smartPtr, + smartPtrType: o.smartPtrType +}); + +var throwInstanceAlreadyDeleted = obj => { + function getInstanceTypeName(handle) { + return handle.$$.ptrType.registeredClass.name; + } + throwBindingError(getInstanceTypeName(obj) + " instance already deleted"); +}; + +var finalizationRegistry = false; + +var detachFinalizer = handle => {}; + +var runDestructor = $$ => { + if ($$.smartPtr) { + $$.smartPtrType.rawDestructor($$.smartPtr); + } else { + $$.ptrType.registeredClass.rawDestructor($$.ptr); + } +}; + +var releaseClassHandle = $$ => { + $$.count.value -= 1; + var toDelete = 0 === $$.count.value; + if (toDelete) { + runDestructor($$); + } +}; + +var downcastPointer = (ptr, ptrClass, desiredClass) => { + if (ptrClass === desiredClass) { + return ptr; + } + if (undefined === desiredClass.baseClass) { + return null; + } + var rv = downcastPointer(ptr, ptrClass, desiredClass.baseClass); + if (rv === null) { + return null; + } + return desiredClass.downcast(rv); +}; + +var registeredPointers = {}; + +var getInheritedInstanceCount = () => Object.keys(registeredInstances).length; + +var getLiveInheritedInstances = () => { + var rv = []; + for (var k in registeredInstances) { + if (registeredInstances.hasOwnProperty(k)) { + rv.push(registeredInstances[k]); + } + } + return rv; +}; + +var deletionQueue = []; + +var flushPendingDeletes = () => { + while (deletionQueue.length) { + var obj = deletionQueue.pop(); + obj.$$.deleteScheduled = false; + obj["delete"](); + } +}; + +var delayFunction = undefined; + +var setDelayFunction = fn => { + delayFunction = fn; + if (deletionQueue.length && delayFunction) { + delayFunction(flushPendingDeletes); + } +}; + +var init_embind = () => { + Module["getInheritedInstanceCount"] = getInheritedInstanceCount; + Module["getLiveInheritedInstances"] = getLiveInheritedInstances; + Module["flushPendingDeletes"] = flushPendingDeletes; + Module["setDelayFunction"] = setDelayFunction; +}; + +var registeredInstances = {}; + +var getBasestPointer = (class_, ptr) => { + if (ptr === undefined) { + throwBindingError("ptr should not be undefined"); + } + while (class_.baseClass) { + ptr = class_.upcast(ptr); + class_ = class_.baseClass; + } + return ptr; +}; + +var getInheritedInstance = (class_, ptr) => { + ptr = getBasestPointer(class_, ptr); + return registeredInstances[ptr]; +}; + +var makeClassHandle = (prototype, record) => { + if (!record.ptrType || !record.ptr) { + throwInternalError("makeClassHandle requires ptr and ptrType"); + } + var hasSmartPtrType = !!record.smartPtrType; + var hasSmartPtr = !!record.smartPtr; + if (hasSmartPtrType !== hasSmartPtr) { + throwInternalError("Both smartPtrType and smartPtr must be specified"); + } + record.count = { + value: 1 + }; + return attachFinalizer(Object.create(prototype, { + $$: { + value: record + } + })); +}; + +function RegisteredPointer_fromWireType(ptr) { + var rawPointer = this.getPointee(ptr); + if (!rawPointer) { + this.destructor(ptr); + return null; + } + var registeredInstance = getInheritedInstance(this.registeredClass, rawPointer); + if (undefined !== registeredInstance) { + if (0 === registeredInstance.$$.count.value) { + registeredInstance.$$.ptr = rawPointer; + registeredInstance.$$.smartPtr = ptr; + return registeredInstance["clone"](); + } else { + var rv = registeredInstance["clone"](); + this.destructor(ptr); + return rv; + } + } + function makeDefaultHandle() { + if (this.isSmartPointer) { + return makeClassHandle(this.registeredClass.instancePrototype, { + ptrType: this.pointeeType, + ptr: rawPointer, + smartPtrType: this, + smartPtr: ptr + }); + } else { + return makeClassHandle(this.registeredClass.instancePrototype, { + ptrType: this, + ptr: ptr + }); + } + } + var actualType = this.registeredClass.getActualType(rawPointer); + var registeredPointerRecord = registeredPointers[actualType]; + if (!registeredPointerRecord) { + return makeDefaultHandle.call(this); + } + var toType; + if (this.isConst) { + toType = registeredPointerRecord.constPointerType; + } else { + toType = registeredPointerRecord.pointerType; + } + var dp = downcastPointer(rawPointer, this.registeredClass, toType.registeredClass); + if (dp === null) { + return makeDefaultHandle.call(this); + } + if (this.isSmartPointer) { + return makeClassHandle(toType.registeredClass.instancePrototype, { + ptrType: toType, + ptr: dp, + smartPtrType: this, + smartPtr: ptr + }); + } else { + return makeClassHandle(toType.registeredClass.instancePrototype, { + ptrType: toType, + ptr: dp + }); + } +} + +var attachFinalizer = handle => { + if ("undefined" === typeof FinalizationRegistry) { + attachFinalizer = handle => handle; + return handle; + } + finalizationRegistry = new FinalizationRegistry(info => { + releaseClassHandle(info.$$); + }); + attachFinalizer = handle => { + var $$ = handle.$$; + var hasSmartPtr = !!$$.smartPtr; + if (hasSmartPtr) { + var info = { + $$: $$ + }; + finalizationRegistry.register(handle, info, handle); + } + return handle; + }; + detachFinalizer = handle => finalizationRegistry.unregister(handle); + return attachFinalizer(handle); +}; + +function ClassHandle_clone() { + if (!this.$$.ptr) { + throwInstanceAlreadyDeleted(this); + } + if (this.$$.preservePointerOnDelete) { + this.$$.count.value += 1; + return this; + } else { + var clone = attachFinalizer(Object.create(Object.getPrototypeOf(this), { + $$: { + value: shallowCopyInternalPointer(this.$$) + } + })); + clone.$$.count.value += 1; + clone.$$.deleteScheduled = false; + return clone; + } +} + +function ClassHandle_delete() { + if (!this.$$.ptr) { + throwInstanceAlreadyDeleted(this); + } + if (this.$$.deleteScheduled && !this.$$.preservePointerOnDelete) { + throwBindingError("Object already scheduled for deletion"); + } + detachFinalizer(this); + releaseClassHandle(this.$$); + if (!this.$$.preservePointerOnDelete) { + this.$$.smartPtr = undefined; + this.$$.ptr = undefined; + } +} + +function ClassHandle_isDeleted() { + return !this.$$.ptr; +} + +function ClassHandle_deleteLater() { + if (!this.$$.ptr) { + throwInstanceAlreadyDeleted(this); + } + if (this.$$.deleteScheduled && !this.$$.preservePointerOnDelete) { + throwBindingError("Object already scheduled for deletion"); + } + deletionQueue.push(this); + if (deletionQueue.length === 1 && delayFunction) { + delayFunction(flushPendingDeletes); + } + this.$$.deleteScheduled = true; + return this; +} + +var init_ClassHandle = () => { + ClassHandle.prototype["isAliasOf"] = ClassHandle_isAliasOf; + ClassHandle.prototype["clone"] = ClassHandle_clone; + ClassHandle.prototype["delete"] = ClassHandle_delete; + ClassHandle.prototype["isDeleted"] = ClassHandle_isDeleted; + ClassHandle.prototype["deleteLater"] = ClassHandle_deleteLater; +}; + +function ClassHandle() {} + +var char_0 = 48; + +var char_9 = 57; + +var makeLegalFunctionName = name => { + if (undefined === name) { + return "_unknown"; + } + name = name.replace(/[^a-zA-Z0-9_]/g, "$"); + var f = name.charCodeAt(0); + if (f >= char_0 && f <= char_9) { + return `_${name}`; + } + return name; +}; + +function createNamedFunction(name, body) { + name = makeLegalFunctionName(name); + return { + [name]: function() { + return body.apply(this, arguments); + } + }[name]; +} + +var ensureOverloadTable = (proto, methodName, humanName) => { + if (undefined === proto[methodName].overloadTable) { + var prevFunc = proto[methodName]; + proto[methodName] = function() { + if (!proto[methodName].overloadTable.hasOwnProperty(arguments.length)) { + throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${arguments.length}) - expects one of (${proto[methodName].overloadTable})!`); + } + return proto[methodName].overloadTable[arguments.length].apply(this, arguments); + }; + proto[methodName].overloadTable = []; + proto[methodName].overloadTable[prevFunc.argCount] = prevFunc; + } +}; + +var exposePublicSymbol = (name, value, numArguments) => { + if (Module.hasOwnProperty(name)) { + if (undefined === numArguments || undefined !== Module[name].overloadTable && undefined !== Module[name].overloadTable[numArguments]) { + throwBindingError(`Cannot register public name '${name}' twice`); + } + ensureOverloadTable(Module, name, name); + if (Module.hasOwnProperty(numArguments)) { + throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`); + } + Module[name].overloadTable[numArguments] = value; + } else { + Module[name] = value; + if (undefined !== numArguments) { + Module[name].numArguments = numArguments; + } + } +}; + +function RegisteredClass(name, constructor, instancePrototype, rawDestructor, baseClass, getActualType, upcast, downcast) { + this.name = name; + this.constructor = constructor; + this.instancePrototype = instancePrototype; + this.rawDestructor = rawDestructor; + this.baseClass = baseClass; + this.getActualType = getActualType; + this.upcast = upcast; + this.downcast = downcast; + this.pureVirtualFunctions = []; +} + +var upcastPointer = (ptr, ptrClass, desiredClass) => { + while (ptrClass !== desiredClass) { + if (!ptrClass.upcast) { + throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`); + } + ptr = ptrClass.upcast(ptr); + ptrClass = ptrClass.baseClass; + } + return ptr; +}; + +function constNoSmartPtrRawPointerToWireType(destructors, handle) { + if (handle === null) { + if (this.isReference) { + throwBindingError(`null is not a valid ${this.name}`); + } + return 0; + } + if (!handle.$$) { + throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`); + } + if (!handle.$$.ptr) { + throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`); + } + var handleClass = handle.$$.ptrType.registeredClass; + var ptr = upcastPointer(handle.$$.ptr, handleClass, this.registeredClass); + return ptr; +} + +function genericPointerToWireType(destructors, handle) { + var ptr; + if (handle === null) { + if (this.isReference) { + throwBindingError(`null is not a valid ${this.name}`); + } + if (this.isSmartPointer) { + ptr = this.rawConstructor(); + if (destructors !== null) { + destructors.push(this.rawDestructor, ptr); + } + return ptr; + } else { + return 0; + } + } + if (!handle.$$) { + throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`); + } + if (!handle.$$.ptr) { + throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`); + } + if (!this.isConst && handle.$$.ptrType.isConst) { + throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType ? handle.$$.smartPtrType.name : handle.$$.ptrType.name} to parameter type ${this.name}`); + } + var handleClass = handle.$$.ptrType.registeredClass; + ptr = upcastPointer(handle.$$.ptr, handleClass, this.registeredClass); + if (this.isSmartPointer) { + if (undefined === handle.$$.smartPtr) { + throwBindingError("Passing raw pointer to smart pointer is illegal"); + } + switch (this.sharingPolicy) { + case 0: + if (handle.$$.smartPtrType === this) { + ptr = handle.$$.smartPtr; + } else { + throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType ? handle.$$.smartPtrType.name : handle.$$.ptrType.name} to parameter type ${this.name}`); + } + break; + + case 1: + ptr = handle.$$.smartPtr; + break; + + case 2: + if (handle.$$.smartPtrType === this) { + ptr = handle.$$.smartPtr; + } else { + var clonedHandle = handle["clone"](); + ptr = this.rawShare(ptr, Emval.toHandle(() => clonedHandle["delete"]())); + if (destructors !== null) { + destructors.push(this.rawDestructor, ptr); + } + } + break; + + default: + throwBindingError("Unsupporting sharing policy"); + } + } + return ptr; +} + +function nonConstNoSmartPtrRawPointerToWireType(destructors, handle) { + if (handle === null) { + if (this.isReference) { + throwBindingError(`null is not a valid ${this.name}`); + } + return 0; + } + if (!handle.$$) { + throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`); + } + if (!handle.$$.ptr) { + throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`); + } + if (handle.$$.ptrType.isConst) { + throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`); + } + var handleClass = handle.$$.ptrType.registeredClass; + var ptr = upcastPointer(handle.$$.ptr, handleClass, this.registeredClass); + return ptr; +} + +function readPointer(pointer) { + return this["fromWireType"](HEAPU32[pointer >> 2]); +} + +function RegisteredPointer_getPointee(ptr) { + if (this.rawGetPointee) { + ptr = this.rawGetPointee(ptr); + } + return ptr; +} + +function RegisteredPointer_destructor(ptr) { + if (this.rawDestructor) { + this.rawDestructor(ptr); + } +} + +var RegisteredPointer_deleteObject = handle => { + if (handle !== null) { + handle["delete"](); + } +}; + +var init_RegisteredPointer = () => { + RegisteredPointer.prototype.getPointee = RegisteredPointer_getPointee; + RegisteredPointer.prototype.destructor = RegisteredPointer_destructor; + RegisteredPointer.prototype["argPackAdvance"] = GenericWireTypeSize; + RegisteredPointer.prototype["readValueFromPointer"] = readPointer; + RegisteredPointer.prototype["deleteObject"] = RegisteredPointer_deleteObject; + RegisteredPointer.prototype["fromWireType"] = RegisteredPointer_fromWireType; +}; + +function RegisteredPointer(name, registeredClass, isReference, isConst, isSmartPointer, pointeeType, sharingPolicy, rawGetPointee, rawConstructor, rawShare, rawDestructor) { + this.name = name; + this.registeredClass = registeredClass; + this.isReference = isReference; + this.isConst = isConst; + this.isSmartPointer = isSmartPointer; + this.pointeeType = pointeeType; + this.sharingPolicy = sharingPolicy; + this.rawGetPointee = rawGetPointee; + this.rawConstructor = rawConstructor; + this.rawShare = rawShare; + this.rawDestructor = rawDestructor; + if (!isSmartPointer && registeredClass.baseClass === undefined) { + if (isConst) { + this["toWireType"] = constNoSmartPtrRawPointerToWireType; + this.destructorFunction = null; + } else { + this["toWireType"] = nonConstNoSmartPtrRawPointerToWireType; + this.destructorFunction = null; + } + } else { + this["toWireType"] = genericPointerToWireType; + } +} + +var replacePublicSymbol = (name, value, numArguments) => { + if (!Module.hasOwnProperty(name)) { + throwInternalError("Replacing nonexistant public symbol"); + } + if (undefined !== Module[name].overloadTable && undefined !== numArguments) { + Module[name].overloadTable[numArguments] = value; + } else { + Module[name] = value; + Module[name].argCount = numArguments; + } +}; + +var dynCallLegacy = (sig, ptr, args) => { + var f = Module["dynCall_" + sig]; + return args && args.length ? f.apply(null, [ ptr ].concat(args)) : f.call(null, ptr); +}; + +var wasmTableMirror = []; + +var getWasmTableEntry = funcPtr => { + var func = wasmTableMirror[funcPtr]; + if (!func) { + if (funcPtr >= wasmTableMirror.length) wasmTableMirror.length = funcPtr + 1; + wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); + } + return func; +}; + +var dynCall = (sig, ptr, args) => { + if (sig.includes("j")) { + return dynCallLegacy(sig, ptr, args); + } + var rtn = getWasmTableEntry(ptr).apply(null, args); + return rtn; +}; + +var getDynCaller = (sig, ptr) => { + var argCache = []; + return function() { + argCache.length = 0; + Object.assign(argCache, arguments); + return dynCall(sig, ptr, argCache); + }; +}; + +var embind__requireFunction = (signature, rawFunction) => { + signature = readLatin1String(signature); + function makeDynCaller() { + if (signature.includes("j")) { + return getDynCaller(signature, rawFunction); + } + return getWasmTableEntry(rawFunction); + } + var fp = makeDynCaller(); + if (typeof fp != "function") { + throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`); + } + return fp; +}; + +var extendError = (baseErrorType, errorName) => { + var errorClass = createNamedFunction(errorName, function(message) { + this.name = errorName; + this.message = message; + var stack = new Error(message).stack; + if (stack !== undefined) { + this.stack = this.toString() + "\n" + stack.replace(/^Error(:[^\n]*)?\n/, ""); + } + }); + errorClass.prototype = Object.create(baseErrorType.prototype); + errorClass.prototype.constructor = errorClass; + errorClass.prototype.toString = function() { + if (this.message === undefined) { + return this.name; + } else { + return `${this.name}: ${this.message}`; + } + }; + return errorClass; +}; + +var UnboundTypeError = undefined; + +var getTypeName = type => { + var ptr = ___getTypeName(type); + var rv = readLatin1String(ptr); + _free(ptr); + return rv; +}; + +var throwUnboundTypeError = (message, types) => { + var unboundTypes = []; + var seen = {}; + function visit(type) { + if (seen[type]) { + return; + } + if (registeredTypes[type]) { + return; + } + if (typeDependencies[type]) { + typeDependencies[type].forEach(visit); + return; + } + unboundTypes.push(type); + seen[type] = true; + } + types.forEach(visit); + throw new UnboundTypeError(`${message}: ` + unboundTypes.map(getTypeName).join([ ", " ])); +}; + +var __embind_register_class = (rawType, rawPointerType, rawConstPointerType, baseClassRawType, getActualTypeSignature, getActualType, upcastSignature, upcast, downcastSignature, downcast, name, destructorSignature, rawDestructor) => { + name = readLatin1String(name); + getActualType = embind__requireFunction(getActualTypeSignature, getActualType); + if (upcast) { + upcast = embind__requireFunction(upcastSignature, upcast); + } + if (downcast) { + downcast = embind__requireFunction(downcastSignature, downcast); + } + rawDestructor = embind__requireFunction(destructorSignature, rawDestructor); + var legalFunctionName = makeLegalFunctionName(name); + exposePublicSymbol(legalFunctionName, function() { + throwUnboundTypeError(`Cannot construct ${name} due to unbound types`, [ baseClassRawType ]); + }); + whenDependentTypesAreResolved([ rawType, rawPointerType, rawConstPointerType ], baseClassRawType ? [ baseClassRawType ] : [], function(base) { + base = base[0]; + var baseClass; + var basePrototype; + if (baseClassRawType) { + baseClass = base.registeredClass; + basePrototype = baseClass.instancePrototype; + } else { + basePrototype = ClassHandle.prototype; + } + var constructor = createNamedFunction(legalFunctionName, function() { + if (Object.getPrototypeOf(this) !== instancePrototype) { + throw new BindingError("Use 'new' to construct " + name); + } + if (undefined === registeredClass.constructor_body) { + throw new BindingError(name + " has no accessible constructor"); + } + var body = registeredClass.constructor_body[arguments.length]; + if (undefined === body) { + throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${arguments.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`); + } + return body.apply(this, arguments); + }); + var instancePrototype = Object.create(basePrototype, { + constructor: { + value: constructor + } + }); + constructor.prototype = instancePrototype; + var registeredClass = new RegisteredClass(name, constructor, instancePrototype, rawDestructor, baseClass, getActualType, upcast, downcast); + if (registeredClass.baseClass) { + if (registeredClass.baseClass.__derivedClasses === undefined) { + registeredClass.baseClass.__derivedClasses = []; + } + registeredClass.baseClass.__derivedClasses.push(registeredClass); + } + var referenceConverter = new RegisteredPointer(name, registeredClass, true, false, false); + var pointerConverter = new RegisteredPointer(name + "*", registeredClass, false, false, false); + var constPointerConverter = new RegisteredPointer(name + " const*", registeredClass, false, true, false); + registeredPointers[rawType] = { + pointerType: pointerConverter, + constPointerType: constPointerConverter + }; + replacePublicSymbol(legalFunctionName, constructor); + return [ referenceConverter, pointerConverter, constPointerConverter ]; + }); +}; + +var heap32VectorToArray = (count, firstElement) => { + var array = []; + for (var i = 0; i < count; i++) { + array.push(HEAPU32[firstElement + i * 4 >> 2]); + } + return array; +}; + +var runDestructors = destructors => { + while (destructors.length) { + var ptr = destructors.pop(); + var del = destructors.pop(); + del(ptr); + } +}; + +function newFunc(constructor, argumentList) { + if (!(constructor instanceof Function)) { + throw new TypeError(`new_ called with constructor type ${typeof constructor} which is not a function`); + } + var dummy = createNamedFunction(constructor.name || "unknownFunctionName", function() {}); + dummy.prototype = constructor.prototype; + var obj = new dummy; + var r = constructor.apply(obj, argumentList); + return r instanceof Object ? r : obj; +} + +function craftInvokerFunction(humanName, argTypes, classType, cppInvokerFunc, cppTargetFunc, isAsync) { + var argCount = argTypes.length; + if (argCount < 2) { + throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!"); + } + var isClassMethodFunc = argTypes[1] !== null && classType !== null; + var needsDestructorStack = false; + for (var i = 1; i < argTypes.length; ++i) { + if (argTypes[i] !== null && argTypes[i].destructorFunction === undefined) { + needsDestructorStack = true; + break; + } + } + var returns = argTypes[0].name !== "void"; + var argsList = ""; + var argsListWired = ""; + for (var i = 0; i < argCount - 2; ++i) { + argsList += (i !== 0 ? ", " : "") + "arg" + i; + argsListWired += (i !== 0 ? ", " : "") + "arg" + i + "Wired"; + } + var invokerFnBody = `\n return function ${makeLegalFunctionName(humanName)}(${argsList}) {\n if (arguments.length !== ${argCount - 2}) {\n throwBindingError('function ${humanName} called with ' + arguments.length + ' arguments, expected ${argCount - 2}');\n }`; + if (needsDestructorStack) { + invokerFnBody += "var destructors = [];\n"; + } + var dtorStack = needsDestructorStack ? "destructors" : "null"; + var args1 = [ "throwBindingError", "invoker", "fn", "runDestructors", "retType", "classParam" ]; + var args2 = [ throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1] ]; + if (isClassMethodFunc) { + invokerFnBody += "var thisWired = classParam.toWireType(" + dtorStack + ", this);\n"; + } + for (var i = 0; i < argCount - 2; ++i) { + invokerFnBody += "var arg" + i + "Wired = argType" + i + ".toWireType(" + dtorStack + ", arg" + i + "); // " + argTypes[i + 2].name + "\n"; + args1.push("argType" + i); + args2.push(argTypes[i + 2]); + } + if (isClassMethodFunc) { + argsListWired = "thisWired" + (argsListWired.length > 0 ? ", " : "") + argsListWired; + } + invokerFnBody += (returns || isAsync ? "var rv = " : "") + "invoker(fn" + (argsListWired.length > 0 ? ", " : "") + argsListWired + ");\n"; + if (needsDestructorStack) { + invokerFnBody += "runDestructors(destructors);\n"; + } else { + for (var i = isClassMethodFunc ? 1 : 2; i < argTypes.length; ++i) { + var paramName = i === 1 ? "thisWired" : "arg" + (i - 2) + "Wired"; + if (argTypes[i].destructorFunction !== null) { + invokerFnBody += paramName + "_dtor(" + paramName + "); // " + argTypes[i].name + "\n"; + args1.push(paramName + "_dtor"); + args2.push(argTypes[i].destructorFunction); + } + } + } + if (returns) { + invokerFnBody += "var ret = retType.fromWireType(rv);\n" + "return ret;\n"; + } else {} + invokerFnBody += "}\n"; + args1.push(invokerFnBody); + return newFunc(Function, args1).apply(null, args2); +} + +var __embind_register_class_constructor = (rawClassType, argCount, rawArgTypesAddr, invokerSignature, invoker, rawConstructor) => { + var rawArgTypes = heap32VectorToArray(argCount, rawArgTypesAddr); + invoker = embind__requireFunction(invokerSignature, invoker); + whenDependentTypesAreResolved([], [ rawClassType ], function(classType) { + classType = classType[0]; + var humanName = `constructor ${classType.name}`; + if (undefined === classType.registeredClass.constructor_body) { + classType.registeredClass.constructor_body = []; + } + if (undefined !== classType.registeredClass.constructor_body[argCount - 1]) { + throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount - 1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`); + } + classType.registeredClass.constructor_body[argCount - 1] = () => { + throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`, rawArgTypes); + }; + whenDependentTypesAreResolved([], rawArgTypes, argTypes => { + argTypes.splice(1, 0, null); + classType.registeredClass.constructor_body[argCount - 1] = craftInvokerFunction(humanName, argTypes, null, invoker, rawConstructor); + return []; + }); + return []; + }); +}; + +var __embind_register_class_function = (rawClassType, methodName, argCount, rawArgTypesAddr, invokerSignature, rawInvoker, context, isPureVirtual, isAsync) => { + var rawArgTypes = heap32VectorToArray(argCount, rawArgTypesAddr); + methodName = readLatin1String(methodName); + rawInvoker = embind__requireFunction(invokerSignature, rawInvoker); + whenDependentTypesAreResolved([], [ rawClassType ], function(classType) { + classType = classType[0]; + var humanName = `${classType.name}.${methodName}`; + if (methodName.startsWith("@@")) { + methodName = Symbol[methodName.substring(2)]; + } + if (isPureVirtual) { + classType.registeredClass.pureVirtualFunctions.push(methodName); + } + function unboundTypesHandler() { + throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`, rawArgTypes); + } + var proto = classType.registeredClass.instancePrototype; + var method = proto[methodName]; + if (undefined === method || undefined === method.overloadTable && method.className !== classType.name && method.argCount === argCount - 2) { + unboundTypesHandler.argCount = argCount - 2; + unboundTypesHandler.className = classType.name; + proto[methodName] = unboundTypesHandler; + } else { + ensureOverloadTable(proto, methodName, humanName); + proto[methodName].overloadTable[argCount - 2] = unboundTypesHandler; + } + whenDependentTypesAreResolved([], rawArgTypes, function(argTypes) { + var memberFunction = craftInvokerFunction(humanName, argTypes, classType, rawInvoker, context, isAsync); + if (undefined === proto[methodName].overloadTable) { + memberFunction.argCount = argCount - 2; + proto[methodName] = memberFunction; + } else { + proto[methodName].overloadTable[argCount - 2] = memberFunction; + } + return []; + }); + return []; + }); +}; + +function handleAllocatorInit() { + Object.assign(HandleAllocator.prototype, { + get(id) { + return this.allocated[id]; + }, + has(id) { + return this.allocated[id] !== undefined; + }, + allocate(handle) { + var id = this.freelist.pop() || this.allocated.length; + this.allocated[id] = handle; + return id; + }, + free(id) { + this.allocated[id] = undefined; + this.freelist.push(id); + } + }); +} + +function HandleAllocator() { + this.allocated = [ undefined ]; + this.freelist = []; +} + +var emval_handles = new HandleAllocator; + +var __emval_decref = handle => { + if (handle >= emval_handles.reserved && 0 === --emval_handles.get(handle).refcount) { + emval_handles.free(handle); + } +}; + +var count_emval_handles = () => { + var count = 0; + for (var i = emval_handles.reserved; i < emval_handles.allocated.length; ++i) { + if (emval_handles.allocated[i] !== undefined) { + ++count; + } + } + return count; +}; + +var init_emval = () => { + emval_handles.allocated.push({ + value: undefined + }, { + value: null + }, { + value: true + }, { + value: false + }); + emval_handles.reserved = emval_handles.allocated.length; + Module["count_emval_handles"] = count_emval_handles; +}; + +var Emval = { + toValue: handle => { + if (!handle) { + throwBindingError("Cannot use deleted val. handle = " + handle); + } + return emval_handles.get(handle).value; + }, + toHandle: value => { + switch (value) { + case undefined: + return 1; + + case null: + return 2; + + case true: + return 3; + + case false: + return 4; + + default: + { + return emval_handles.allocate({ + refcount: 1, + value: value + }); + } + } + } +}; + +function simpleReadValueFromPointer(pointer) { + return this["fromWireType"](HEAP32[pointer >> 2]); +} + +var __embind_register_emval = (rawType, name) => { + name = readLatin1String(name); + registerType(rawType, { + name: name, + "fromWireType": handle => { + var rv = Emval.toValue(handle); + __emval_decref(handle); + return rv; + }, + "toWireType": (destructors, value) => Emval.toHandle(value), + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": simpleReadValueFromPointer, + destructorFunction: null + }); +}; + +var embindRepr = v => { + if (v === null) { + return "null"; + } + var t = typeof v; + if (t === "object" || t === "array" || t === "function") { + return v.toString(); + } else { + return "" + v; + } +}; + +var floatReadValueFromPointer = (name, width) => { + switch (width) { + case 4: + return function(pointer) { + return this["fromWireType"](HEAPF32[pointer >> 2]); + }; + + case 8: + return function(pointer) { + return this["fromWireType"](HEAPF64[pointer >> 3]); + }; + + default: + throw new TypeError(`invalid float width (${width}): ${name}`); + } +}; + +var __embind_register_float = (rawType, name, size) => { + name = readLatin1String(name); + registerType(rawType, { + name: name, + "fromWireType": value => value, + "toWireType": (destructors, value) => value, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": floatReadValueFromPointer(name, size), + destructorFunction: null + }); +}; + +var integerReadValueFromPointer = (name, width, signed) => { + switch (width) { + case 1: + return signed ? pointer => HEAP8[pointer >> 0] : pointer => HEAPU8[pointer >> 0]; + + case 2: + return signed ? pointer => HEAP16[pointer >> 1] : pointer => HEAPU16[pointer >> 1]; + + case 4: + return signed ? pointer => HEAP32[pointer >> 2] : pointer => HEAPU32[pointer >> 2]; + + default: + throw new TypeError(`invalid integer width (${width}): ${name}`); + } +}; + +var __embind_register_integer = (primitiveType, name, size, minRange, maxRange) => { + name = readLatin1String(name); + if (maxRange === -1) { + maxRange = 4294967295; + } + var fromWireType = value => value; + if (minRange === 0) { + var bitshift = 32 - 8 * size; + fromWireType = value => value << bitshift >>> bitshift; + } + var isUnsignedType = name.includes("unsigned"); + var checkAssertions = (value, toTypeName) => {}; + var toWireType; + if (isUnsignedType) { + toWireType = function(destructors, value) { + checkAssertions(value, this.name); + return value >>> 0; + }; + } else { + toWireType = function(destructors, value) { + checkAssertions(value, this.name); + return value; + }; + } + registerType(primitiveType, { + name: name, + "fromWireType": fromWireType, + "toWireType": toWireType, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": integerReadValueFromPointer(name, size, minRange !== 0), + destructorFunction: null + }); +}; + +var __embind_register_memory_view = (rawType, dataTypeIndex, name) => { + var typeMapping = [ Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array ]; + var TA = typeMapping[dataTypeIndex]; + function decodeMemoryView(handle) { + var size = HEAPU32[handle >> 2]; + var data = HEAPU32[handle + 4 >> 2]; + return new TA(HEAP8.buffer, data, size); + } + name = readLatin1String(name); + registerType(rawType, { + name: name, + "fromWireType": decodeMemoryView, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": decodeMemoryView + }, { + ignoreDuplicateRegistrations: true + }); +}; + +var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 55296 && u <= 57343) { + var u1 = str.charCodeAt(++i); + u = 65536 + ((u & 1023) << 10) | u1 & 1023; + } + if (u <= 127) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 192 | u >> 6; + heap[outIdx++] = 128 | u & 63; + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 224 | u >> 12; + heap[outIdx++] = 128 | u >> 6 & 63; + heap[outIdx++] = 128 | u & 63; + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 240 | u >> 18; + heap[outIdx++] = 128 | u >> 12 & 63; + heap[outIdx++] = 128 | u >> 6 & 63; + heap[outIdx++] = 128 | u & 63; + } + } + heap[outIdx] = 0; + return outIdx - startIdx; +}; + +var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + +var lengthBytesUTF8 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + if (c <= 127) { + len++; + } else if (c <= 2047) { + len += 2; + } else if (c >= 55296 && c <= 57343) { + len += 4; + ++i; + } else { + len += 3; + } + } + return len; +}; + +var __embind_register_std_string = (rawType, name) => { + name = readLatin1String(name); + var stdStringIsUTF8 = name === "std::string"; + registerType(rawType, { + name: name, + "fromWireType": value => { + var length = HEAPU32[value >> 2]; + var payload = value + 4; + var str; + if (stdStringIsUTF8) { + var decodeStartPtr = payload; + for (var i = 0; i <= length; ++i) { + var currentBytePtr = payload + i; + if (i == length || HEAPU8[currentBytePtr] == 0) { + var maxRead = currentBytePtr - decodeStartPtr; + var stringSegment = UTF8ToString(decodeStartPtr, maxRead); + if (str === undefined) { + str = stringSegment; + } else { + str += String.fromCharCode(0); + str += stringSegment; + } + decodeStartPtr = currentBytePtr + 1; + } + } + } else { + var a = new Array(length); + for (var i = 0; i < length; ++i) { + a[i] = String.fromCharCode(HEAPU8[payload + i]); + } + str = a.join(""); + } + _free(value); + return str; + }, + "toWireType": (destructors, value) => { + if (value instanceof ArrayBuffer) { + value = new Uint8Array(value); + } + var length; + var valueIsOfTypeString = typeof value == "string"; + if (!(valueIsOfTypeString || value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Int8Array)) { + throwBindingError("Cannot pass non-string to std::string"); + } + if (stdStringIsUTF8 && valueIsOfTypeString) { + length = lengthBytesUTF8(value); + } else { + length = value.length; + } + var base = _malloc(4 + length + 1); + var ptr = base + 4; + HEAPU32[base >> 2] = length; + if (stdStringIsUTF8 && valueIsOfTypeString) { + stringToUTF8(value, ptr, length + 1); + } else { + if (valueIsOfTypeString) { + for (var i = 0; i < length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode > 255) { + _free(ptr); + throwBindingError("String has UTF-16 code units that do not fit in 8 bits"); + } + HEAPU8[ptr + i] = charCode; + } + } else { + for (var i = 0; i < length; ++i) { + HEAPU8[ptr + i] = value[i]; + } + } + } + if (destructors !== null) { + destructors.push(_free, base); + } + return base; + }, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": readPointer, + destructorFunction: ptr => _free(ptr) + }); +}; + +var UTF16Decoder = typeof TextDecoder != "undefined" ? new TextDecoder("utf-16le") : undefined; + +var UTF16ToString = (ptr, maxBytesToRead) => { + var endPtr = ptr; + var idx = endPtr >> 1; + var maxIdx = idx + maxBytesToRead / 2; + while (!(idx >= maxIdx) && HEAPU16[idx]) ++idx; + endPtr = idx << 1; + if (endPtr - ptr > 32 && UTF16Decoder) return UTF16Decoder.decode(HEAPU8.subarray(ptr, endPtr)); + var str = ""; + for (var i = 0; !(i >= maxBytesToRead / 2); ++i) { + var codeUnit = HEAP16[ptr + i * 2 >> 1]; + if (codeUnit == 0) break; + str += String.fromCharCode(codeUnit); + } + return str; +}; + +var stringToUTF16 = (str, outPtr, maxBytesToWrite) => { + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 2147483647; + } + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; + var startPtr = outPtr; + var numCharsToWrite = maxBytesToWrite < str.length * 2 ? maxBytesToWrite / 2 : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + var codeUnit = str.charCodeAt(i); + HEAP16[outPtr >> 1] = codeUnit; + outPtr += 2; + } + HEAP16[outPtr >> 1] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF16 = str => str.length * 2; + +var UTF32ToString = (ptr, maxBytesToRead) => { + var i = 0; + var str = ""; + while (!(i >= maxBytesToRead / 4)) { + var utf32 = HEAP32[ptr + i * 4 >> 2]; + if (utf32 == 0) break; + ++i; + if (utf32 >= 65536) { + var ch = utf32 - 65536; + str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023); + } else { + str += String.fromCharCode(utf32); + } + } + return str; +}; + +var stringToUTF32 = (str, outPtr, maxBytesToWrite) => { + if (maxBytesToWrite === undefined) { + maxBytesToWrite = 2147483647; + } + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + var codeUnit = str.charCodeAt(i); + if (codeUnit >= 55296 && codeUnit <= 57343) { + var trailSurrogate = str.charCodeAt(++i); + codeUnit = 65536 + ((codeUnit & 1023) << 10) | trailSurrogate & 1023; + } + HEAP32[outPtr >> 2] = codeUnit; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + HEAP32[outPtr >> 2] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF32 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var codeUnit = str.charCodeAt(i); + if (codeUnit >= 55296 && codeUnit <= 57343) ++i; + len += 4; + } + return len; +}; + +var __embind_register_std_wstring = (rawType, charSize, name) => { + name = readLatin1String(name); + var decodeString, encodeString, getHeap, lengthBytesUTF, shift; + if (charSize === 2) { + decodeString = UTF16ToString; + encodeString = stringToUTF16; + lengthBytesUTF = lengthBytesUTF16; + getHeap = () => HEAPU16; + shift = 1; + } else if (charSize === 4) { + decodeString = UTF32ToString; + encodeString = stringToUTF32; + lengthBytesUTF = lengthBytesUTF32; + getHeap = () => HEAPU32; + shift = 2; + } + registerType(rawType, { + name: name, + "fromWireType": value => { + var length = HEAPU32[value >> 2]; + var HEAP = getHeap(); + var str; + var decodeStartPtr = value + 4; + for (var i = 0; i <= length; ++i) { + var currentBytePtr = value + 4 + i * charSize; + if (i == length || HEAP[currentBytePtr >> shift] == 0) { + var maxReadBytes = currentBytePtr - decodeStartPtr; + var stringSegment = decodeString(decodeStartPtr, maxReadBytes); + if (str === undefined) { + str = stringSegment; + } else { + str += String.fromCharCode(0); + str += stringSegment; + } + decodeStartPtr = currentBytePtr + charSize; + } + } + _free(value); + return str; + }, + "toWireType": (destructors, value) => { + if (!(typeof value == "string")) { + throwBindingError(`Cannot pass non-string to C++ string type ${name}`); + } + var length = lengthBytesUTF(value); + var ptr = _malloc(4 + length + charSize); + HEAPU32[ptr >> 2] = length >> shift; + encodeString(value, ptr + 4, length + charSize); + if (destructors !== null) { + destructors.push(_free, ptr); + } + return ptr; + }, + "argPackAdvance": GenericWireTypeSize, + "readValueFromPointer": simpleReadValueFromPointer, + destructorFunction: ptr => _free(ptr) + }); +}; + +var __embind_register_void = (rawType, name) => { + name = readLatin1String(name); + registerType(rawType, { + isVoid: true, + name: name, + "argPackAdvance": 0, + "fromWireType": () => undefined, + "toWireType": (destructors, o) => undefined + }); +}; + +var __emscripten_throw_longjmp = () => { + throw Infinity; +}; + +var __emval_incref = handle => { + if (handle > 4) { + emval_handles.get(handle).refcount += 1; + } +}; + +var requireRegisteredType = (rawType, humanName) => { + var impl = registeredTypes[rawType]; + if (undefined === impl) { + throwBindingError(humanName + " has unknown type " + getTypeName(rawType)); + } + return impl; +}; + +var __emval_take_value = (type, arg) => { + type = requireRegisteredType(type, "_emval_take_value"); + var v = type["readValueFromPointer"](arg); + return Emval.toHandle(v); +}; + +var isLeapYear = year => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + +var MONTH_DAYS_LEAP_CUMULATIVE = [ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ]; + +var MONTH_DAYS_REGULAR_CUMULATIVE = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; + +var ydayFromDate = date => { + var leap = isLeapYear(date.getFullYear()); + var monthDaysCumulative = leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE; + var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1; + return yday; +}; + +var convertI32PairToI53Checked = (lo, hi) => hi + 2097152 >>> 0 < 4194305 - !!lo ? (lo >>> 0) + hi * 4294967296 : NaN; + +function __localtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[tmPtr >> 2] = date.getSeconds(); + HEAP32[tmPtr + 4 >> 2] = date.getMinutes(); + HEAP32[tmPtr + 8 >> 2] = date.getHours(); + HEAP32[tmPtr + 12 >> 2] = date.getDate(); + HEAP32[tmPtr + 16 >> 2] = date.getMonth(); + HEAP32[tmPtr + 20 >> 2] = date.getFullYear() - 1900; + HEAP32[tmPtr + 24 >> 2] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[tmPtr + 28 >> 2] = yday; + HEAP32[tmPtr + 36 >> 2] = -(date.getTimezoneOffset() * 60); + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; + HEAP32[tmPtr + 32 >> 2] = dst; +} + +var __mktime_js = function(tmPtr) { + var ret = (() => { + var date = new Date(HEAP32[tmPtr + 20 >> 2] + 1900, HEAP32[tmPtr + 16 >> 2], HEAP32[tmPtr + 12 >> 2], HEAP32[tmPtr + 8 >> 2], HEAP32[tmPtr + 4 >> 2], HEAP32[tmPtr >> 2], 0); + var dst = HEAP32[tmPtr + 32 >> 2]; + var guessedOffset = date.getTimezoneOffset(); + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dstOffset = Math.min(winterOffset, summerOffset); + if (dst < 0) { + HEAP32[tmPtr + 32 >> 2] = Number(summerOffset != winterOffset && dstOffset == guessedOffset); + } else if (dst > 0 != (dstOffset == guessedOffset)) { + var nonDstOffset = Math.max(winterOffset, summerOffset); + var trueOffset = dst > 0 ? dstOffset : nonDstOffset; + date.setTime(date.getTime() + (trueOffset - guessedOffset) * 6e4); + } + HEAP32[tmPtr + 24 >> 2] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[tmPtr + 28 >> 2] = yday; + HEAP32[tmPtr >> 2] = date.getSeconds(); + HEAP32[tmPtr + 4 >> 2] = date.getMinutes(); + HEAP32[tmPtr + 8 >> 2] = date.getHours(); + HEAP32[tmPtr + 12 >> 2] = date.getDate(); + HEAP32[tmPtr + 16 >> 2] = date.getMonth(); + HEAP32[tmPtr + 20 >> 2] = date.getYear(); + return date.getTime() / 1e3; + })(); + return setTempRet0((tempDouble = ret, +Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? +Math.floor(tempDouble / 4294967296) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)), + ret >>> 0; +}; + +var stringToNewUTF8 = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8(str, ret, size); + return ret; +}; + +var __tzset_js = (timezone, daylight, tzname) => { + var currentYear = (new Date).getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + var winterOffset = winter.getTimezoneOffset(); + var summerOffset = summer.getTimezoneOffset(); + var stdTimezoneOffset = Math.max(winterOffset, summerOffset); + HEAPU32[timezone >> 2] = stdTimezoneOffset * 60; + HEAP32[daylight >> 2] = Number(winterOffset != summerOffset); + function extractZone(date) { + var match = date.toTimeString().match(/\(([A-Za-z ]+)\)$/); + return match ? match[1] : "GMT"; + } + var winterName = extractZone(winter); + var summerName = extractZone(summer); + var winterNamePtr = stringToNewUTF8(winterName); + var summerNamePtr = stringToNewUTF8(summerName); + if (summerOffset < winterOffset) { + HEAPU32[tzname >> 2] = winterNamePtr; + HEAPU32[tzname + 4 >> 2] = summerNamePtr; + } else { + HEAPU32[tzname >> 2] = summerNamePtr; + HEAPU32[tzname + 4 >> 2] = winterNamePtr; + } +}; + +var _abort = () => { + abort(""); +}; + +var _emscripten_memcpy_big = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num); + +var getHeapMax = () => 2147483648; + +var growMemory = size => { + var b = wasmMemory.buffer; + var pages = (size - b.byteLength + 65535) / 65536; + try { + wasmMemory.grow(pages); + updateMemoryViews(); + return 1; + } catch (e) {} +}; + +var _emscripten_resize_heap = requestedSize => { + var oldSize = HEAPU8.length; + requestedSize >>>= 0; + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + var alignUp = (x, multiple) => x + (multiple - x % multiple) % multiple; + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + .2 / cutDown); + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); + var newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536)); + var replacement = growMemory(newSize); + if (replacement) { + return true; + } + } + return false; +}; + +var ENV = {}; + +var getExecutableName = () => thisProgram || "./this.program"; + +var getEnvStrings = () => { + if (!getEnvStrings.strings) { + var lang = (typeof navigator == "object" && navigator.languages && navigator.languages[0] || "C").replace("-", "_") + ".UTF-8"; + var env = { + "USER": "web_user", + "LOGNAME": "web_user", + "PATH": "/", + "PWD": "/", + "HOME": "/home/web_user", + "LANG": lang, + "_": getExecutableName() + }; + for (var x in ENV) { + if (ENV[x] === undefined) delete env[x]; else env[x] = ENV[x]; + } + var strings = []; + for (var x in env) { + strings.push(`${x}=${env[x]}`); + } + getEnvStrings.strings = strings; + } + return getEnvStrings.strings; +}; + +var stringToAscii = (str, buffer) => { + for (var i = 0; i < str.length; ++i) { + HEAP8[buffer++ >> 0] = str.charCodeAt(i); + } + HEAP8[buffer >> 0] = 0; +}; + +var _environ_get = (__environ, environ_buf) => { + var bufSize = 0; + getEnvStrings().forEach((string, i) => { + var ptr = environ_buf + bufSize; + HEAPU32[__environ + i * 4 >> 2] = ptr; + stringToAscii(string, ptr); + bufSize += string.length + 1; + }); + return 0; +}; + +var _environ_sizes_get = (penviron_count, penviron_buf_size) => { + var strings = getEnvStrings(); + HEAPU32[penviron_count >> 2] = strings.length; + var bufSize = 0; + strings.forEach(string => bufSize += string.length + 1); + HEAPU32[penviron_buf_size >> 2] = bufSize; + return 0; +}; + +var _fd_close = fd => 52; + +var _fd_read = (fd, iov, iovcnt, pnum) => 52; + +function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + return 70; +} + +var printCharBuffers = [ null, [], [] ]; + +var printChar = (stream, curr) => { + var buffer = printCharBuffers[stream]; + if (curr === 0 || curr === 10) { + (stream === 1 ? out : err)(UTF8ArrayToString(buffer, 0)); + buffer.length = 0; + } else { + buffer.push(curr); + } +}; + +var _fd_write = (fd, iov, iovcnt, pnum) => { + var num = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[iov >> 2]; + var len = HEAPU32[iov + 4 >> 2]; + iov += 8; + for (var j = 0; j < len; j++) { + printChar(fd, HEAPU8[ptr + j]); + } + num += len; + } + HEAPU32[pnum >> 2] = num; + return 0; +}; + +var arraySum = (array, index) => { + var sum = 0; + for (var i = 0; i <= index; sum += array[i++]) {} + return sum; +}; + +var MONTH_DAYS_LEAP = [ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; + +var MONTH_DAYS_REGULAR = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; + +var addDays = (date, days) => { + var newDate = new Date(date.getTime()); + while (days > 0) { + var leap = isLeapYear(newDate.getFullYear()); + var currentMonth = newDate.getMonth(); + var daysInCurrentMonth = (leap ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR)[currentMonth]; + if (days > daysInCurrentMonth - newDate.getDate()) { + days -= daysInCurrentMonth - newDate.getDate() + 1; + newDate.setDate(1); + if (currentMonth < 11) { + newDate.setMonth(currentMonth + 1); + } else { + newDate.setMonth(0); + newDate.setFullYear(newDate.getFullYear() + 1); + } + } else { + newDate.setDate(newDate.getDate() + days); + return newDate; + } + } + return newDate; +}; + +function intArrayFromString(stringy, dontAddNull, length) { + var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +} + +var writeArrayToMemory = (array, buffer) => { + HEAP8.set(array, buffer); +}; + +var _strftime = (s, maxsize, format, tm) => { + var tm_zone = HEAPU32[tm + 40 >> 2]; + var date = { + tm_sec: HEAP32[tm >> 2], + tm_min: HEAP32[tm + 4 >> 2], + tm_hour: HEAP32[tm + 8 >> 2], + tm_mday: HEAP32[tm + 12 >> 2], + tm_mon: HEAP32[tm + 16 >> 2], + tm_year: HEAP32[tm + 20 >> 2], + tm_wday: HEAP32[tm + 24 >> 2], + tm_yday: HEAP32[tm + 28 >> 2], + tm_isdst: HEAP32[tm + 32 >> 2], + tm_gmtoff: HEAP32[tm + 36 >> 2], + tm_zone: tm_zone ? UTF8ToString(tm_zone) : "" + }; + var pattern = UTF8ToString(format); + var EXPANSION_RULES_1 = { + "%c": "%a %b %d %H:%M:%S %Y", + "%D": "%m/%d/%y", + "%F": "%Y-%m-%d", + "%h": "%b", + "%r": "%I:%M:%S %p", + "%R": "%H:%M", + "%T": "%H:%M:%S", + "%x": "%m/%d/%y", + "%X": "%H:%M:%S", + "%Ec": "%c", + "%EC": "%C", + "%Ex": "%m/%d/%y", + "%EX": "%H:%M:%S", + "%Ey": "%y", + "%EY": "%Y", + "%Od": "%d", + "%Oe": "%e", + "%OH": "%H", + "%OI": "%I", + "%Om": "%m", + "%OM": "%M", + "%OS": "%S", + "%Ou": "%u", + "%OU": "%U", + "%OV": "%V", + "%Ow": "%w", + "%OW": "%W", + "%Oy": "%y" + }; + for (var rule in EXPANSION_RULES_1) { + pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_1[rule]); + } + var WEEKDAYS = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]; + var MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; + function leadingSomething(value, digits, character) { + var str = typeof value == "number" ? value.toString() : value || ""; + while (str.length < digits) { + str = character[0] + str; + } + return str; + } + function leadingNulls(value, digits) { + return leadingSomething(value, digits, "0"); + } + function compareByDay(date1, date2) { + function sgn(value) { + return value < 0 ? -1 : value > 0 ? 1 : 0; + } + var compare; + if ((compare = sgn(date1.getFullYear() - date2.getFullYear())) === 0) { + if ((compare = sgn(date1.getMonth() - date2.getMonth())) === 0) { + compare = sgn(date1.getDate() - date2.getDate()); + } + } + return compare; + } + function getFirstWeekStartDate(janFourth) { + switch (janFourth.getDay()) { + case 0: + return new Date(janFourth.getFullYear() - 1, 11, 29); + + case 1: + return janFourth; + + case 2: + return new Date(janFourth.getFullYear(), 0, 3); + + case 3: + return new Date(janFourth.getFullYear(), 0, 2); + + case 4: + return new Date(janFourth.getFullYear(), 0, 1); + + case 5: + return new Date(janFourth.getFullYear() - 1, 11, 31); + + case 6: + return new Date(janFourth.getFullYear() - 1, 11, 30); + } + } + function getWeekBasedYear(date) { + var thisDate = addDays(new Date(date.tm_year + 1900, 0, 1), date.tm_yday); + var janFourthThisYear = new Date(thisDate.getFullYear(), 0, 4); + var janFourthNextYear = new Date(thisDate.getFullYear() + 1, 0, 4); + var firstWeekStartThisYear = getFirstWeekStartDate(janFourthThisYear); + var firstWeekStartNextYear = getFirstWeekStartDate(janFourthNextYear); + if (compareByDay(firstWeekStartThisYear, thisDate) <= 0) { + if (compareByDay(firstWeekStartNextYear, thisDate) <= 0) { + return thisDate.getFullYear() + 1; + } + return thisDate.getFullYear(); + } + return thisDate.getFullYear() - 1; + } + var EXPANSION_RULES_2 = { + "%a": date => WEEKDAYS[date.tm_wday].substring(0, 3), + "%A": date => WEEKDAYS[date.tm_wday], + "%b": date => MONTHS[date.tm_mon].substring(0, 3), + "%B": date => MONTHS[date.tm_mon], + "%C": date => { + var year = date.tm_year + 1900; + return leadingNulls(year / 100 | 0, 2); + }, + "%d": date => leadingNulls(date.tm_mday, 2), + "%e": date => leadingSomething(date.tm_mday, 2, " "), + "%g": date => getWeekBasedYear(date).toString().substring(2), + "%G": date => getWeekBasedYear(date), + "%H": date => leadingNulls(date.tm_hour, 2), + "%I": date => { + var twelveHour = date.tm_hour; + if (twelveHour == 0) twelveHour = 12; else if (twelveHour > 12) twelveHour -= 12; + return leadingNulls(twelveHour, 2); + }, + "%j": date => leadingNulls(date.tm_mday + arraySum(isLeapYear(date.tm_year + 1900) ? MONTH_DAYS_LEAP : MONTH_DAYS_REGULAR, date.tm_mon - 1), 3), + "%m": date => leadingNulls(date.tm_mon + 1, 2), + "%M": date => leadingNulls(date.tm_min, 2), + "%n": () => "\n", + "%p": date => { + if (date.tm_hour >= 0 && date.tm_hour < 12) { + return "AM"; + } + return "PM"; + }, + "%S": date => leadingNulls(date.tm_sec, 2), + "%t": () => "\t", + "%u": date => date.tm_wday || 7, + "%U": date => { + var days = date.tm_yday + 7 - date.tm_wday; + return leadingNulls(Math.floor(days / 7), 2); + }, + "%V": date => { + var val = Math.floor((date.tm_yday + 7 - (date.tm_wday + 6) % 7) / 7); + if ((date.tm_wday + 371 - date.tm_yday - 2) % 7 <= 2) { + val++; + } + if (!val) { + val = 52; + var dec31 = (date.tm_wday + 7 - date.tm_yday - 1) % 7; + if (dec31 == 4 || dec31 == 5 && isLeapYear(date.tm_year % 400 - 1)) { + val++; + } + } else if (val == 53) { + var jan1 = (date.tm_wday + 371 - date.tm_yday) % 7; + if (jan1 != 4 && (jan1 != 3 || !isLeapYear(date.tm_year))) val = 1; + } + return leadingNulls(val, 2); + }, + "%w": date => date.tm_wday, + "%W": date => { + var days = date.tm_yday + 7 - (date.tm_wday + 6) % 7; + return leadingNulls(Math.floor(days / 7), 2); + }, + "%y": date => (date.tm_year + 1900).toString().substring(2), + "%Y": date => date.tm_year + 1900, + "%z": date => { + var off = date.tm_gmtoff; + var ahead = off >= 0; + off = Math.abs(off) / 60; + off = off / 60 * 100 + off % 60; + return (ahead ? "+" : "-") + String("0000" + off).slice(-4); + }, + "%Z": date => date.tm_zone, + "%%": () => "%" + }; + pattern = pattern.replace(/%%/g, "\0\0"); + for (var rule in EXPANSION_RULES_2) { + if (pattern.includes(rule)) { + pattern = pattern.replace(new RegExp(rule, "g"), EXPANSION_RULES_2[rule](date)); + } + } + pattern = pattern.replace(/\0\0/g, "%"); + var bytes = intArrayFromString(pattern, false); + if (bytes.length > maxsize) { + return 0; + } + writeArrayToMemory(bytes, s); + return bytes.length - 1; +}; + +var _strftime_l = (s, maxsize, format, tm, loc) => _strftime(s, maxsize, format, tm); + +embind_init_charCodes(); + +BindingError = Module["BindingError"] = class BindingError extends Error { + constructor(message) { + super(message); + this.name = "BindingError"; + } +}; + +InternalError = Module["InternalError"] = class InternalError extends Error { + constructor(message) { + super(message); + this.name = "InternalError"; + } +}; + +init_ClassHandle(); + +init_embind(); + +init_RegisteredPointer(); + +UnboundTypeError = Module["UnboundTypeError"] = extendError(Error, "UnboundTypeError"); + +handleAllocatorInit(); + +init_emval(); + +var wasmImports = { + __assert_fail: ___assert_fail, + __cxa_throw: ___cxa_throw, + __syscall_ioctl: ___syscall_ioctl, + __syscall_openat: ___syscall_openat, + _embind_register_bigint: __embind_register_bigint, + _embind_register_bool: __embind_register_bool, + _embind_register_class: __embind_register_class, + _embind_register_class_constructor: __embind_register_class_constructor, + _embind_register_class_function: __embind_register_class_function, + _embind_register_emval: __embind_register_emval, + _embind_register_float: __embind_register_float, + _embind_register_integer: __embind_register_integer, + _embind_register_memory_view: __embind_register_memory_view, + _embind_register_std_string: __embind_register_std_string, + _embind_register_std_wstring: __embind_register_std_wstring, + _embind_register_void: __embind_register_void, + _emscripten_throw_longjmp: __emscripten_throw_longjmp, + _emval_decref: __emval_decref, + _emval_incref: __emval_incref, + _emval_take_value: __emval_take_value, + _localtime_js: __localtime_js, + _mktime_js: __mktime_js, + _tzset_js: __tzset_js, + abort: _abort, + emscripten_memcpy_big: _emscripten_memcpy_big, + emscripten_resize_heap: _emscripten_resize_heap, + environ_get: _environ_get, + environ_sizes_get: _environ_sizes_get, + fd_close: _fd_close, + fd_read: _fd_read, + fd_seek: _fd_seek, + fd_write: _fd_write, + invoke_vi: invoke_vi, + invoke_viii: invoke_viii, + invoke_viiii: invoke_viiii, + invoke_viiiii: invoke_viiiii, + strftime_l: _strftime_l +}; + +var wasmExports = createWasm(); + +var ___wasm_call_ctors = () => (___wasm_call_ctors = wasmExports["__wasm_call_ctors"])(); + +var _free = a0 => (_free = wasmExports["free"])(a0); + +var _malloc = a0 => (_malloc = wasmExports["malloc"])(a0); + +var ___getTypeName = a0 => (___getTypeName = wasmExports["__getTypeName"])(a0); + +var __embind_initialize_bindings = Module["__embind_initialize_bindings"] = () => (__embind_initialize_bindings = Module["__embind_initialize_bindings"] = wasmExports["_embind_initialize_bindings"])(); + +var ___errno_location = () => (___errno_location = wasmExports["__errno_location"])(); + +var _setThrew = (a0, a1) => (_setThrew = wasmExports["setThrew"])(a0, a1); + +var setTempRet0 = a0 => (setTempRet0 = wasmExports["setTempRet0"])(a0); + +var stackSave = () => (stackSave = wasmExports["stackSave"])(); + +var stackRestore = a0 => (stackRestore = wasmExports["stackRestore"])(a0); + +var stackAlloc = a0 => (stackAlloc = wasmExports["stackAlloc"])(a0); + +var ___cxa_increment_exception_refcount = a0 => (___cxa_increment_exception_refcount = wasmExports["__cxa_increment_exception_refcount"])(a0); + +var ___cxa_is_pointer_type = a0 => (___cxa_is_pointer_type = wasmExports["__cxa_is_pointer_type"])(a0); + +var dynCall_iijii = Module["dynCall_iijii"] = (a0, a1, a2, a3, a4, a5) => (dynCall_iijii = Module["dynCall_iijii"] = wasmExports["dynCall_iijii"])(a0, a1, a2, a3, a4, a5); + +var dynCall_jiji = Module["dynCall_jiji"] = (a0, a1, a2, a3, a4) => (dynCall_jiji = Module["dynCall_jiji"] = wasmExports["dynCall_jiji"])(a0, a1, a2, a3, a4); + +var dynCall_viijii = Module["dynCall_viijii"] = (a0, a1, a2, a3, a4, a5, a6) => (dynCall_viijii = Module["dynCall_viijii"] = wasmExports["dynCall_viijii"])(a0, a1, a2, a3, a4, a5, a6); + +var dynCall_iiiiij = Module["dynCall_iiiiij"] = (a0, a1, a2, a3, a4, a5, a6) => (dynCall_iiiiij = Module["dynCall_iiiiij"] = wasmExports["dynCall_iiiiij"])(a0, a1, a2, a3, a4, a5, a6); + +var dynCall_iiiiijj = Module["dynCall_iiiiijj"] = (a0, a1, a2, a3, a4, a5, a6, a7, a8) => (dynCall_iiiiijj = Module["dynCall_iiiiijj"] = wasmExports["dynCall_iiiiijj"])(a0, a1, a2, a3, a4, a5, a6, a7, a8); + +var dynCall_iiiiiijj = Module["dynCall_iiiiiijj"] = (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) => (dynCall_iiiiiijj = Module["dynCall_iiiiiijj"] = wasmExports["dynCall_iiiiiijj"])(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9); + +function invoke_vi(index, a1) { + var sp = stackSave(); + try { + getWasmTableEntry(index)(a1); + } catch (e) { + stackRestore(sp); + if (e !== e + 0) throw e; + _setThrew(1, 0); + } +} + +function invoke_viii(index, a1, a2, a3) { + var sp = stackSave(); + try { + getWasmTableEntry(index)(a1, a2, a3); + } catch (e) { + stackRestore(sp); + if (e !== e + 0) throw e; + _setThrew(1, 0); + } +} + +function invoke_viiii(index, a1, a2, a3, a4) { + var sp = stackSave(); + try { + getWasmTableEntry(index)(a1, a2, a3, a4); + } catch (e) { + stackRestore(sp); + if (e !== e + 0) throw e; + _setThrew(1, 0); + } +} + +function invoke_viiiii(index, a1, a2, a3, a4, a5) { + var sp = stackSave(); + try { + getWasmTableEntry(index)(a1, a2, a3, a4, a5); + } catch (e) { + stackRestore(sp); + if (e !== e + 0) throw e; + _setThrew(1, 0); + } +} + +var calledRun; + +dependenciesFulfilled = function runCaller() { + if (!calledRun) run(); + if (!calledRun) dependenciesFulfilled = runCaller; +}; + +function run() { + if (runDependencies > 0) { + return; + } + preRun(); + if (runDependencies > 0) { + return; + } + function doRun() { + if (calledRun) return; + calledRun = true; + Module["calledRun"] = true; + if (ABORT) return; + initRuntime(); + if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"](); + postRun(); + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(function() { + setTimeout(function() { + Module["setStatus"](""); + }, 1); + doRun(); + }, 1); + } else { + doRun(); + } +} + +if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ]; + while (Module["preInit"].length > 0) { + Module["preInit"].pop()(); + } +} + +run(); diff --git a/d2m/converters/rlottie-wasm.wasm b/d2m/converters/rlottie-wasm.wasm index b7778c99eebd14f5fbf655c4a082d8c5562ca351..ca65b5b6ff7139db1ca9106fc194510b1bd6034f 100755 GIT binary patch delta 104498 zcmeHw2bg3^m;w&Su7C1r}EKiNa%Xc|L!;sNg~-j+M`iZ8vdIB~+E{$cs8cZ2_qI%KRkR@!!0 zp>SgU#IfQ;{%%JfHg>3g>S1@om*Ua)7(4Vy{H2;d=;%`Msbi(FL$CPeT=79yoO4I< zv~uCV(S@lIfYQ@Sd5>LnYGEy4fgTQ(kB$vyiacl+jzy^75e&7@N!}GbL;m>^!yf%O6J@SY8etmDvAI$eU zdkP0Wo*(mzhxx(6?pn)lw(GrMwxnJg0lt>+CBF`8y8<7IEe4gte81N32fcwmTWhus zD%EB>y}`lvfr2y5cEj%lbItxB==r-l&Bh@II)%e?haR}B@W9;N58PBZGI!X4PZjQ) zyT^f_7w&)mdmbLCG~4Rb2LE!vD;~M?UjO3z{eAs`m-`m@+>t?_SAK)vtjH3z|c$Pe{arym@C;2Xum*?kZ82j}&Jfq!oQeuoDO{d%w2 z9RzKF(Qlp?`13)nd;j}MP(gclv)5_+_@(d91%n6NJM;y9JK~W3pf@{cE(Ct-f%gZh zG)T^Bztw5aox9LI^5ABBcV`y^RyH4WD1XMsX7j;!Coj#h!{}S{sAF2eT&>ky=mMjU zjj(K{IoE6tj(&huNVB0I1iOwo-1@e_c5zJ4?H_x8>qDyp%$A=XaxYHhg3tM1A9Q;C zaS%w*>$N*TPQ5m0cG?d;#K-4dv&~jey}R!(w$+<=knb-58-l@vc&c~zJ?voLA9O%2 zY!Iv}1H)QD`?z9%rnhVIphjx}RF0YUx(Z+s3})A?hX)YG?3xYGQmsD^w5;6-kFEZE4<0umCX21XEL(fL zd~B|rhNnhz_h5GIBTBOioknZ(p@8&R!9w@UV6ggxgZqP8ebEA9xWw z5(Ew(PC`6R1Q4}G13;VvPyNLicsjWP)Gu^)2U`z9aPz%j&nbuW0nCnIa4uWuo_fdt z0O89*ciX**zDP}Z6h~(E(ZJL}Z?Rn;%r<7%Y=`d4J4nAdnEiL?hrwRZ+D2m*NPY~w z@^6c6{Cb+H*q>eV*aykZ*=#olevb)z;bVVow&(g)?{WW*y>+bKY4-;cV+Nh+X;7`8 z*RHjur-8F4)d!2U)>-?y!R9jsjI+VoY^jo$3Geh%I4n1iUTu%xU_g3r5p^m7%o(@m z+#t4N7Ycft`-8^xwC}H(o?dJ>pS&3O-GK#f)tD*K2xe;_dA|{Wq6ExpcFPu|5@A+N zg<(P35XY($Ue)vd<35Bj)6-y3YlEQboym>>d)BJ~{bTK5PZcFL*A5zED0^?moK*?z zVT0f`1JiRe=)^U2LN-(Rk>D_xK|S^QWh0d+G9kp!eZzr38Ue}*iC09ysG5SZNuOXe z3&k>xQgIf_=A6R!i5Evye#f8(gSt|Br4gX>Z0SR20_XnMURU2cy<4j{2mAQA$%Nao z?7h!B=XK8h7Vd7$rDQ8J)G6^;9gH;t^cz=TWGAL5p-X}NnON>VlV;ky7YxGf4r4DF#4JW?c6O{M!Yf(M@_rW9ozZX8pM3|PESL4r`>O19Ep1E-mVAg5*QQ>eQiBL3m<)VCIWh5dK~n? zJ*@S2O;2}wLATeb2mO9^=b1r$pj@Pd`{Sy?UVpCDnSq6ZTCZ2z=Qm&iF}b$=aKjo1 zeFDuv!`~CscKP+rVtarj_8{a|gWxUxTJ<__%=KD~FIQMP3`T1wt}awHqTFxyG4`>_ zX#E*IRPVt)%Gm63S0d5#-&ncZSa{vn0M=QM-iFH;sYql0A0BJvmc$~>6~cQmayS`R zhO0%>(`R%*8B&p50bJQQ6)9rj^{zHpo9U&|(5)R<{Sbp1AK`?u808y^oh+_n+NR~S z)6d#j>osfb!MNAoWZFvawrJc%@0suhVytO(=3v&zyZAJl-cMeu-r{|9=N*8)t6snR zA^PVYj%vD7>z_~B+w6OLiuF!^c5kuYY|rki>ThZsm{32d?mSjM9mD+R9TP2jLb&Yz z9IXp2)VkH3TxGTyNC@aD+pc)#*2;q!3?DO1uoNAzFnb3g;uZbZd+L9Pa{MpKD$nO~fpU8uEzuaS3Ij$^BtF44X) z_YhHj4&^ihKwxPX3e{%O!$ZIV{fk&`yLmTg0aiXx1c=sY`FQLA3-9T4*wdY#4Ygm-mrwH&;KyFF8DNGBFnUm+EPpo(nv^qF(Pd!F!Knmbv0VxSW;WG)Mxg?{{Or?)UYjdL%k%pH8OSjDFu zf)Ur|%J8|pX7**dRy?#*AgDdhm+0YXetUbiBLEqvWjCM%A%Dqg^SkjT`KK*g#-$Li z%k7SD?@_&4qZy!szsMN`T}(UGWa)gTDu;uP@?Y(>_|3WuG%fV^<+&&F$d{>-1$opz zT-1f!B(Q{nQ0Ne=w}^=nJaPgjyjSGz8#AA|K6lWn60E8B#W-RvS(M^CsTKrN6j~0fc zeg|D0G#NMKR@q;Yi@!FvRTS@ZA!Vm|9wZe>%j6GgGcW_RfvIMATjn3{b-7c4=JT3e zOxNKSha;}cjy7?ao4^wkL?j<@wmx|`vfOG32s5$3Nr1&yNGcO*UzML~z50A}cOWUg zDc6y|V*(LmHNe~{zJ_iOijdaPFk)AMNjiYF+wnDA(c*-6bMCRS2qt;?JV`7Q!P>fO zGvKo!JPFKD?Ieb&K-1GZz#5;?X;itr_TG>?QhpX+9|xZC$y^D<_c!LYSl_v( z3Hxz&b1=WH(*gb&hr*^D-n=*EuFmuPg&wPQn~ih(9avd-lyF4O*1GerFv z(0?~mI9rTZxF)c-2Sd5m|sJh7;J*axO=8g<~6(3LOEaJ%Qy(4#o^ZUsVZmxRo%pK$WDtf#?qUyaX zR0@(xp%EN@_}C-IEAO`4o_IIzwA#L)H!)@nK?{)Sy*Z`ImZ}FiW3Af4Or!QtIn;sw z>%-{H^C-pO_U_z+qI7hzOYnB9-t9Te0VLKRbw)uNB#4mLa8Q_ia~8NnLrJI6w?pqSUd+bONY}yUCq`)CX_Cs zg&)aX<_aylr(q*vf^#@FsM!d`;PcVk!VrqB<&&z}OfHRVwyTVLAIlvZ)ds0FfzpI= z@8i7Z4LYq)ArJY@7;Br@oxzw}`RR_FGtrf2AIokwT(@)M7I^IWNP>uxodJmjL(KgkkoHSczFVY9A(@lgIkOYBMgN!?tL@2 z&$Mb9Wg_MclLSb5j&LWDIigM`_IxXMO4LO05shKwSrL;!FBTj3zMXq|f~u>qu~X17 zq)7T{)l3w9C-;OHiWH~}L_tx;1L83HqTi=xU*F9=BLkVE$V?%pWe5E)G$V_+!rx2k zROpcmr#4u_u$Y;tV5H&i=bq|PS)s9KbXxl$^y@HbNfu4j(g-jz7mXnBgWQv&LKaIG zmT64TTQTO4#Fdbv3;ef0O?W>{V)t1v<_*Vgtf8rm63cJ{qnLC2D2d%_L2bCkRjqgg zU{irc;K#X#$IDx3E+j6h$Ex>}+^JCkY1bzUFt9&^+PL@A+&S^3RZmXv3l&OYSx%a7 z;?n;tx7m#-v(wi?+5w?K+3zvc(#J^;Mm>a)%tq7s#`>@;p zyr9>S`HWTXSGflpzBN029~@`MpTo`6az*{uxzk-qqOkG-3;pTY*1#0)i;Fr{?>CqX zq8#PWR~N+4rYhufs8-ecZSIsXOd!Vv&CX9Zz@T{5`<-y+=X<};?bYy@Az>+dlX=_- zB1v61@jU9m|CxD{SN*k3R(Q?U69`-z6vi7fD6f3KLVOQRAV+A z@5211*#WGn<8+{hr&4r5XW+fo8^|UlP+6Zedr^KhsoI3q+(zK61B0Bz^y&HaQ>0F3 zagd?X#re~(xP4pYzH=7vlRG^~ACOqtL=m}TPxCIxd!$j2_n>2eQcwU-@6vqjieGK3 z+>co$5*rIQZohla$Zr?FDCunNw0U>1ehm}CP)R^uAz;tU|A!{UhZA)$g95S|nqiWL zK-jx1{{#yae=GD?hMmItIgFtb!3e_7${(i@22>$C*acK*74rYt`N#7D%vbC>buS)h zBM5R*U00Q!VM zUyU}rlNaxK`J==u3>FM>&ET{e5V|yO3c2U!Ptb*+OsBhJaR$ssjh91y;7B72EAZqe z??Cw3>q* zz5409AtT0A0ZO?Q(o48}3M4YBUYMW6O8*JFI!y(ZRO3bYy_E-bO`PeOG zn_goXY}Zr~)RbUNUU2z6&S2{r5T^-MNtlz~kiU?O9?OCZ9KJ)^cOny=fxb5XK&z4r zT?KILRUi2Ix_EwWxG@j0n1GvQZqV4)8Ic<-M^_l+eBA*s@;%|*lwWVP)>F)bWkqlz zSB+s@_RPf=?98LR*tS1})uKM%!>8Tfn^5m#fC1;Y8Rs8#jenEys0 z4lcH#;AY*<$d-RUqw0%h@ec>VEn6fxBxVVevrZ3^caHT}yL*Zjpx#tW_S@-?oP(on z9i-#`P}5~6HHsL_JyS8VWbrcC%?I)Yml2Gj-fQ=!{Hu%0Ddl(}FZTEnUz()vs=s;73$PdpGG_1~~v%^c-7P4s$@&w`xLl(O*A)Tf-=YKbI$Yzg8H_oxmtjJ8N)Xh;$Am50v^A>c$4Z?%)CG1Ahr z!L9jUE{CZfQBmOVY0CN~PJs<~DNv(enmt-kK>wnH|A@4VLW($i&J~<6vJJTT7)z_%g!~!yI8LLJbu?|2eTwN8&i|OpW8{ z`Iqhyt`4zsd|^6tZhC2GVxv1P;Y-%;ol1ZAE}uto{z zj_|Hvq#>3+#WFTSE@&8H!RurBjiO$Z-L$Hs=Eqfw{O~Jxi*_~4|CsPTk)IYVibJ#Z z;z>YRYV*pU%)b*hPOAVf9PI{MDqPtt@=F^eYSw1a|KJt)!A(9MIJg;!lzC?cu1OCz zXY@!x^FJnxg4aKAMsAtdLb5w#JNz`76>s0*jn`1|{3$q!6x4+i3tPk$jSQ0OXyFy$ zg#nMib<1EQ%xh;brpK1XPc(83)a z5&tvQRzMqZL4OwR&M|D-QdQ!gQ|lkzr}7cKhP)`Q+NZXH1vuTxRDz}EM@bv6EZV2@ z7o?^Js5-RUFzh!}Y{m`LZP4;qJRw8jGx?`dUK-izoc<70Hm!T_2v|f@6F!?i4zdEk z7GnxY15W>gBs97GSh7kab1A;D&%wYEtpK&0#Xq0_FT1oD1Rmdp%rtR;S~!!4juJ4a z(E$Se6naji_l5k)F6a^bLol`1fR4NO#k{u@ceQZ_z+Z7Dg~*jKG-2k%xc8+zJI172 zg|d!K($$yq53xW74L7*2K(Y+qt!Q*$H%~)`Qvi61xYT4KX-2|Nb|KU9-$I4FujcQ&^Au13kteiYKUcl4%PVZK|ujh}^-{Q}p{zm>Z zQVP5mtd3oSEC5K#c>tLZOz6c=3{HuIO_ccubMzk@^ue{jW3`(L7K^=aC3rt;Hhnw) zL)XSWJJ-c|yb6oiB>Nm-O8BaR7R3J36gqLtsuB7{BY;@W)gIiR$@xh%bsQS!9?&X5Y{M*i8?^j7em{4m?z2M1pkH z;Z>An(9z*l^auHOy2(}4>LUX{{%~5KIF2uF!@3b2uO^L2d$HBR9cwW4wtCPOVT1JN z@o=xS#D$0dPrxss!;*fO_v18{ON%4IGq)orpXwt>K^MiAT0yLhruU=#tC2-A{%*7{ zL@e51e$*--qcIV$AGNWGsB*=E@xhEq#>Fv*|GN+nHPJr*IRB~)^3=HE-q?(&;yGQ0 zRs(L@qud?cd@5|uSLk0e~>ir_WA+flz z|4@gSH`w!36)K-Wzq)Zjj?W23H8o<2E zi2V)K*dnsCxp%70V*Y(dhhy|n$mcY;e!c4bA@4fB1xmmg`7_=$Gt!1PjD~?5rlIpEe8x8sQxZKX(T&Byg%jtD+U0mkLdS1t=$0} zyM)55!j&EJ5yoIWOXK>fZu0>BANT&8-*1W{1`y1JTW*cQt-wNvM29u_Y0rPjuS=v) zTYl*H`nl@;HGgW>YUVm0INNp(=H!CCkpG4&Ab-o>mON@<>5OMY7`uR#oO&Xa9KJ*R zKWU`m&B9q&>9dl*r%~WOJ;r@bk|6&Ze;0VFpmP6KEt zp_L6q&U5m`@`%Ae_^mviUdBzAkybum$2DDr6DbFPx!a6-2$T zknEsxG-2f6f2-a_q&PF~jOm1Y^oR2NZ`FHx0j?NQD-bfQrp8b7^~^^zvoqf@j_AY1 z34J^S(-+kZ(X8s(fg11PLb5N5@&?@SPu07Gkm91mDm*!LX<>hets(zYluvbJ!8P30 zs?C{=Gp@GbREWVoEC%6XmF40+qwquvgUI|toD@8YD}$cQGYhVxmW*TtedJm(Hn=Q> zg)-Ctr!&FzDOK-Tg*6rdVdqmB84!_rta{HbTo7X{mH(;A>MKOJm*$*BjL!V$6pG!> zp4rO_Pu3(j_1_>PILCcxqva~5uA3 z2RjQfhJ9xWXSfdsJQ&==p6rq?U!^X!_xyr=t)x&4IkJK;0V_f{1>-?0eh5Cg=Q|<) zQCz8apsth3db0Og*(5x`#5$2Ib~*FSlFWV#?|4KKP21; z=m7Q)RqsWG6T<-P_>*110bz3Lih}ECEz`rL?ape#*a$Z4#f5g53~TNuQOMMH@cad% zoo4*7JI_lBcGM`X*bvYHK)v;@bt;(dj%)*EkrysHW%e&TRx6u|xB zq3T_ofIZBAA@r=*Ebpj@6k$QyV8&ZtLj(NBz2-Z8ZT8j~k#j6sOA9SCIg(p~YC0CukI>pc` z0u|ys*dsz@B3JD-&aub{)&PL@m;ToIT6+%G)OnwWf4mzCaQa0;v>)4xZTq!_jTtJd zW*Kewb%nSvnTT@9Ba>sBzA*)FU0<~xG?=0Krj*(ki-~Re<`k+$Wz()|mJPhurzoj( zj8;ZR`TisC4Jjj<_RY{)TMlpVjRmYo8(7I&w_TEx>T8#T1Sv*$YvWAe={%a=>tQhiJ3$uy+ibk9;#-H%LfeP%-eFBq8B7a8fiv0Jcv_H{ z<-fC#9397_W7MLReug`dR40q+V_1|q>P=4FmSU20Bg3kDWKhBU1J#Kvb$@rkc_g|i z9>KvRn(@w*ZSBT6|D|1N+`GN-q{NPAAu_C5^7WVZ6q1)QNDGNt`QDU4h`>=wiSbvw zqw9Tz62v#!jXMetHPaDuxNp4`z}cMIaG;a${b@bh>>E zARzMC57oS0n0mtiju9)^c2o5}Te#<8KHQW(I19Z0$H5x@=MHoaIg)+8 zP=ZKNd-e+jek0s`?~8@I!)a^!7VIw-?(O?~YW=3)-`B50(7^8ngD)2Vlqkc`D*TG6 zgV$ew)qLg`TYs%^kPqqhcsuRa3-^NkpIE2^n+0G^zP>AHe4|hroCleu-z*%wTV0p) zt-_($1l(HNYcICGU8pqs{VuF>e~05a=1kXclC&S zX@6d*?5X$g$6pk-85@;!;vR(A_q6*yp2imnvI|B0W#L)Yf=FMSWesD(5)Wmu&w#Sp z7AYIRK9M}P#kv>hR|N)?L7!OWGcge0Y;({D_4DD5WeXu=)p1jR_v^w1%*B*t;0fqX z0B11I!!fshA5JugLj%_2=e*w(NG5+cYCHO#apXh~PSRX}9Sw~b1Ry6B5cqB36NC@G zxb~W#83+LSi>-m##A9-*)oJHOKTSwaY-@!9t;78jdvA~|7Wi=KGlRjlKd_K`5M-3rYHo+V#f*xqBeg44erE0>BtNet_{4|Hz?} z$2t@1EMQdiCmc=Wrd^NYvH*>hTU3IoQ4%b z)IxyYDlq6Zcf&qXv-}!beHF-v_62EtuZLkU%k>#*}c)p2E*ie+G)gIN>3GW|;T?S&Dguoa&u!c1;Tav5B*67t#v(mDty8Vx`cZR)pJ{RBk}LNgc|f;@$1oE;vas7ELyOe(`wII9Nb16R^v( z4)Zt&RN^>?0||tI`0pX~FDO0+iL&}gt2tBec73FO0bHS8Fw0?~zV`SQKnn)nPbjt$ zj(Zmt*C(L@TM6B0TvWU=r|mP0izs;TfhGuEaD63c;SA1B?e@d<_?~k8BT`3hrI)bn zGR`0IOuRXvfaBiNi!UXe8v6qt*rrBC-IxIcl4~?)XCYwSt_OTSF(IV^$*48>J(&)q zKRf2$#l=08^{t@a-%Jq;4He)OZW^i8>%b(vN8E>mQ18;>gB2HXPO7gp+OHm~-ZP3vs3OzTFsZ2F0pR<^ zL)Cj`@l5koTY@3(Jkl2wF_@I$O-VTEysY>iOM?n-5rX1|Ufxdftm6M9!c57D00rW6 z#o1Z9BiKMx3=_o~O>ow!I(pBhR%;fnsoL!O3vjnWZ2)V!{x5K5Zy$z)wk6Bw6}NFS&RwQ)Vrmwe zZHtG*TI-%oYI@HvZdZ&2{U3!&ypOd9`yBREy#vKl6|fOtF(7qO0D3`j(f~poR+bl^ zq1d1UQ`!MHvz#s73yY_j3ZqcbovA_mW46|8;S!zqqT<<0gSWTeU6e8tdum$)cZHcT>^}TH!c7}Rf2A7W@F%CPE^ajv5Eyrm$1Ee0XIpSw zOx3%lc(egyj30DWkY-i7YA-D=m_l`Q_PHEYUL@(bi+vXOxOXHr0zj(E&6Ye|By>m( z(AO3lra>ud7zp&Bm!L*W+<}KqsH37~RjS_0ieJsmbUH0UfpRZSurqoST2u`Rt1RX} zj;1p}(6((<;sF)hld4bT^R6rY_Yz=nP{!k$2ysuK`|{#RriuE1Y3)D(*fsRPgf>LX z-=v1UqWDAuWHjbUJn>BYhds=0*SnsAm)$$#Wr^T|@X_6ds3}1+`^w@a#*tKaC-Tmi zu`l*v6|1)oT@L(9Mt9P$Gl7CzT3GQc!%0zJm|o~V8B`D^wQyAw4EZ*_-m8m`4TE)4 z2;;BIrYjQnOB%h`6gL@wY6Mw?2_Lr}ga_(dv!S-o-H_z0GHg9% z7}uX!TP<9mAqQ4&|KVyUxaZOKp@mq4vb?$YBqMeSFyr{DMmsYs z*aIaK?YyN}H!X~+SPlXN{>lw~E44~O=iyE=SS#ARN4$X-z+=(!73#?+4M$sY?$^Gp z*f(ekjw(o^2A!p7YehRGJBKWH*bw6Vom+}O&+3OrbmA=B9&Z0&DoPu3nVmE^l7Gs_ zqwOVv!!ze>&V>Z@z&yh8aQEn%*qQGp&bLqJFH%a z{ZCCm6wxup_npN@8JuiYN%fs1Xz4z^t5{QzF#e+leB&@@Cs8@T5hv=l;uBOO(Rs0k zpC*6cAs;7cpE;-My}LMPfV(V`Zom|ygPi=lC1FpVEaKf>e6oR|Rbd&Hh&e3g1NQ+~ zh`#p}dn?nZh_st^OS1k|_1;^2zwt~>Tx1z&Ra#;je{kEF>ix^dSyUl<0!j}OTN)dvOy`p3JYc#cC%u69E94WZzi6Xw5zq?M)jC)ptcLJcjxD_f!( z@cl0KA6vcn*!6+pyFu8R|K^+xSOUWP!eMO%jtl55!c}^30EZQ22gLcQ8Drz)W2&@R ze8samoJH*eHVo5Pe@|#>&91KE^!(@qy2uYc>A3g7;*CpG*qA#>h{XzbPOd+o2>f&p z?!6eTFm#2|6Qnob-C3M9WpIUsufI{D;hN{Liz+B=x!$R8@jg_14q;7rF-O#ZmRRii zjas1yFHkrdqe_^t?C{|tSn5!JXWaVPebUueXcHmcVmFFp*vOIYBh*3DhJYBfk@FuQ z^P*O74lswnq!6eG*hhBpQa9)x5q0&I>^-LsCL zjdEFx9dS8LlH(sQI`hnyr+rK%1MjT|0e@UUso7ye)fsJme4?0?2n6dNMpDR-q4ing9Cu;Qz%&f8+mIdv&Ohy&%g_$2Y>_@vZz(@&S z1^FB`(aVe$469<*2;+dCFTN<(H}+?lo65Ls$*^2>-SP|R8W&!3voth8<7|Zk|9{;3 zBAwhVGavLyGkV#eSyk^#G{nYeb4wkDthTD&m+3kPZHFkhk^hj!$7+kyH97T_;$>va zk7>~||2~YrJOF~Nr={F1rhmH@ggT0uJLCD(U_e2wymCUCVAb*0LF65!Gc-!zEfN@L!ht>yae3Ix~$}e znw?zp-5AEpabL>csC(2#Lc1t7m;Y>U@B$I<1tKz|O60-N>Vr2k_5W(@R zKfx!S82Qs8Yz=Dm_wrpge(Pt&3MPWKnhQTK!UmuIJVsl8QLHS?z}A^P^ET8fSG0T~M z>WiGG{ZphZsXOT+4VX(7a7c1Nm4KdBI?*Us6j3+^U_KU0Pv*a$Uus2Rm>xmsl|O2H zhI^MNUr^E+LfHQ#?N)n+{NxrS#0yI|lg>juFSWE3GsrXbJ;wP`NP*NTRJ&J&Q51bD z{Hy)$T~xXhP}cE33SHq8gR}aaj&k}=EXcp*(a5KlE*_16!yx0>48>sMhlw$AR4y)k zk)W~+o1usdBm1Ga|Dh}e1T_4N`=K;UV)V^)TYaSgQ%>6@rISrZ!}6@oFXdJhAR2+^ z8qmWu)#A*FCxc1w-*=CoJL##*CG`frZ^SFZ(A_J z4z>f}5iZ$~VEq@2ex$jG8*b^k4Pqn+GUs|clz$5X!vr&xy7J5tSziX$bQjxo!5g(O zN}^ZJ^<}!eYy%K0zS91S(MU*Y+`FtqHd@)W@V=BqSSwfvDTuU#&nl5Lop4R=D1@pq z=zg;9vr9LWj9OXtC{@x_4NNx>N+9)*a0+|{7~B0_!M;5V?hCV2oh-*MUCil3WS)QP zUSwD#97jQ9tb(U#Cxhe%0RdFcfW9k8=aC1Me@PpU5g;v+&e)7`4Rt3icu$6U!dTfQ z?7)WXrQUN&?SOpO_xpmDqgpV&LPgGYyz};pW6X+-z41!gDYfneD$ zIzzQJtTHCg?1|^tt!qJWM@aOe1109#Vgs?5bdMkzi(sJ}vo`pG(ix^JE;21^I`GN%LMNXyq<$@s=mB+o8mcB>VACrN# zI-M>z;5-rBPJ@b)8fjl zGbMzrcU=i=iH`q-Cu@Uwuq&{I817pNLg{0241YNt{lnE2y%sywOz#yXxHVje2l^k~ z|BiR}T2;7aOF0VOCy7Hr#YN@oOZTQd1rCCO4Qj6}9n3?lkVevaRq2e~9oB3Rwde(~ zK;O5gwq{fU>4x^IhxN{Tb?K_%f*DNB8qyt(Wwm>XH8B5JX}iY*e9HK}rqt(MQT0b2 z|AK@s-67NaBZrry1=JMCPI*J=Wp26H>(8UH0>;>sC{z4+WanS4RFo;y^~o#a>0C-W zuPrgnJpO_74I?2{B*tc{&tn`yilAivkr*Y>e7>$E7C|u6t94`PrEalValfTVjU@#K zB-AtyiHw+kZ?Ofhf`dd(f#KMyG}#!o(?08#`d(szu!ZZ2KF z6cI6kZ)C_@5sPOVJUo+jln8LTh_sngf_i=F5_9~E`pTSM3C#Wz-4gYgo5E4-;TuX< z2&swrw{$@xW6+SwEZoqcq-Mo`q6)Bto+Xj7AVj6c?*xHCg0BcGNxhNAOtjq-0Ev^$ z>-foTHh2g~m2^nQy*HJ9X?cf?vYt=Fr3Wv76SteM3s}%3Y~}5WZI{U=c$pAtI=76ts$y|-BNmz_<_#9l}b1@LKM%g5^pbE^Z)tJQ@ce+K3vVkruv?un8hkg|*89csMc$=<}~{Xgn|WAqemVjQdUJ6YdZA_w_E{*XADPGlPS zEjxRLrx!t{5ylN*Zob=vRBD?f8I1w22=N4-{Zq`IAOeQXg-)&PCZfe}S ztwc^Ej)A=2`MV9-dw1z|F8@Pj3|J-w=nPv(M=|O;sAPsLjtJj>rE*AUyH0?#fXiBD zMa;m#`FGSS!9WtY3^Wzs_7XXKQIAK8R3ZP=-R9A300#L43JBc;ZYKo1r}X`}rTL5` z8;W&`353h{@bhPI`?*wL%R|tM^h2)EXGDqnJ39Vl`AQif#1RE<1fhwR63z)>xXC|V ziavp^80$S0pwRgTmJfJWumtq~u4pElWh?s&#Q#GMD|R{Wy(QTBs(il{Ku@1p2Ju%+ z{DvzjN^v_kRPUq`cG)KS-8k=B(;^E`a@`5eU(p=EkzEEA?ojsG8m}GQVkl*^&)VST znI)%ZiMhF>T^9>1<;{qgwf|Cci!w9bJR`r9NBjN~Iq*pPPfB=vVh1roJ{=)T0L>2T z^8{?@z_RfPDS;1|990q>wE$*X>ycn#uXO2G*345lVa%9+GP8x@2mAt9@zI=zU&Q>9 zOggpVh|x-yjp;imGRjykLYV(#+kd2cENvB#v0e=u<#^)VS?Wu?q9Y70HrVGcDfY^; zS}0o-Bf=lt@8jNws0(HreLKOQiL=LT9}hVbhOG4B!=*PkCN>V>#Hl# zRhiDMa1$KQIV$;d30O);))1I6;5@KE$JCFPZcODL_fM90~ z>44(+X9uo7Q6dMSy1fu9yxi@vxEv9w4lu&XCo{H=d!H=*!{JLfT?rD$mVo5ER=5)k zz#{d;tFW5WAX>854SPRM^q)TWg-i+KJp>>fAKX9v@l zpq+Na@zbR@g;@sYA1SAc8k8mU=;1mvtiRKC(@9P3(cB`X!;u{t^*^Lx+z#4|`^<`2 zLdd_D62A+bKH~G?_%oaUVlAPLsp7d+pDlfaSSXz9z{#VL^-b9S8jUWZ?BY zkWA%pw{f^Hr9)a}t-l!3L`@!y5EFsVmCiMm!U{>GHbQAJkpV#o+{rx?%!Q8fsCu6- z&96vn(~*nFnD>R!9;A~5uvCidQTAzZTzZgzWiW$_0r8?GOreq=1+$<}Whmr*5fa!; z{4EO1#7rWG8-9sKW0xWhhJjYK0}V|V+z)}-yDBZrj+>*c8 z+e8RtswVUmjU2kkcqjK)qM;6GpT|XzA8?E@+|-Q8MtqhmJq0~KM&+?>sQ1+pxyV2- zN2r?}<%)_J>dE;>tH#0$9>O)zE#cbA*GiA4swEAJ#ubmz9W-b6!It?(10#D}V%o(q zRTV>+)L8H9C2~N6aNU%};#$E&M?NHEid51tJCy&Vo8k>C=gR63R%oAj-zZ&92IbMr z7?Cs_I9O|NJmL0Bpuke#hyj-Ie-Z0(6W`!q z1m;W)JH#9atSj{httQA5y_dj625l1huDV2IgO_8iRl_e)>O zE-YqEK2Y>@?8bLH)pEIo!l>AOQ2JPQ>e7j`;@@|bDvHSk*F%_p{;>4#rrYb0 zM-ORo_~r9>I-F^nN~bU0k4kqE_LvQ8((;FQje-3&WwcS^IjM9LCF%hfSc)d>odjnR zMq9qkk>qL(w{;TuzbFo`aqOM^F+Jx%`w_LiyyaPBW=sHL861b7l&qWURCnj`Pv-z1 zVYnJ$h8^O)pO!vNWTILYeT~E)MjLIH*ddu&{YkboWL7K<=+6?!GMbV?RW$yFd>O)k zrdlkjwh{Ze8~0zzo+QcodFg8oX*t3&F3KFms0|HG^}#)3WIDAJfH72Re8^Px7bS9_ zIT`TvNUaAlBu3Pe+SQ9Ruj+2bnjOyCX;CV1%z@J z$ppI;lUn!d()mQ|^eCW%LpTx3JOz=G0E4BTz+oVdMiOxUC?1=oCPBZkVya8gM{BG| zn2~7tQ=M|@T-v-ul9vibGeXeJ43qSLDnaRY!!BY!WFhajC3-#d(7g%bpO)Qqb|(te z8l=PX4u#b3O5Y~hq(&SEiy;3Y>a#n_IVF;H=t+Wy=by;$v72yoeLe;ePxtqw?`I@4 zmP(c${ag+*W2uDuhq$L_I%vrEmG^=`o@aT8zz@QR?uB%@rmQ#AN;9wVa|$whWaV!UDW0$c%@P(#-QQG6!jxK z8d;GD6ob|BKbO9Hm+;!f>gf3c%cWb~SXd1Ii-S&+e?=Lzl#y>ZbCJRRP{drFh|lTx zWg#j@>95vu)u6c$#GdIES%1WSOk1S=8lSmqzI>ZD)%@RKpwls-coNRg2{GvXU_s8@ z^ni|#<%{ya{zkVOSzDOewO+5Y#~yYCYWS}-&wnpnNrJeE9Jd##SeCQzgh~O^eS}QB zQ`a)>aBO2s2a(8m$)A73@dwsFEGdqp2ZlclI5}}B{y_dSGRzKgEgd5Zf*InYnyK{f zpVoe-J4b;e;rEEEl9~en!f<>zTC-F$?(9_Ct&kA4RUnUl*^Y6scA9gXx zB0SLt@1w8=&e1wc5(u&XB}<5duAiM0|&P36CxU%ocgDB<}pqfuo#-9o25nCJX5 zNM(ijf9U)L7!!n6vB3r98&bKn6efuSg$$2uox=S`BXyr7rP2%9)C{@IobkVHtkiZ5 ztfNjkmA&x7@`xjy+faq%Z3`yMtBcFmEvvP1t$!__cMr;%VmkHu>k@jNSdfvc?qUeMqQ)}(g>qJ25VYH9~!KX#){$V{el5NO$Y4a~FpKOd^csGq} zexxx)sY)QvD8Jqz5@-B(Da4x*B*R=u?SG}5r-~@)2t_6xZL~IPxmNEZS`*efL$~4bekHHSSn@f*> zlR1MPRoqUUqkSv?%B>|ajDsmFm43R`x_uYSe>RMJR{0J`A6-6e!-+OmHZjn{REyzs zH1k}`#X(m$OZ8|pAhP}*!@^M4BpuwkH^`oPJ-hstY>Zs;_#;lfgBydh&A~L`l8*5C zpLewms`H4TP{d!M-uGQzpA=Q^526*`_(1GDUZ8%YB%&o*$x z&Hsnsqtw6*6E+9X4Ew)V)BwWz2Q`4tEx+q7R6c)S0ON0_D3gPAPJ5MA@6_1A&_Q=X ziZ~_NEZ_6WAIw8X$~vhN=N~$#TT16M)4SAx*g5}@=wz4~I}jg^nHYa|jU!^V_x$qP zR#+V||3EJLrce8_Q)_f3MUKh*S6a$vk!}Y@bYI65@Iaa7ad9UQ7J9=01UBtj>q8mR zwy8|e;dS>{>?axvG#&A#L`{*{GAG<;5_Ws>(}68r%c;;hc7o$#(LKZyvk5A-MMXUIE5 zSlH9zGIhc8^Ilf&hxTrvfbjTHzti*Eoxwb8%v1Rk;Qzr5kQ{mHwGBm}^X}J#rOtu^{l!Q2^)cA3I`4k6`C=4@bIl;VA2b9tNxha{Rp6%7@u0zPvES)7deHqY+79aM7FR*5E6r8Ja1oJKGCp+WVdtPTAA%OJNWsp(*Jr*d8>hu zs{hl|J-C$&g2y&M?}l=3C@4(zWP7hS8Fx}qf$N4|TmHp}!{KoGPXw*eMhj{?IRq9u zeq$5^A3q!#QN;NmG5cRAo?$;n4Xm##-%8X!t^vLRhg7-!DC~dX2KX8?j@+7mU*bIICKj8Z8-tBG2ycL4d%CSsRKCIKS2-AyfTq>HliutOmZN1SMC#uxE&e^Ev!%6HM0(Lt z!}SlNXfdG}Q-BylO|8Qvkl$zmws&)xoDHQV!bUjyC)xdaeVJ_UvaEu}*wEu#f1(^Q zwn5fL@2|VqpNCxw^fE=~()CCX|irx`txIv!d)uis4F7V7#)P9}Ok zX8+q;%FkK`Yew1UG(DiaV!1+j5-5M&8As@gw8c^2=<^q&rYwFmF#R{1lG$rt6y+Kd z*Uj^@z`2tEL2v1hzP0?6Wo#(g4n)3dT~@0%V=iiaTlsxid$I0L36#1J@O=icxs`Qv zZYk62jfJ0M0?{P?;fjKy5>t6mrN(FF-s9J(MXC@>*@h}4p|Tj3oz zJqe?z`mitVJU|F@9FJN)D$x$b2%p7v$W%>H6;Hdc5fL%VOYpkNc z`T6flV|@bjbh+aBFIJE?tS}lnhSuKUjNydBFDB|AiVX-*?<`-H<1*)duk3&IPIsSG znnXkBppF#G7`k3$zdeS98UDr=S)6w7#(?>Z_IS59d$T`eqDTwg&iMbbI;2qQR67 zMhX{x{+k?-PPF5aNG1q%M1!hE5&~e2al74$6Ki9$WI@ zr-5wn{0(#bq;4+;`tpc}wxZ&177@$b431pyDc_V+0w&rG_itqzcw}8EZs

SlsXv zsbE-2M<5m{rvQ8JEx$h!)JDf$7NAld-3|_M{+**;s$KFk=93urT9_Y~U6t%W`?GSg zmqXP1%Gb~#D(LrXb3vbbT}+I}5KU>TDR~&9VY^gXkFw?(YX9vWuzF}TWPze+Vg@40 z4eHFZv#0`CEF1=57Ky&g-YHr4{_?dhyp*tYI3pu!5go$(GskF8X>*JqHjW=Cle3)! zcVY3aRu2a9kL6j~OA5^UVEIx);qIW0=Fo`3f(K)R`QrQie#bZ1nJ?iwYgaF}(0}oL zeoO-1+vmfHws1;S)!aXY9Q5uiKh=O)XH=1Q5WGJ*nHo?4)mFao4Y4TbUuRu`Ya2s29j#^Itc=?y%1Pa}` z)NH`eF}$R7-=nOt;?Fj3#F4e~;98>eL$+*r({>9J-X~a@91FRk%wMSOg)F}F0>$o6 zmhs>)9e-NUwtasvk8#dc(4HI2`vIJ2(i2i(Q}O~VIo^Bq4M)>`b_ps^HhlFD5^1TEo8@^BVLAw9J-fqv|Q}4rIXt7QuGsqZ& z_k;2g7ONmst$8)IcGC~7?86livz=q`-zrlvjOdR-h*CVKrhXjHpN(sNQeGcY>SOt| zJleB3sh^jpLd~CGfYz0Se-Tf?+D*Sq0-+W5SMea$t@(8X z#1?~v*4E#|b9w#L>fc6SFatyD>hI#wSiA1`@eNF^UH^wNxqsZ!wwQ1OS=psCEHmC? zJ+!vAmT4u!&#Ei@F&^(t8~^0~$k3*&-}vXUa<#zIf)g&q1{-DVVSG`)RlUEICyX|z zpNRn?MJK2JN|nq}xB)jC59T|IeYmov@Au)FJ?Y{8t-Lz170eImbr!q)UTDF9zo%Eh z^d`QIUAFR%G9#+(!ms1=L>*e`pXG-*P~&p481=NuL(|K#LDid_I=`|ezG&PYp9@kd zIr(~FalHe(xOR)h(zRg8)WMjw{@qQzbvb91_c7E!ic~e^56oLIdE+%BIMZO0&77LL+>` zAlYm?aD?Urig>cMY}LY97f^dbkSbX={HSoev_ibij3`265rhsd{CsoP#mqFmJflLw zSQ*>oO^Qiw6=qgis8W80^eAiWxrX4_Gh_8If5)%J8000W1d|hQv;<>+9AiD z61D^TWvQz7oQmGC2_Cz&BPn&6D27J(@(Mj8K2v?@I#C-WTh!u6d}peEOWCMDN+i#% zJi(Y#eYwh>dT-EKnDJY-YmDpx0GebX5BNEJ{fhUz%H=sn@72{^+y;uB`lzYJO0E}B zLkZYm2v2HdPwU|GE3izb=AN41g&81tPd!YqfoL^rSqV#DB5YuT3GYCqZko(0_jr>D zZAd}zKT47>sGRI*MP`_+#BXlMCdQ7kQ?;i zW4he}eS5(NRG|lBS`Z-BF9;!UU7guN`UI9W@$1*Y6N42_u{6#ev zA<}fl2=pZtvWY7b9u6)-W{t)rf{LZb(q+A}asp8!>re_lt~UGj(w**hvi?;S5}&IN zU}t$ioOkTr{m{%+L(nX;@KVQ_UrjBQoz1}!>(^^-1a4J^$f8s|4cva0SYK25uSBXj zaM-jOf}ReX;WfA~t*lLiK7ffvH|Qz8NSv;<6gn!2P%Y3R2Wen`UzXZ>tFtH4`gN(T zTeaM+!^+0XE2kyW&aboKH=1b)Sqdv@Qd;ne3OV(_p#|a`Ld8k2{|HPn^!m!qL{#j$ z8XcRf$_OA_h14r64-Nyy&tPHqGmc}`dsXGAxFWK|0Mw;v0ivM*-AL!9VKzw_ zVtFnpri6b}<+1V2lUiEr2*P?1l2=&VOa-c9`*MkXKmS|xUSBze_&+g)Ca2y&-Qp}k z9h1nK9Mgey1IAbUV1V*FvwpAEo(nD7Na4J(vfXe8mT!RepaF-V*LFd&#A2JTLEsUG z4cKlB+4mxtgBr*4`30MlBl4!oXUu#%lvya`Vw%Et75m3$lM)GWqG*r`4r>z|HTc!G zy`bBI{F1Ouwgvzg|E%{RUx^ZUZ?4eGXe>EN6G5dRDHHKG`8|d@^%d;J@C2q1v^s@+ z+qijesXYJCW7u0OH6HV>%`|sUAyTu8?FO8L--g5!e$gYBI_}(QL)yonJ`a;zSWVEy zVY0tq%IQvoTO_p~{6)7mdX?DM(eFwzi(Py z0(Q6>`1Z;@;JgYvlM>Qm>$U#i)(V6YEFznJq{%+y^3>=3+Dr!y>w%4)A-+&}oP}U1 z`Myy_nRwwo1AO8sW}H~T!go|I%-J|Z5a{+gb;w%tF^mO>bKVV&jpo0F8|H5cJla356WCIrLC(ue42TJR3OD+%gh~ExxDH zH!Y%#K~4z*812Q&-0XWR)x|EJZVcn$Y#;B^=?EDSOAliP&**qxSkHGaI23)E5xk>v zPRP*6lo-mCr5N7#S7vR-Yxo_1Ay}9RdKUk6Q+^v#+aOKBuIx1Rvfq3lEMh1Bq|GE9 zm05N+)o6yzl=Ey~1LW6i{%m)daHJc^WSN&KCafz7EzBnb6>ospq56^StQ zAF6chb|W1*%J2}snHlw*qg1%b4_BTRVtA6E(}yFl|IT(3`Tdc~Q?k*$LhXOFavyBJ z1|bMv_b70XKUR4lcXxxm^`JX|47%y*J@YkKl@nv>$16wk&#lfJj21j44|c=sC$AVf z{zT?RXKFF3x=)NYX-g9Pgg47(>L3V+0Wopz0+^Z zeztO`56OIx+tTc;nf+Yl5X?n_&AIJbn|;1=P<=kA?*c)8p>p3AoIcp<4;E*R1!dd{ zPPyM~p4)7Gv2u9y%enC7OO^XYKkVjpzU=zc#Q)CyO6C5l7NafUw{yQ*d4T#D`UW4r zRym4)1ShqAtPd6p2nEXKU?V>4H-_;T$f!Ipf46%A4+d~tg%VHhph2+EJu?`rKKS~| z>36G4_Bt>y)N%(i`7}9|KUh7kDd@+wMg6$0q#xIp_2Y($e%yGFe%y4hemwq=LloSp z)rXqrsk`%MxDBc{Iko06^I`2h%=5Z?n&X1n$6k&4R zy5r1;^^@j#!^6$<#?|I|)0BBWevN)!zk02Co?2&~*Q__sYd7d;wtn44{jz@jCiA@E zc=Nn*+B|Q1gn2%Gvwq&N`ULYlwZ%NId8Bz>d!l(>cM?7`>;{t?)}L%XY}jg^H=bgi yH=SypkKd-BH?Dq^d7gT-d0w;KJg@zC^Stgc=6U^tPcttY9&4UAKCU#0sQqtN=WJF0 delta 184 zcmbQ=ue#%r+=l!7jJum3@V7tUX9Qv3DX=K8y74kIJ1{vYuxK$cDDf%q z#=G(|fH*8l%nH15V2%b8hY~AGtpck8uL9p>1!e`l>HANzC``{h%;L<(Jz0rcfqVO= d!z^#&8Fx&dugp5T-CBheh}pJVtFSZn0s#6BG4lWb diff --git a/test/res/lottie-bee.png b/test/res/lottie-bee.png index 3fdc19a99dd727d13647e9bda65d547daee86775..4586cb50ebc6aa34f4d1acc222322bff7f4ff617 100644 GIT binary patch delta 5363 zcmV`zEO*mn2J;Z0*9fma;5EfUsrE zK%f~2A%p=2#)JvoArlfh>CW^VIztalpXqd9(w**XBw;lNG9d#bKy2AY*w$`&k*o#V z+ANji(pG!bd-qN`#~Bo1%Lh83;-M5;p)^Y~QLK+7+0W*nsY#RKljSuz`$QkPEH1Jf%yIYdPPxQ(lqT$9UUF7;f(U~@@`dC zx8gmA0dTA^1cSPA>$RsYyW=iTUP&3lGK5Z@;)7l7`tkNQ9tq8V?FvpzZ0qamJBbgW zva&MI?RM|gb^UW49UU!Lq@tqYKM~P?0Whv<+SdjL2j9gRn>KB_hB5YeBHAc~n9_B< zH5!eU0La&M{i*)`{-pW?1a?XFz-rjx--ZNBHRh3Od_#GnJ3}7!l=2RGeP;YPVP+n279uG&j)#+gP z05WoO*){h)U~2t8FKq#%&gPvv8vtlnCPh)Uan84I+3f$C({9?p7!?aHd_)uy5F?3{ z=#Rv-w>mmHb^zc=^Lo863L#!>Z*Tucyx&^@Zbq6ALeq7<2?n6TppvoJzTqR?SClti zf)7ezjBUAp<+G}4QY#rz_nsX))&me&n&r|<8op*y*?-P(sG+T!T=um-v)Subkm*(s zjd29$^_q!U4ZFI-_qrTgm`ZqaCY(A1AlBB__K!G=>FMdav$C>|04UaV{bvIM1KltH zfn}Q|meR+Fw zu(kEIea(A5gL7zTX!xwtsyuO3ozr>8)gD`ZmYLx^VoA=Mxg4$Ncqe|Jdc|dp+e`B;4?eyvH|zS%Zi~yN z5SGED5S0~Jm76d1Ad#ZV(P`Z0_GG;_IXM}`3P3QJy1Kfn-CpnG9~g34-t1q;kg1^Aoa|6XG7@S(!t;o%vim6n#e z3-hzz{Nx5l)n9%#$H5pedpk0)bsK9`Pfj+ShVyV{WKdC6HPv79Day#sM)!_4S>L{9?p;$PY)%(Ln$_Wd zBrTahZ2DBy$nfxs0F;f@W$)a2x#yw>Z_05{8a||g95dToZZkJ^M{f3HX8(F(Vj=(o zQ(yp9RaHe5>(?LNa>I?~H+=EW)x5Rq6vi0g1M=qQQ_YoE3A4q5<`-X}cr2>>)|N1& zdGiZt=wSCcGou&RSC;&bE!B=sK6Fcejsq8v*P~KJq1pUSZ~P`jwftyiW+nlHQeXfZ z{Qkawch@}`jn{wLq$mpE0w92VMa8Vl?-z&n>|)0{JNcTLTA`X$C7DcMW@MMy3+{z+hYqPbP(`Lb&Y; zVHpk{i}7zhH6h+U6hY5O5F@hD6}%1ark(QKlzIwf*}nxB{swo94B5&p}fG% zw_Tfw!W;|3IrNRh`QJT1iErKILq(yLAuXEV{Odm)r^`1uoxf@m-vO`%2BhF)&f##3 z5a(Z7fB7ew$!sQkL?~_D#v0Mrx}P0y*@M5iErYV$Dj{vp!7%^37pCxk;7wk9<(3>= zvC+w{sdLfIpKyz0N}yvP4u6@Qa1Mu0#KfOn>_$bQmCZ+W-ZzpEUXO~)HaOwSP-*|s zn8WJuzA-T|5rDxc_?VlXo}SOi$tetuk8h}K+``fb9}%V~l;O+O`}e%T9=IW!A#Gwt z=IgyLKadEF}g;Meooldo~S*fgQM zuxnNn`%KtUswe58n(^ zc6JU;Pft^6S-ChoWM_`0-;L zACb}S7P_R;L^#8w63fk08PeWppXV<%N14-=K}gFlEW;DOHu0XJ7{_@$-!zAKJkACO z2N}|8YHD!befQyiyT6>qx?D4aVEI0u&jEvBFp#(3e*4aW&W`(Ec&sv+C*EGNK^I!ew8(&X`sB`VZyhroY?z`s-Jg`8UzV%P&{+OUpq3!v_?N#Ne`JGb}P4(y>@` zb2Dzc?KYf$)^j2TU#$yglhScE?4gGq!fCN2hcCmV0v}^bI!wmFc=jpKp+r^1OkCa0X$k!QIS77 zF!0Ecx0~*?+ifbTW}fBqkzlGPS0#cd~?i^IxTMS+?3kEO7f5Q0~&G2_KOoDE0>OwQ?;4rz$aahOd6i<#l4ozr?K%2}F%G(}Os2>SF~3cd^# z3wd2C%g<8z$!U$@eFQHrv|%aRuFb?>{%8`5`O9oroo!*kc}Rgn#g-Mcjdk3N42i*2s3 zAvaTHSVCi!lYRcm1b?n+j^PZii}7!KZWTf?ormK(_4M>iz+hCYAgi*RzcQOxc1?+$ zVHwutnbF!8$3oErXU&Jh_{?P<{2mT|)rg0;*%Npp8e%xZ>tg(?pUK8@+<(27)w^wf zTI-HD%T!fddy|7_Whm;7t`Ik=v;zjDVujeWY13`Gpo`|Bf=$k8e3eIKIFHgiGZqs< zpl>vRSdw#_nK7Kfj}Z82K^EnBRE7n{P9<>HKMdooju0OUCVA6=Fn{Ek zNu+dv)7WZ`-yyHS z7-!El%~CYMInJOwfyF3KV6oNN7WPkH%tLE`6yJI}fPKADJa&%{UYE*{M#X^g37s>) z&e=weAHE)p=(_m((b3Tu3`oTaU^S~>N~ECRjFzKO%tv+Zwkr(hap^^NY~MYPg@}mh z9NM!xX6U}_yf}@O3nl`b^8*ZjfPk`5fRziJO@5Y{ee-r7&Vez%z-cD}3BKo0H2L=4 z)+b?53B6)oWTXLw>m>uj!!qbAcWvpBpl=H|L&ismfkSO8OmjxnI1^N z^hF))lbiuy_<*7b&L954L^u>p-3=fCgJLi^hlSmG^Cg~)H900M^m6lmJkmJMW6dfH z+frr6VnoDd!a5$?ehOp3B**V4#Gr!VLl8pn|NgT;WIm$(Yez@N>o70|gR@$c#?pK< zt0}Q!p|MjM+t(Aua@=`!2F`}1aSr{zes3K8BXN!kAdwRMtKT1w92tu3-rd~#4Hz7Q z!6kK6tj)8avBHkUe*DIN4AMBxW5Ze-yJDlA<1FT)+*G{E%>U}C3G6u-*6<-r&T0G$ z|1=Wr9ZLM9X?M$wSOFM}jdWc&+;)XERM=rsz_CF8Sc>gA7{+pZ@rEpzRB)U{Yky3= z?Q#!)Zs!bp_y;HX@MMDH50FR+{=;7d<9GkV@Z9Kh>Y=8$_kS6GD}(}rD_T?V*LPi$ zX;KxUp~<8;HjyGM&^MY8e|E83D2xb8aM~2oxey25jT6q|(D69FeSbc8I}|*=eTq5< zV?uCJdk2oB%T8M=1(T@uU{^}RkcoZ4#d!O zAS}N3YLFC#xZ6e|!>Po>F%dg8uPdRbZnl`w6N>R4L?ZE*4;?x*gB3`H!KsRf%tg5) z!=a$Q%+AX4&3xcwl3{^ocFf|MItO2~%EGV&RU!7*x99MGFFY~Eg&>ASjt4aMKVO*S z-?}Ro4Hb6QP+`YuCjv?B{wGGmJwwsE8DnFqRBH6#!Gn{KpupftiVHTc^_i>J=2-}7 zZaaxx2cv`qgaGxNNQmniU4ju2mf&@(1V9{kH%>TB+pPLMgO(wtI z*52{_cv>KT5QsqfEd>VW&B+s7VC&U&E-TWCd}gt)Cn|y=ov=VKq*FA(#pVh-;XG>A zSw+uqT#N-Z!dVJrYaF~#mlVPH%IZX_VB?${qTJzzE_{2pvk?B$i zOR$(2ZK<-0-*kpWA|(j#N8QI_@o=2q*wxk53F)j9tN<3Pv-#wV@)uXvxhyUlBc%Du zY@(@uJ1jy`PFO(WpuIg&ado{@*e#5(1h-uwf2mdM><){R2Eqa^8%1NW)cvh(9j`(< zCj~3O+O=!Tb%8rtjz;WTH@RU}iByHCw!|vl=m-l<2VsF|LQqR@R9sQx5H>R-EFs&Y zQbUyAvA;J?TN_-$ zq!4AfRH`a8iyd7NAp{5u%tkov?}>^lYMjDmW`repJt|#QW7iKJkMp^3DmWKTUft2r z)dcCh6c}7_aq-U+iA0&M>!y^Zvv7>_o!v32FSUwHmrD6wGgTIt#Gbq%xn2m6M zYB~@Wn=5Q0!>JO^;oz|t|8GAFgr?^7-Ls+O)&mC)9D}qh3JlEe_g_y$e-V$z?UaVJ zl%}(pP*QK|P9U7nwe`hTD$cdA%@sD$+7}g(xF9SLi3@t8BP`bBsG`VcA-soo4MxSc zUp_VeeACSM)SP~M^WN6SCMG7Lkd}>qU|ApOAz z231>ITV^tuTEgM5n}}E%H=E5NRaIl0^M~5o+h0IhQBjdKCnx9DOqcqlcv4hrp5$qdDfII#U%Aj39I_be$8ondn43rN0o4CZFo=?jD2mp%4tp@AqE^;8{)6>?V^b zOhhw6h`()bZ+{j5$2rv1)#WqBE@F&j3L$hMga#nWIX}|X)pZim1w}BZ>gsBz-EMyb zKup*5SGu~o+964jBuSDaNs=Uy${Bwb3_&^<^85X6v)P=%Irk`v;t)dE0N98~B_f4$ zt`HF?q9o@$NkmD`d6I~dobv=@Y%ZBh&USQk%mWaR-iILlR%vOeB`YhdL{-&dBFY8e z1EAu<&;SGgjBA>9A{Y!F8yp-=KmuWf^ZWgIX0!PdilS5qAsGG$h=`{h)pdWpxuvBg zfE7%^3f9=zSY@$TK2Joe01*CoC@qJG>U=)mtY8EQe!oA@WHQw-#>$8&10Ss!&iR0*Y29sYZ4+4G z2-5HI`~4n^#gfZ8&s7v9i*rYwK|~n<>_o({3`CR^LL@lnb3%w&0MnYLO$Z?-IyyS$ zApL&?Y1yi)tIf7F#+aH&Bovd$q%g)rJRVOFQPN~GB|ADgQjn3cFc=Kr{{cxGETnHy Rp?UxS002ovPDHLkV1g$tP<;RZ delta 5337 zcmV;~6ejD2EA%RmGJh>eL_t*T!3lhoXPqzb-~V~v_ne$HdrlGp*+@dtmMn)PK#FCG zO5G}L)h<@O)wXI!$C-|G9Ce&tM{9LPomR)`Ox0Gq(cY*?va7K@Au zbF#b1~<$rWt|E$mF`z_8YEiLU-Rdpj? zaR30vB115#)oa&=*WG-pQdCo`;37;6oiMsy+#&`K>}N#ineqF7uRD0~;8DB*D^{$? zal75khGE>~^ZE8-p0cvC|3yTf0x+WM`se%l`i|nPRjXFLoiTO`5v>$LoH7ifB@_x3 z1IRTDP2=Ogl*Y`2TvcuumD@s?dH5wmz_#fA-ZMf;>=H2B0I9>y<*INVN@|KpCkKiH| z6%{2XyudmCWm{X@4S2;sd3kvT5#f)Bs186g-r`i4P)A39NB5eB1`(K=G@Pkv3SI|K zVUhBIul|d%<%bUztZu;2p540(0K{;S6h*n6bN=+|I`8M5cI^trs6cSxA);9UaUvQQ zy}_`))#vkV1Hh4xo}T`U5aOA(wzlu!)wTk-9tlDS-7t(sm;k|~)-*I6Tyf>wO6sn? zUcno{g)m-!c<2Y>)bXSK=H0uq0dOqLdga>M&ugmk)f5Mt-MGqSFZ5U}>23w7ZUvz* z$Ha_*(P4n)-rnAAFad%| zRaaMUusdDP-|~%b3(mPX*?R~heMczdp9X;-1c%Lk<%XxYSmczhK$6X_Vgb)R@NfF* zp%Wc@o15!#9<{Z#A95xs4_saCbbfe4vMo2w!f*lM80U@cv&Ijfn`ANF`03cGnZG-H z`0y+i34%%0)YN!2i)FjTX0yj55wf{lNYBX?&Xg1|MFEHqoSw$S$Or}f(@-@H8To}q z)jKwSBCD`i#XQk)*m(9E|BOf=a9?X{>%I6>>iTu{Pc6%`Z1~d6neGCQmEkpt#5jNS zh3UvIny3Af)1j;P@85qIivYo7s;jGSv^$;O0pKaFtmao-wVulBSA!{vf(0Buu;1`) zd7k>(eZ*8vta{%jUhJ(^F~_MB1IF{;{~o4)r>2&5cXy8>Vfpgq?xndIFI;tnqx=gW z%5*SB3~#`mo}hm3<70tfm~U!rZQTWvBbbE8dd$s)T0zMqb5b3<=pKrJ`r&jvS9C>$%dRrT?_L!g1BzH)J|+38g2iRJPP& z+1(S_q^Q;(PfbllVNwcAps2XGf9y`I?>aSH*)fg@T0$AY{D_L3XD&F+-|Dzmj zYvm=Y*K*BbRsG{*hJSQyU3Qw%@NakKI8}uRm&m|)lph_AiPF3zSQN4hPpNgs2ZkPp zNhx@X^Lo8s%viEy!;Sy&B^7T1XG$vbzH=k**!Chjuzf3)u4yn*bC!ty)}5Yz`#+v- z^`vSHbDTI8R9j=#Gh6YEzx+1smKbbW(=Tg2i5z9SKM zOYDU6IM5#!?_c9a+0rC76EgVW6H$?#tYY02PIyvO+H)xENOGia866!RgUKj(i#vV# z^yJc|OV{-5X)f}<^BTbvh2c%2s4DXm7U_Mvw<&jBo57GUI;Hb}`soZIStp43!$MN_Jqsp^Qez8196M2~y!Ub&g&F~++5TJ~Ij7*xGoTTL?rQ*P_ z71@q}Sg|Zg+1eh+Io?0~A21mPlW2bFrLyVK(dGv~^C{}<*k|BPGSs=38p^GNvow`h zcB;ydu%&H=|4&nhIbA7)gxsYic;I=BcMpU)F5sy~KO&I`>+9=dNT{r=#2t6sfgk*O z5=Ge-2*Gka9*+ZmCc|JN&CSj0gVWPr*?iyEvHAY{^x%x&z$G?2af-6jG@Q#5+our> z2HBA#M;H?7>+5m*?YFb<|7rpY8J*Jk(a|`=9R2M;Hfwp7>^;{&0-#pX9-M( z!DM_s-?xtT_2nID_jP^u_S@(;Km5KPobemDL}sRDkmgo@G2h^Lln;#S408+)4x*-} z2Hk@(Ha4wuoX6Bm9P_odw&LKygE*smAc_pP0;|=U4UNEwXrxA}u8BUW}4f3R@n5%e+6$^3O3FqTT zVyrCB!h46~m`@17%a>X3%r4G}XapvsVv*4_&FXe2>mK@>3}soCmCXd>dRuPKm7ko{8D2&3(xo;m z7WJFdH^_AYxE=clRhvM#UnMmFE1tMPnJ2#dd~^P?Tdq%i##-3Pm|ag82|eZWZ+_a@;&LS#%Vpp69tk6}R1xofuah#9RfbpaXk5Um5yM#rj)(a&j|FbK!fKe4Ri(KZ64$BdfjPt^;g8)d)TdcX_T3)GR!f2I*N~edk|ZFvwV0W#v3~V{NBgM5jO! z4T;gw(ci&@7#4y2{Jg(b7&&lO)4>of#*z#RTaux2%o7N6_GIHUg`%9}EXomon2&M< z=3A0sW#9XB4qAFc_}XJ*c)2HpZ{FrXx=Uq9paMW?FgWuXoUP>e(eEaLh9T}B8X5}2 zgj6g7Nf!09(Kr;Gwf9g6Ga-Y!?Fz#MTzQ!tPw$+;TtvhS4*iL3Q*_6>({TnX6-Q0%4VmIq-{3?}FGdh0S%6e}P& z&f=d`%d)c7<#x( z1?R#NIEV4K|2cx*6A_L}AQ~6^@Be!wcyJ)Rb7xb_7h!S?CKoeMR+wW&eVHBe{bb7& z5;!j4ib5M(ztYZe4*n2-*9x*M{2w0~#jdV^jyGW3uk$;oKX!a3|e9>I4%nakY{1>bu56z%H^3&A0B(lw!}M3_ei!S^4J zh{v`~2k(D+Ja%X(^7!EJ#CtnC_wT_XqQdmZ5JGSwV(+PQ;l=hK<~li{v&Xhg@sDpz z!-afgLn?N52Kn)Su{gs#kvQ0wA05R*pUcD5RZi?X5=LW3K>X)(6Qn4_-8K>_P9+iu zi}2|gLzxX37OMr_vtj;lFc^7u|Ni|`ScFuVoT`YRKg1m=4h1zOc2<&W;YUu!80L6< z+ce%@?cmF@tPBfK6=GkyIg{V{z%Unr80I-XrnCDtkMpm8eKZ@jWp-9uX2%)*V=?`c z4-5sm2SOiXj19-*@u9A+u5n0EU~XMO$2@!8?=*gv$k`JNg{7#kZ4Liz&*CYPR> zBe=lE4b`rHBqZc}EaK(vkeHY?2y;x#8Wf6hQCDUsTtH<}lIR|ch~WvHa1LF^qvT0Z zc*zng?dcBkpEpe(-MPEv4oI(}z~o#m*U(Hp!-@!&6iC;a16;f-TsV-t{GMS;mpOiaWI z3-WTqQR9l*GKYqQ%oL4I`E_w*I7XO*5}FPKMZ;w_k?K+j3$SX8R+rnwi~9m18W)6D zqt0XDNFc(mZEtVi2kD#?ECSXfXVb|kE*_$OOhea}HUF9nN0VPRpZA@Je7hl2Kv ztK6`tM5;nmRh%TY_yR&VK$s&G6|}b}B-U3tgw4VT3&==TskX!>nhpj0>wTNx)K_LVP zb4&+0?dcAQ^_5OxvoOK}(vwxXy3%fR9glE-AU@#_#5VYR?TwHwNP)=}6cqeC8jY41 zhM~oEg9XBzZ|@9K&GICX>QX5;-9jt!G_mVIP#7G9Ii`c08ao1_uFNJdEr&xQ7!iayf)PPmd;zg6Qx*9hE8!Kqy)Pub`|RnNry8e5PWg?So0?m` zIXXHTf^^XcCRSTp>x)Dpm4;z3yn@?*Y_?ed+uaWJ6c=>UnsS@iwASq^@M!!8zn)}U z_XQcwVWZb+TvO{1ZM~8Bb9-jv0ysP!&>v`RZG8%Wfb^OXOscA?szlSYy@5c$O++k# zTP&7YRaL{B^Sj&H+BPF0KR-VyGc)spsV?=ik(gMKY**sx$qJ24>5iF@!BmB21S1H8 z3&Y^VZ5s7Q;>PAcDE3HaXXh}aH-unPUa$Av0G`lw-L7d`fQY7q5dXWat?dZ_9OqG8 zU7gDqyNofGDughE5ITSm=lo!Md;3X9mlVOIDk>_RcDwyv0Aa&0e%0RI-UgAuB7eGM z2-5kG*Xwm#ES40`d9tD?4k3gMfQ^V$B2qZ#3K4N4igC_kL=@wk$A~D#Igc{N{IOVU z+UN7l01%K~g&_UG^5x5|X=!Q2s;U+cQ8oY%02P;p4qyzxh_37X6B851`uh5!kU&`E zyk2jP#bRkt6s1fE!SH85L_Fb;VSgA+d-v`g!y=|&5v#ASFSlB)w-8Yl0K%USC1es& zwa4QbKY8-x1QscVMT{|4B!po23n3zAwOWg?h%qc;u~@7Tz)}2#aL$hm3=HhRB1Vwl z^?GwOO{-*#l@L)1-dt0h^CP;hceb{+j$)A`NPoiX^(I@b)@;sswxTF$oJR8$B1!>Z zCnAQ6KtwSiM3i&x7eY(}nACNBR0uKZ^ZERc{y&0r(JCq`EVcy3m>P{n6;0C=#+Zmi rB2glWX_^-E`FwFmkB7+00000NkvXXu0mjfPl8N4 From 7e9005216d316c0f3bb9b8867d75185bfade61ee Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 3 Aug 2024 22:46:09 +1200 Subject: [PATCH 043/346] Switch to new application service library --- matrix/appservice.js | 14 +- package-lock.json | 802 ++++++++----------------------------------- package.json | 2 +- passthrough.js | 2 +- scripts/register.js | 22 -- start.js | 2 +- 6 files changed, 160 insertions(+), 684 deletions(-) delete mode 100644 scripts/register.js diff --git a/matrix/appservice.js b/matrix/appservice.js index e99a822..b239084 100644 --- a/matrix/appservice.js +++ b/matrix/appservice.js @@ -1,8 +1,8 @@ -const reg = require("../matrix/read-registration") -const AppService = require("matrix-appservice").AppService -const as = new AppService({ - homeserverToken: reg.hs_token -}) -as.listen(+(new URL(reg.url).port)) +// @ts-check -module.exports = as +const reg = require("../matrix/read-registration") +const {AppService} = require("@cloudrac3r/in-your-element") +const as = new AppService(reg) +as.listen() + +module.exports.as = as diff --git a/package-lock.json b/package-lock.json index 9b97e23..67df9fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@cloudrac3r/discord-markdown": "^2.6.2", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", + "@cloudrac3r/in-your-element": "^1.0.0", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", @@ -24,7 +25,6 @@ "get-stream": "^6.0.1", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", - "matrix-appservice": "^2.0.0", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", @@ -45,6 +45,29 @@ "supertape": "^10.4.0" } }, + "../in-your-element": { + "name": "@cloudrac3r/in-your-element", + "version": "0.0.0", + "extraneous": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "h3": "^1.12.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.2", + "@types/node": "^18.19.42", + "c8": "^10.1.2", + "cross-env": "^7.0.3", + "mock-req": "^0.2.0", + "readable-mock-req": "^0.2.2", + "supertape": "^10.7.2", + "try-to-catch": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -169,6 +192,19 @@ "html-es6cape": "^2.0.0" } }, + "node_modules/@cloudrac3r/in-your-element": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/in-your-element/-/in-your-element-1.0.0.tgz", + "integrity": "sha512-g6vdxNJtc9+Y0djClrc0xNwL6DFiEQ9ikWHBBDJ3iKAWygdjANnFJ/Q1DVMmNqUYRsQIN3yH1aIQICKA2CDXhQ==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "h3": "^1.12.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@cloudrac3r/mixin-deep": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@cloudrac3r/mixin-deep/-/mixin-deep-3.0.0.tgz", @@ -973,18 +1009,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -1015,11 +1039,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, "node_modules/as-table": { "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", @@ -1063,22 +1082,6 @@ } ] }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/better-sqlite3": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.1.2.tgz", @@ -1143,29 +1146,6 @@ "node": ">= 6" } }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1174,14 +1154,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/c8": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.2.tgz", @@ -1215,24 +1187,6 @@ } } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1392,23 +1346,13 @@ "node": ">= 0.8" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "license": "MIT", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/convert-source-map": { @@ -1417,18 +1361,11 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" }, "node_modules/cross-env": { "version": "7.0.3", @@ -1461,6 +1398,20 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.2.4.tgz", + "integrity": "sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==", + "license": "MIT", + "peerDependencies": { + "uWebSockets.js": "*" + }, + "peerDependenciesMeta": { + "uWebSockets.js": { + "optional": true + } + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1471,14 +1422,6 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1501,21 +1444,11 @@ "node": ">=4.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -1526,22 +1459,11 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" }, "node_modules/detect-libc": { "version": "2.0.3", @@ -1574,24 +1496,11 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1611,25 +1520,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1639,19 +1529,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1660,47 +1537,6 @@ "node": ">=6" } }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1730,23 +1566,6 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1797,22 +1616,6 @@ "node": ">= 6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1843,24 +1646,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", @@ -1908,15 +1693,22 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/h3": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.12.0.tgz", + "integrity": "sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "cookie-es": "^1.1.0", + "crossws": "^0.2.4", + "defu": "^6.1.4", + "destr": "^2.0.3", + "iron-webcrypto": "^1.1.1", + "ohash": "^1.1.3", + "radix3": "^1.1.2", + "ufo": "^1.5.3", + "uncrypto": "^0.1.3", + "unenv": "^1.9.0" } }, "node_modules/has-flag": { @@ -1927,39 +1719,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -1990,32 +1749,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2045,12 +1778,13 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" } }, "node_modules/is-arrayish": { @@ -2203,50 +1937,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/matrix-appservice": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-2.0.0.tgz", - "integrity": "sha512-HCIuJ5i0YuO8b0dMyGe5dqlsE4f3RzHU0MuMg/2gGAZ4HL3r7aSWOFbyIWStSSUrk1qCa9Eml0i4EnEi0pOtdA==", - "dependencies": { - "body-parser": "^1.19.0", - "express": "^4.18.1", - "js-yaml": "^4.1.0", - "morgan": "^1.10.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" + "node": ">=10.0.0" } }, "node_modules/mime-db": { @@ -2314,50 +2014,11 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-abi": { "version": "3.40.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", @@ -2388,32 +2049,17 @@ } } }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "license": "MIT" }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "license": "MIT" }, "node_modules/once": { "version": "1.4.0", @@ -2458,14 +2104,6 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2511,10 +2149,11 @@ "node": "20 || >=22" } }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" }, "node_modules/peek-readable": { "version": "4.1.0", @@ -2587,18 +2226,6 @@ "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2608,41 +2235,11 @@ "once": "^1.3.1" } }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" }, "node_modules/rc": { "version": "1.2.8", @@ -2749,11 +2346,6 @@ } ] }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2765,69 +2357,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", - "dependencies": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, "node_modules/sharp": { "version": "0.33.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", @@ -2886,19 +2415,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3007,14 +2523,6 @@ "get-source": "^2.0.12" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stream-head": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-head/-/stream-head-2.0.2.tgz", @@ -3380,14 +2888,6 @@ "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.7.tgz", "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==" }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/token-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", @@ -3442,17 +2942,17 @@ "node": "*" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" }, "node_modules/undici": { "version": "6.19.2", @@ -3468,12 +2968,17 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" + "node_modules/unenv": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3", + "defu": "^6.1.4", + "mime": "^3.0.0", + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" } }, "node_modules/util-deprecate": { @@ -3481,14 +2986,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -3503,14 +3000,6 @@ "node": ">=10.12.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3674,6 +3163,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 10486ac..31990ed 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@cloudrac3r/discord-markdown": "^2.6.2", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", + "@cloudrac3r/in-your-element": "^1.0.0", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", @@ -30,7 +31,6 @@ "get-stream": "^6.0.1", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", - "matrix-appservice": "^2.0.0", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", diff --git a/passthrough.js b/passthrough.js index ec80004..18cca6b 100644 --- a/passthrough.js +++ b/passthrough.js @@ -7,7 +7,7 @@ * @property {import("./d2m/discord-client")} discord * @property {import("heatsync").default} sync * @property {import("better-sqlite3/lib/database")} db - * @property {import("matrix-appservice").AppService} as + * @property {import("@cloudrac3r/in-your-element").AppService} as * @property {import("./db/orm").from} from * @property {import("./db/orm").select} select */ diff --git a/scripts/register.js b/scripts/register.js deleted file mode 100644 index e19adb3..0000000 --- a/scripts/register.js +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-check - -const { AppServiceRegistration } = require("matrix-appservice"); - -let id = AppServiceRegistration.generateToken() -try { - const reg = require("../matrix/read-registration") - if (reg.id) id = reg.id -} catch (e) {} - -// creating registration files -const newReg = new AppServiceRegistration(null); -newReg.setAppServiceUrl("http://localhost:6693"); -newReg.setId(id); -newReg.setHomeserverToken(AppServiceRegistration.generateToken()); -newReg.setAppServiceToken(AppServiceRegistration.generateToken()); -newReg.setSenderLocalpart("_ooye_bot"); -newReg.addRegexPattern("users", "@_ooye_.*", true); -newReg.addRegexPattern("aliases", "#_ooye_.*", true); -newReg.setProtocols(["discord"]); // For 3PID lookups -newReg.setRateLimited(false); -newReg.outputAsYaml("registration.yaml"); diff --git a/start.js b/start.js index 819281f..2b6e3df 100644 --- a/start.js +++ b/start.js @@ -17,7 +17,7 @@ const DiscordClient = require("./d2m/discord-client") const discord = new DiscordClient(config.discordToken, "full") passthrough.discord = discord -const as = require("./matrix/appservice") +const {as} = require("./matrix/appservice") passthrough.as = as const orm = sync.require("./db/orm") From 5e4d5398071f954c7d6815023ec38f18a15437f9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 15 Aug 2024 23:51:34 +1200 Subject: [PATCH 044/346] Fix potential abuse of error retry system --- m2d/event-dispatcher.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index f7e5e1b..5529a26 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -62,15 +62,34 @@ function guard(type, fn) { } } -async function retry(roomID, eventID) { - const event = await api.getEvent(roomID, eventID) +/** + * @param {Ty.Event.Outer} reactionEvent + */ +async function onRetryReactionAdd(reactionEvent) { + const roomID = reactionEvent.room_id + const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id) + + // Check that it's a real error from OOYE const error = event.content["moe.cadence.ooye.error"] if (event.sender !== `@${reg.sender_localpart}:${reg.ooye.server_name}` || !error) return + + // To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator + if (reactionEvent.sender !== event.sender) { + // Check if it's a room moderator + const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "") + const powerLevel = powerLevelsStateContent.users?.[reactionEvent.sender] || 0 + if (powerLevel < 50) return + } + + // Retry if (error.source === "matrix") { - as.emit("type:" + error.payload.type, error.payload) + as.emit(`type:${error.payload.type}`, error.payload) } else if (error.source === "discord") { discord.cloud.emit("event", error.payload) } + + // Redact the error to stop people from executing multiple retries + api.redactEvent(roomID, event.event_id) } sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message", @@ -103,7 +122,7 @@ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return if (event.content["m.relates_to"].key === "🔁") { // Try to bridge a failed event again? - await retry(event.room_id, event.content["m.relates_to"].event_id) + await onRetryReactionAdd(event) } else { matrixCommandHandler.onReactionAdd(event) await addReaction.addReaction(event) From 283cf669065887b0a224361790fcc47bdc66cf7f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 16 Aug 2024 01:08:01 +1200 Subject: [PATCH 045/346] Move reply previews to one line, fix android small text --- m2d/converters/event-to-message.js | 8 +-- m2d/converters/event-to-message.test.js | 72 ++++++++++++------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 86975ab..4047980 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -541,7 +541,7 @@ async function eventToMessage(event, guild, di) { .replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) .replace(/<[^>]+>/g, "") // Completely strip all HTML tags and formatting. ), 50) - replyLine = "> -# " + contentPreviewChunks[0] + replyLine = "-# > " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) replyLine = replyLine.replace(/[,.']$/, "") + "..." replyLine += "\n" return @@ -565,7 +565,7 @@ async function eventToMessage(event, guild, di) { assert(match) senderName = match[1] } - replyLine += `Ⓜ️**${senderName}**` + replyLine += `**Ⓜ${senderName}**` } // If the event has been edited, the homeserver will include the relation in `unsigned`. if (repliedToEvent.unsigned?.["m.relations"]?.["m.replace"]?.content?.["m.new_content"]) { @@ -595,14 +595,14 @@ async function eventToMessage(event, guild, di) { repliedToContent = entities.decodeHTML5Strict(repliedToContent) // Remove entities like & " const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { - contentPreview = ":\n> -# " + contentPreviewChunks[0] + contentPreview = ": " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." } else { console.log("Unable to generate reply preview for this replied-to event because we stripped all of it:", repliedToEvent) contentPreview = "" } } - replyLine = `> -# ${replyLine}${contentPreview}\n` + replyLine = `-# > ${replyLine}${contentPreview}\n` })() if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 3a187c3..a88cff7 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1038,8 +1038,8 @@ test("event2message: rich reply to a sim user", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> -# Slow news day." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + "\nTesting this reply, ignore", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1110,8 +1110,8 @@ test("event2message: rich reply to a rich reply to a multi-line message should c messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# I just checked in a fix that will probably work..." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " I just checked in a fix that will probably work..." + "\nwill try later (tomorrow if I don't forgor)", avatar_url: undefined, allowed_mentions: { @@ -1190,8 +1190,8 @@ test("event2message: rich reply to an already-edited message will quote the new messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" - + "\n> -# this is the new content. heya!" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" + + " this is the new content. heya!" + "\nhiiiii....", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1244,7 +1244,7 @@ test("event2message: rich reply to a missing event will quote from formatted_bod messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# But who sees the seashells she sells sitting..." + content: "-# > But who sees the seashells she sells sitting..." + "\nWhat a tongue-bender...", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1395,8 +1395,8 @@ test("event2message: should avoid using blockquote contents as reply preview in messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> -# that can't be true! there's no way :o" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " that can't be true! there's no way :o" + "\nI agree!", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1484,8 +1484,8 @@ test("event2message: should include a reply preview when message ends with a blo messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**_ooye_cookie**:" - + "\n> -# https://tootsuite.net/Warp-Gate2.gif tanget: @..." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜ_ooye_cookie**:" + + " https://tootsuite.net/Warp-Gate2.gif tanget: @..." + "\naichmophobia", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1568,8 +1568,8 @@ test("event2message: should include a reply preview when replying to a descripti messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" - + "\n> -# It looks like this queue has ended." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" + + " It looks like this queue has ended." + `\nso you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?`, avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1621,8 +1621,8 @@ test("event2message: entities are not escaped in main message or reply preview", messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# Testing? \"':.`[]&things" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " Testing? \"':.`[]&things" + "\n_Testing?_ \"':.\\`\\[\\]&things", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -1671,8 +1671,8 @@ test("event2message: reply preview converts emoji formatting when replying to a messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# <:hippo:230201364309868544>" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " <:hippo:230201364309868544>" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1721,8 +1721,8 @@ test("event2message: reply preview can guess custom emoji based on the name if i messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# <:hippo:230201364309868544>" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " <:hippo:230201364309868544>" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1771,8 +1771,8 @@ test("event2message: reply preview uses emoji title text when replying to an unk messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# :svkftngur_gkdne:" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " :svkftngur_gkdne:" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1821,8 +1821,8 @@ test("event2message: reply preview ignores garbage image", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" - + "\n> -# I am having a nice day" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + + " I am having a nice day" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1871,7 +1871,7 @@ test("event2message: reply to empty message doesn't show an extra line or anythi messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**" + "\nreply", avatar_url: undefined, allowed_mentions: { @@ -1953,8 +1953,8 @@ test("event2message: editing a rich reply to a sim user", async t => { id: "1144874214311067708", message: { username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> -# Slow news day." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + "\nEditing this reply, which is also a test", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2244,8 +2244,8 @@ test("event2message: rich reply to a matrix user's long message with formatting" messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" - + "\n> -# i should have a little happy test list bold em..." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 **Ⓜcadence [they]**:" + + " i should have a little happy test list bold em..." + "\n**no you can't!!!**", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2307,7 +2307,7 @@ test("event2message: rich reply to an image", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + "\nCaught in 8K UHD VR QLED Epic Edition", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2362,8 +2362,8 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" - + "\n> -# [spoiler] cw crossword spoilers you'll never..." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " [spoiler] cw crossword spoilers you'll never..." + "\nomg NO WAY!!", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2417,8 +2417,8 @@ test("event2message: with layered rich replies, the preview should only be the r messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" - + "\n> -# two" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 **Ⓜcadence [they]**:" + + " two" + "\nthree", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { @@ -2473,8 +2473,8 @@ test("event2message: if event is a reply and starts with a quote, they should be messagesToEdit: [], messagesToSend: [{ username: "Rose", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Rose**:" - + "\n> -# i have a feeling that clients are meant to strip..." + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**ⓂRose**:" + + " i have a feeling that clients are meant to strip..." + "\n" + "\n> To strip the fallback on the `body`, the client should iterate over each line of the string, removing any lines that start with the fallback prefix (\"> “, including the space, without quotes) and stopping when a line is encountered without the prefix. This prefix is known as the “fallback prefix sequence”.", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", @@ -2533,7 +2533,7 @@ test("event2message: rich reply to a deleted event", async t => { messagesToEdit: [], messagesToSend: [{ username: "Ampflower 🌺", - content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**Ampflower 🌺** (in reply to a deleted message)" + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**ⓂAmpflower 🌺** (in reply to a deleted message)" + "\nHuh it did the same thing here too", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", allowed_mentions: { From 5c486453342e599a19887fbc65625ddd5a9191e4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 15 Aug 2024 15:23:32 +1200 Subject: [PATCH 046/346] m->d: Suppress embeds for links in reply preview --- m2d/converters/event-to-message.js | 1 + m2d/converters/event-to-message.test.js | 51 ++++++++- package-lock.json | 138 ++++++++++++++++++++---- package.json | 2 +- test/ooye-test-data.sql | 7 +- 5 files changed, 173 insertions(+), 26 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 4047980..2a32dde 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -592,6 +592,7 @@ async function eventToMessage(event, guild, di) { return convertEmoji(mxcUrlMatch?.[1], titleTextMatch?.[1], false, false) }) repliedToContent = repliedToContent.replace(/<[^:>][^>]*>/g, "") // Completely strip all HTML tags and formatting. + repliedToContent = repliedToContent.replace(/\bhttps?:\/\/[^ )]*/g, "<$&>") repliedToContent = entities.decodeHTML5Strict(repliedToContent) // Remove entities like & " const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index a88cff7..f997b44 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1407,6 +1407,55 @@ test("event2message: should avoid using blockquote contents as reply preview in ) }) +test("event2message: should suppress embeds for links in reply preview", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@rnl:cadence.moe", + content: { + msgtype: "m.text", + body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + format: "org.matrix.custom.html", + formatted_body: `

In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU" + } + } + }, + event_id: "$0Bs3rbsXaeZmSztGMx1NIsqvOrkXOpIWebN-dqr09i4", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU", { + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "m.mentions": {}, + "msgtype": "m.text", + "body": "https://www.youtube.com/watch?v=uX32idb1jMw" + } + }) + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "RNL", + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:" + + " " + + `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: should include a reply preview when message ends with a blockquote", async t => { t.deepEqual( await eventToMessage({ @@ -1485,7 +1534,7 @@ test("event2message: should include a reply preview when message ends with a blo messagesToSend: [{ username: "cadence [they]", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜ_ooye_cookie**:" - + " https://tootsuite.net/Warp-Gate2.gif tanget: @..." + + " tanget: @..." + "\naichmophobia", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { diff --git a/package-lock.json b/package-lock.json index 67df9fe..67beae8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", - "@cloudrac3r/discord-markdown": "^2.6.2", + "@cloudrac3r/discord-markdown": "^2.6.3", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.0.0", @@ -83,6 +83,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.0.6", @@ -96,6 +97,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -104,6 +106,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -115,6 +118,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -123,6 +127,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", @@ -137,6 +142,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -145,6 +151,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -157,12 +164,14 @@ "node_modules/@cloudcmd/stub/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "node_modules/@cloudcmd/stub/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -171,12 +180,11 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.2.tgz", - "integrity": "sha512-nHk7HPRK6smrYP+BbEHdLEsv/utXdsylLmdqVgiB4U+PgWnILR7RlcKnKjS5P+K8guIzh+3HvTzWkF0PchO54g==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.3.tgz", + "integrity": "sha512-9pELy0wk0SiAfdj8QQDUpxZkzFum1c3/ybCG7DKs9EQQMPgP0AF7tNGEIfX76eJVisdGKftzuNd0xIfeGFKJxg==", "dependencies": { - "simple-markdown": "^0.7.3", - "supertape": "^10.7.2" + "simple-markdown": "^0.7.3" } }, "node_modules/@cloudrac3r/giframe": { @@ -702,6 +710,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -718,6 +727,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -728,12 +738,14 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -750,6 +762,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -775,6 +788,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -811,6 +825,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "optional": true, "engines": { "node": ">=14" @@ -820,6 +835,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-2.0.0.tgz", "integrity": "sha512-EXJv2HaXM+5scjoxE6Tf+o4+pxwL1tYJZJBDMygrF7cocjirGcU05GgNr9WHOaUPaVOpVjVU98ugYD7XJLmMkw==", + "dev": true, "dependencies": { "ci-info": "^4.0.0", "fullstore": "^3.0.0" @@ -832,6 +848,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-2.0.0.tgz", "integrity": "sha512-/tl1XiBog6XMb1T9kalFerYU86sYsl6EtrlvGI5RVtlHOQdEEJAIPRxmX4vnKG3uoY5aVEkJOWzbPM5tsncmFQ==", + "dev": true, "dependencies": { "fastest-levenshtein": "^1.0.12", "just-kebab-case": "^4.2.0" @@ -843,12 +860,14 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "node_modules/@supertape/engine-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-2.0.0.tgz", "integrity": "sha512-1G2MmfZnSxx546omLPAVNgvG/iqOQZGiXHnjJ2JXKvuf2lpPdDRnNm5eLl81lvEG473zE9neX979TzeFcr3Dxw==", + "dev": true, "dependencies": { "try-catch": "^3.0.0" }, @@ -860,6 +879,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", + "dev": true, "dependencies": { "@supertape/formatter-tap": "^3.0.3", "fullstore": "^3.0.0" @@ -872,6 +892,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", + "dev": true, "dependencies": { "fullstore": "^3.0.0" }, @@ -883,6 +904,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-6.1.0.tgz", "integrity": "sha512-BVnLW08BMbF/Xf9DNxTtc5V5Ong4VCj0w46Ts2cc1EboX+RQGuxGO0/wrzTBTt4t30iUzFhG/t2g280MfLHutQ==", + "dev": true, "dependencies": { "chalk": "^5.3.0", "ci-info": "^4.0.0", @@ -898,6 +920,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -909,6 +932,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==", + "dev": true, "engines": { "node": ">=16" } @@ -917,6 +941,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==", + "dev": true, "engines": { "node": ">=16" } @@ -925,6 +950,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-time/-/formatter-time-1.0.2.tgz", "integrity": "sha512-QihQWA/3LSNuODHrL8MGNHkdRunaEqNQkuMUDGNgEQO8MYBB0d83WGlNxDFGjn4kRlq47hovw3Skq7Btb2i2JA==", + "dev": true, "dependencies": { "chalk": "^5.3.0", "ci-info": "^4.0.0", @@ -941,6 +967,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -952,6 +979,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.1.0.tgz", "integrity": "sha512-jzC56u1k+3DLRo854+J6v/DP/4SjRV2mAqfR6qzsyaAocC9OFe7NHYQQMmlJ4cUJwgFjUh7AVnjFfC0Z0XuH+g==", + "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0" }, @@ -1013,6 +1041,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -1024,6 +1053,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1043,6 +1073,7 @@ "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, "dependencies": { "printable-characters": "^1.0.42" } @@ -1061,7 +1092,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -1150,6 +1182,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -1191,6 +1224,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1223,6 +1257,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, "funding": [ { "type": "github", @@ -1237,6 +1272,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, "dependencies": { "string-width": "^4.2.3" }, @@ -1389,6 +1425,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1420,7 +1457,8 @@ "node_modules/data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true }, "node_modules/decompress-response": { "version": "6.0.0", @@ -1477,6 +1515,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -1494,12 +1533,14 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -1541,6 +1582,7 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, "engines": { "node": ">= 4.9.1" } @@ -1585,12 +1627,14 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1625,6 +1669,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==", + "dev": true, "engines": { "node": ">=4" } @@ -1633,6 +1678,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1650,6 +1696,7 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -1675,6 +1722,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", @@ -1715,6 +1763,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -1723,6 +1772,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -1796,6 +1846,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -1807,6 +1858,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -1814,7 +1866,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -1856,6 +1909,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -1873,6 +1927,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -1887,6 +1942,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -1905,7 +1961,8 @@ "node_modules/just-kebab-case": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", - "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==" + "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==", + "dev": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -1983,6 +2040,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2005,6 +2063,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -2102,7 +2161,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true }, "node_modules/path-exists": { "version": "4.0.0", @@ -2117,6 +2177,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -2124,12 +2185,14 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -2145,6 +2208,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, "engines": { "node": "20 || >=22" } @@ -2201,6 +2265,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -2214,6 +2279,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -2224,7 +2290,8 @@ "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true }, "node_modules/pump": { "version": "3.0.0", @@ -2264,7 +2331,8 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true }, "node_modules/readable-web-to-node-stream": { "version": "3.0.2", @@ -2307,6 +2375,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -2400,6 +2469,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2411,6 +2481,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -2419,6 +2490,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -2501,6 +2573,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2518,6 +2591,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -2559,6 +2633,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2573,6 +2648,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2586,6 +2662,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2594,6 +2671,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2605,6 +2683,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2613,6 +2692,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2624,6 +2704,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2639,6 +2720,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2650,6 +2732,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2682,6 +2765,7 @@ "version": "10.7.2", "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.7.2.tgz", "integrity": "sha512-pBWvzBtefVgokjJRk2ks+6q8/Sy1fIIvMBL8nXYaXk3100cgcPsw4iYhFSW41tg/cEe4IOlV/G+FZFN4kSMtOg==", + "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^2.0.0", @@ -2719,6 +2803,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2730,6 +2815,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -2886,7 +2972,8 @@ "node_modules/timer-node": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.7.tgz", - "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==" + "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==", + "dev": true }, "node_modules/token-types": { "version": "4.2.1", @@ -2913,6 +3000,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==", + "dev": true, "engines": { "node": ">=6" } @@ -3018,6 +3106,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3050,6 +3139,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3066,6 +3156,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3074,6 +3165,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3110,7 +3202,8 @@ "node_modules/wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", - "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" + "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", + "dev": true }, "node_modules/xxhash-wasm": { "version": "1.0.2", @@ -3148,6 +3241,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index 31990ed..68ac545 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", - "@cloudrac3r/discord-markdown": "^2.6.2", + "@cloudrac3r/discord-markdown": "^2.6.3", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.0.0", diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 35368f4..32777a2 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -54,7 +54,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1197612733600895076', '112760669178241024'), ('1202543413652881428', '1160894080998461480'), ('1207486471489986620', '1160894080998461480'), -('1210387798297682020', '112760669178241024'); +('1210387798297682020', '112760669178241024'), +('1273204543739396116', '687028734322147344'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -89,7 +90,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$7tJoMw1h44n2gxgLUE1T_YinGrLbK0x-TDY1z6M7GBw', 'm.room.message', 'm.text', '1197612733600895076', 0, 0, 1), ('$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU', 'm.room.message', 'm.text', '1202543413652881428', 0, 0, 0), ('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0), -('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1); +('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1), +('$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU', 'm.room.message', 'm.text', '1273204543739396116', 0, 0, 0); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), @@ -125,6 +127,7 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES ('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), ('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL), ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), From 0cf1d26764dfd4d31b9478a5fee8bb35072d0a77 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 15 Aug 2024 15:26:36 +1200 Subject: [PATCH 047/346] Update dependency justification --- readme.md | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/readme.md b/readme.md index b64f780..4e7b344 100644 --- a/readme.md +++ b/readme.md @@ -74,19 +74,18 @@ You'll need: Follow these steps: -1. [Get Node.js version 18 or later](https://nodejs.org/en/download/releases) (the version is required by the better-sqlite3 and matrix-appservice dependencies) +1. [Get Node.js version 18 or later](https://nodejs.org/en/download/prebuilt-installer) 1. Clone this repo and checkout a specific tag. (Development happens on main. Stable versions are tagged.) * The latest release tag is ![](https://img.shields.io/gitea/v/release/cadence/out-of-your-element?gitea_url=https%3A%2F%2Fgitdab.com&style=flat-square&label=%20&color=black). -1. Install dependencies: `npm install --save-dev` (omit --save-dev if you will not run the automated tests) +1. Install dependencies: `npm install` 1. Copy `config.example.js` to `config.js` and fill in Discord token. 1. Copy `registration.example.yaml` to `registration.yaml` and fill in bracketed values. You could generate each hex string with `dd if=/dev/urandom bs=32 count=1 2> /dev/null | basenc --base16 | dd conv=lcase 2> /dev/null`. Register the registration in Synapse's `homeserver.yaml` through the usual appservice installation process, then restart Synapse. 1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. You only need to run this once ever. -1. Make sure the tests work by running `npm t` 1. Start the bridge: `node start.js` @@ -103,7 +102,7 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in # Development setup -* Be sure to install dependencies with `--save-dev` so you can run the tests. +* Install development dependencies with `npm install --save-dev` so you can run the tests. * Any files you change will automatically be reloaded, except for `stdin.js` and `d2m/discord-*.js`. * If developing on a different computer to the one running the homeserver, use SSH port forwarding so that Synapse can connect on its `localhost:6693` to reach the running bridge on your computer. Example: `ssh -T -v -R 6693:localhost:6693 me@matrix.cadence.moe` * I recommend developing in Visual Studio Code so that the JSDoc x TypeScript annotation comments work. I don't know which other editors or language servers support annotations and type inference. @@ -163,23 +162,28 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in (deduped transitive dependency count) dependency name: explanation * (0) @chriscdn/promise-semaphore: It does what I want! I like it! +* (1) @cloudrac3r/discord-markdown: This is my fork! +* (0) @cloudrac3r/giframe: This is my fork! +* (1) @cloudrac3r/html-template-tag: This is my fork! +* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. +* (0) @cloudrac3r/mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.) +* (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. +* (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. * (42) better-sqlite3: SQLite3 is the best database, and this is the best library for it. Really! I love it. * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. -* (8) snowtransfer: Discord API library with bring-your-own-caching that I trust. -* (1) discord-markdown: This is my fork! -* (0) get-stream: Only needed if content_length_workaround is true. -* (0) giframe: This is my fork! -* (1) heatsync: Module hot-reloader that I trust. +* (0) domino: DOM implementation that's already pulled in by turndown. * (0) entities: Looks fine. No dependencies. -* (1) html-template-tag: This is my fork! -* (1) js-yaml: It seems to do what I want, and it's already pulled in by matrix-appservice. -* (70) matrix-appservice: I wish it didn't pull in express :( +* (0) get-stream: Only needed if content_length_workaround is true. +* (1) heatsync: Module hot-reloader that I trust. +* (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON. * (0) minimist: It's already pulled in by better-sqlite3->prebuild-install. -* (0) mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.) -* (3) node-fetch@2: I like it and it does what I want. -* (0) pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. +* (3) node-fetch@2: I like it and it does what I want. Version 2 is used because version 3 is ESM-only. * (0) prettier-bytes: It does what I want and has no dependencies. -* (51) sharp: Jimp has fewer dependencies, but sharp is faster. -* (0) try-to-catch: Not strictly necessary, but it does what I want and has no dependencies. -* (1) turndown: I need an HTML-to-Markdown converter and this one looked suitable enough. It has some bugs that I've worked around, so I might switch away from it later. +* (51) sharp: Image compositing and processing. Jimp has fewer dependencies, but sharp is faster. +* (8) snowtransfer: Discord API library with bring-your-own-caching that I trust. +* (10) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. +* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. +* (0) xxhash-wasm: Used where cryptographically secure hashing is not required. + +Total transitive production dependencies: 113 From 2c93ff1e6ddd52feab70ab6f7c3b4b8ed25aac3e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 16 Aug 2024 12:04:09 +1200 Subject: [PATCH 048/346] Match old and new formats of reply rep --- d2m/converters/message-to-event.js | 8 +- d2m/converters/message-to-event.test.js | 78 +++++++++++ test/data.js | 176 ++++++++++++++++++++++++ test/ooye-test-data.sql | 9 +- 4 files changed, 265 insertions(+), 6 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 45c4032..1e77d9d 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -58,7 +58,7 @@ function getDiscordParseCallbacks(message, guild, useHTML) { emoji: node => { if (useHTML) { const mxc = select("emoji", "mxc_url", {emoji_id: node.id}).pluck().get() - assert(mxc) // All emojis should have been added ahead of time in the messageToEvent function. + assert(mxc, `Emoji consistency assertion failed for ${node.name}:${node.id}`) // All emojis should have been added ahead of time in the messageToEvent function. return `:${node.name}:` } else { return `:${node.name}:` @@ -430,10 +430,12 @@ async function messageToEvent(message, guild, options = {}, di) { repliedToUserHtml = repliedToDisplayName } let repliedToContent = message.referenced_message?.content - if (repliedToContent?.startsWith("> <:L1:")) { + if (repliedToContent?.match(/^(-# )?> (-# )?<:L1:/)) { // If the Discord user is replying to a Matrix user's reply, the fallback is going to contain the emojis and stuff from the bridged rep of the Matrix user's reply quote. // Need to remove that previous reply rep from this fallback body. The fallbody body should only contain the Matrix user's actual message. - repliedToContent = repliedToContent.split("\n").slice(2).join("\n") + // ┌──────A─────┐ A reply rep starting with >quote or -#smalltext >quote. Match until the end of the line. + // ┆ ┆┌─B─┐ There may be up to 2 reply rep lines in a row if it was created in the old format. Match all lines. + repliedToContent = repliedToContent.replace(/^((-# )?> .*\n){1,2}/, "") } if (repliedToContent == "") repliedToContent = "[Media]" else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index edcb44c..fbfe06d 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -559,6 +559,84 @@ test("message2event: simple reply in thread to a matrix user's reply", async t = }]) }) +test("message2event: infinidoge's reply to ami's matrix smalltext reply to infinidoge", async t => { + const events = await messageToEvent(data.message.infinidoge_reply_to_ami_matrix_smalltext_reply_to_infinidoge, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4", { + type: "m.room.message", + sender: "@ami:the-apothecary.club", + content: { + msgtype: "m.text", + body: `> <@_ooye_infinidoge:cadence.moe> Neat that they thought of that\n\nlet me guess they got a lot of bug reports like "empty chest with no loot?"`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to @_ooye_infinidoge:cadence.moe
Neat that they thought of that
let me guess they got a lot of bug reports like "empty chest with no loot?"`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$baby" + } + } + }, + event_id: "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4", + room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4" + } + }, + "m.mentions": { + user_ids: ["@ami:the-apothecary.club"] + }, + msgtype: "m.text", + body: `> Ami (she/her): let me guess they got a lot of bug reports like "empty chest with no loot?"\n\nMost likely`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
Most likely`, + }]) +}) + +test("message2event: infinidoge's reply to ami's matrix smalltext singleline reply to infinidoge", async t => { + const events = await messageToEvent(data.message.infinidoge_reply_to_ami_matrix_smalltext_singleline_reply_to_infinidoge, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4", { + type: "m.room.message", + sender: "@ami:the-apothecary.club", + content: { + msgtype: "m.text", + body: `> <@_ooye_infinidoge:cadence.moe> Neat that they thought of that\n\nlet me guess they got a lot of bug reports like "empty chest with no loot?"`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to @_ooye_infinidoge:cadence.moe
Neat that they thought of that
let me guess they got a lot of bug reports like "empty chest with no loot?"`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$baby" + } + } + }, + event_id: "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4", + room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4" + } + }, + "m.mentions": { + user_ids: ["@ami:the-apothecary.club"] + }, + msgtype: "m.text", + body: `> Ami (she/her): let me guess they got a lot of bug reports like "empty chest with no loot?"\n\nMost likely`, + format: "org.matrix.custom.html", + formatted_body: `
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
Most likely`, + }]) +}) + test("message2event: simple written @mention for matrix user", async t => { const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, { api: { diff --git a/test/data.js b/test/data.js index 77b2ada..771c183 100644 --- a/test/data.js +++ b/test/data.js @@ -1562,6 +1562,182 @@ module.exports = { attachments: [], guild_id: "1100319549670301727" }, + infinidoge_reply_to_ami_matrix_smalltext_reply_to_infinidoge: { + type: 19, + tts: false, + timestamp: "2024-08-15T20:45:20.697000+00:00", + referenced_message: { + webhook_id: "1179987856631017473", + type: 0, + tts: false, + timestamp: "2024-08-15T20:43:42.705000+00:00", + pinned: false, + mentions: [ + { + username: "infinidoge1337", + public_flags: 768, + id: "197126718400626689", + global_name: "Infinidoge", + discriminator: "0", + clan: null, + avatar_decoration_data: null, + avatar: "89c59de97fdb5f90999d86fed01f23fe" + } + ], + mention_roles: [], + mention_everyone: false, + id: "1273743950028607530", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/813954820460642336/1100319550446252084/1273733346005880925 <@197126718400626689>:\n" + + "> -# Neat that they thought of that\n" + + 'let me guess they got a lot of bug reports like "empty chest with no loot?"', + components: [], + channel_id: "1100319550446252084", + author: { + username: "Ami (she/her)", + id: "1179987856631017473", + global_name: null, + discriminator: "0000", + bot: true, + avatar: "8b2ca50a10b39f4f908f536cd82d7953" + }, + attachments: [], + application_id: "684280192553844747" + }, + pinned: false, + nonce: "1273744360743108608", + message_reference: { + type: 0, + message_id: "1273743950028607530", + guild_id: "813954820460642336", + channel_id: "1100319550446252084" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ "832496260219928577" ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2021-05-05T21:22:36.181000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + banner: null, + avatar: null + }, + id: "1273744361036841061", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Most likely", + components: [], + channel_id: "1100319550446252084", + author: { + username: "infinidoge1337", + public_flags: 768, + id: "197126718400626689", + global_name: "Infinidoge", + discriminator: "0", + clan: null, + avatar_decoration_data: null, + avatar: "89c59de97fdb5f90999d86fed01f23fe" + }, + attachments: [], + guild_id: "813954820460642336" + }, + infinidoge_reply_to_ami_matrix_smalltext_singleline_reply_to_infinidoge: { + type: 19, + tts: false, + timestamp: "2024-08-15T20:45:20.697000+00:00", + referenced_message: { + webhook_id: "1179987856631017473", + type: 0, + tts: false, + timestamp: "2024-08-15T20:43:42.705000+00:00", + pinned: false, + mentions: [ + { + username: "infinidoge1337", + public_flags: 768, + id: "197126718400626689", + global_name: "Infinidoge", + discriminator: "0", + clan: null, + avatar_decoration_data: null, + avatar: "89c59de97fdb5f90999d86fed01f23fe" + } + ], + mention_roles: [], + mention_everyone: false, + id: "1273743950028607530", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "> -# <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/813954820460642336/1100319550446252084/1273733346005880925 <@197126718400626689>:" + + " Neat that they thought of that\n" + + 'let me guess they got a lot of bug reports like "empty chest with no loot?"', + components: [], + channel_id: "1100319550446252084", + author: { + username: "Ami (she/her)", + id: "1179987856631017473", + global_name: null, + discriminator: "0000", + bot: true, + avatar: "8b2ca50a10b39f4f908f536cd82d7953" + }, + attachments: [], + application_id: "684280192553844747" + }, + pinned: false, + nonce: "1273744360743108608", + message_reference: { + type: 0, + message_id: "1273743950028607530", + guild_id: "813954820460642336", + channel_id: "1100319550446252084" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ "832496260219928577" ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2021-05-05T21:22:36.181000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + banner: null, + avatar: null + }, + id: "1273744361036841061", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Most likely", + components: [], + channel_id: "1100319550446252084", + author: { + username: "infinidoge1337", + public_flags: 768, + id: "197126718400626689", + global_name: "Infinidoge", + discriminator: "0", + clan: null, + avatar_decoration_data: null, + avatar: "89c59de97fdb5f90999d86fed01f23fe" + }, + attachments: [], + guild_id: "813954820460642336" + }, sticker: { id: "1106366167788044450", type: 0, diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 32777a2..328c4b3 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -55,7 +55,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1202543413652881428', '1160894080998461480'), ('1207486471489986620', '1160894080998461480'), ('1210387798297682020', '112760669178241024'), -('1273204543739396116', '687028734322147344'); +('1273204543739396116', '687028734322147344'), +('1273743950028607530', '1100319550446252084'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -91,7 +92,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU', 'm.room.message', 'm.text', '1202543413652881428', 0, 0, 0), ('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0), ('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1), -('$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU', 'm.room.message', 'm.text', '1273204543739396116', 0, 0, 0); +('$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU', 'm.room.message', 'm.text', '1273204543739396116', 0, 0, 0), +('$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4', 'm.room.message', 'm.text', '1273743950028607530', 0, 0, 0); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), @@ -134,7 +136,8 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES ('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko'), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP'), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL); +('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL); INSERT INTO lottie (sticker_id, mxc_url) VALUES ('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR'); From 2a080b737cbb1f949314e177359bdf92fc5b91ea Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 20 Aug 2024 03:15:33 +1200 Subject: [PATCH 049/346] Parsing HTML with regular expressions (GONE WRONG) --- m2d/converters/event-to-message.js | 6 ++--- m2d/converters/event-to-message.test.js | 34 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 2a32dde..2984e8e 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -329,7 +329,7 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, */ async function handleRoomOrMessageLinks(input, di) { let offset = 0 - for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/(![^"/, ?)]+)(?:\/(\$[^"/ ?)]+))?(?:\?[^",:!? )]*)?)(">|[, )]|$)/g)]) { + for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/(![^"/, ?)]+)(?:\/(\$[^"/ ?)]+))?(?:\?[^",:!? )]*?)?)(">|[,<\n )]|$)/g)]) { assert(typeof match.index === "number") const [_, attributeValue, roomID, eventID, endMarker] = match let result @@ -726,7 +726,7 @@ async function eventToMessage(event, guild, di) { content = turndownService.turndown(root) // Put < > around any surviving matrix.to links to hide the URL previews - content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/g, "<$&>") + content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/g, "<$&>") // It's designed for commonmark, we need to replace the space-space-newline with just newline content = content.replace(/ \n/g, "\n") @@ -745,7 +745,7 @@ async function eventToMessage(event, guild, di) { } content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible - content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews + content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di) if (result) { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index f997b44..b404e59 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -3025,6 +3025,40 @@ test("event2message: mentioning known bridged events works (formatted body)", as ) }) +test("event2message: mentioning known bridged events followed by line break and user mention works (partially formatted body)", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `https://matrix.to/#/!CzvdIdUQXgUjDVKxeU:cadence.moe/$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10?via=cadence.moeextremity` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020<@114147806469554185>", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: mentioning unknown bridged events can approximate with timestamps", async t => { let called = 0 t.deepEqual( From 74632c671cb2b107fd46566362f9cb2d4fe95f6a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 20 Aug 2024 03:18:33 +1200 Subject: [PATCH 050/346] Update dependencies --- package-lock.json | 350 +++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 144 insertions(+), 208 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67beae8..1bd117a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@cloudrac3r/turndown": "^7.1.4", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", - "cloudstorm": "^0.10.8", + "cloudstorm": "^0.10.10", "domino": "^2.1.6", "entities": "^5.0.0", "get-stream": "^6.0.1", @@ -270,437 +270,368 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1316,12 +1247,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.10.9", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.9.tgz", - "integrity": "sha512-LLWCDn8CORM9iCFyWq3sIRK/7zJU7Jw14wLit3p5zWlD9Ihixv2nqjBbnWqEuq97zWh/xA+abACz2CvTVBlIWQ==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.10.tgz", + "integrity": "sha512-WcxpGc8lTQ24nn9L8WflyrU+sHE7StTXu8NeEYzBevMThbYRWVDQOuzca7iiGyaNWZmFu3Iv0nLbUjFFIOQ5CQ==", + "license": "MIT", "dependencies": { - "discord-api-types": "^0.37.90", - "snowtransfer": "^0.10.5" + "discord-api-types": "^0.37.93", + "snowtransfer": "^0.10.6" }, "engines": { "node": ">=14.8.0" @@ -1521,9 +1453,10 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.93", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.93.tgz", - "integrity": "sha512-M5jn0x3bcXk8EI2c6F6V6LeOWq10B/cJf+YJSyqNmg7z4bdXK+Z7g9zGJwHS0h9Bfgs0nun2LQISFOzwck7G9A==" + "version": "0.37.95", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.95.tgz", + "integrity": "sha512-EuEO4vu8+rtWbrVufDtYFH5dm40Wo55jBWEdwyvav1r3XiM51PzAnZ/BUaHmfqYLEooJs+3Tn56o/Vnp1qLMJg==", + "license": "MIT" }, "node_modules/domino": { "version": "2.1.6", @@ -2427,42 +2360,42 @@ } }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -2558,12 +2491,13 @@ } }, "node_modules/snowtransfer": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.10.5.tgz", - "integrity": "sha512-8enl43t7Ca1KYvipWIXaOjnNJBY2FFZfoKKs+b7tlb64mlEZgb5DbV0owc/X96hFzgVPj7akpcQKp7LNTlQJGA==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.10.6.tgz", + "integrity": "sha512-vRIHhV0YXeQ7LdnU+gwaX6ovoUbh5ajAA5etQY5lguySGZlYc3S2+GJ+EOTkVYDHtzGNOiBAUxR4Kn4iSHp+QQ==", + "license": "MIT", "dependencies": { - "discord-api-types": "^0.37.70", - "undici": "^6.6.2" + "discord-api-types": "^0.37.93", + "undici": "^6.19.5" }, "engines": { "node": ">=14.18.0" @@ -3017,6 +2951,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD", "optional": true }, "node_modules/tunnel-agent": { @@ -3043,9 +2978,10 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", - "integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==", + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz", + "integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==", + "license": "MIT", "engines": { "node": ">=18.17" } diff --git a/package.json b/package.json index 68ac545..8fc862c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@cloudrac3r/turndown": "^7.1.4", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", - "cloudstorm": "^0.10.8", + "cloudstorm": "^0.10.10", "domino": "^2.1.6", "entities": "^5.0.0", "get-stream": "^6.0.1", From df1296e579d98e62998f8b92dfe40efb3b42f126 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 26 Aug 2024 01:34:46 +1200 Subject: [PATCH 051/346] Store `invite` in database and sync power on startup --- db/migrations/0012-add-member-power.sql | 14 ++++++++++++ db/orm-defs.d.ts | 9 +++++++- db/orm.js | 11 ++++++++- m2d/event-dispatcher.js | 28 +++++++++++++++++++++-- matrix/api.js | 1 + matrix/power.js | 29 ++++++++++++++++++++++++ start.js | 1 + types.d.ts | 30 +++++++++++++++++++++++++ 8 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 db/migrations/0012-add-member-power.sql create mode 100644 matrix/power.js diff --git a/db/migrations/0012-add-member-power.sql b/db/migrations/0012-add-member-power.sql new file mode 100644 index 0000000..b0bb8d6 --- /dev/null +++ b/db/migrations/0012-add-member-power.sql @@ -0,0 +1,14 @@ +BEGIN TRANSACTION; + +-- the power we want them to have +CREATE TABLE IF NOT EXISTS member_power ( + mxid TEXT NOT NULL, + room_id TEXT NOT NULL, + power_level INTEGER NOT NULL, + PRIMARY KEY(mxid, room_id) +) WITHOUT ROWID; + +-- the power they have +ALTER TABLE member_cache ADD COLUMN power_level INTEGER NOT NULL DEFAULT 0; + +COMMIT; diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index 0223058..e481f95 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -42,7 +42,14 @@ export type Models = { room_id: string mxid: string displayname: string | null - avatar_url: string | null + avatar_url: string | null, + power_level: number + } + + member_power: { + mxid: string + room_id: string + power_level: number } message_channel: { diff --git a/db/orm.js b/db/orm.js index c6cab96..09e4bc7 100644 --- a/db/orm.js +++ b/db/orm.js @@ -44,6 +44,8 @@ class From { /** @private */ this.cols = [] /** @private */ + this.makeColsSafe = true + /** @private */ this.using = [] /** @private */ this.isPluck = false @@ -78,6 +80,12 @@ class From { return r } + selectUnsafe(...cols) { + this.cols = cols + this.makeColsSafe = false + return this + } + /** * @template {Col} Select * @param {Select} col @@ -112,7 +120,8 @@ class From { } prepare() { - let sql = `SELECT ${this.cols.map(k => `"${k}"`).join(", ")} FROM ${this.tables[0]} ` + if (this.makeColsSafe) this.cols = this.cols.map(k => `"${k}"`) + let sql = `SELECT ${this.cols.join(", ")} FROM ${this.tables[0]} ` for (let i = 1; i < this.tables.length; i++) { const table = this.tables[i] const col = this.using[i-1] diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 5529a26..7aa3b70 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -6,7 +6,7 @@ const util = require("util") const Ty = require("../types") -const {discord, db, sync, as} = require("../passthrough") +const {discord, db, sync, as, select} = require("../passthrough") /** @type {import("./actions/send-event")} */ const sendEvent = sync.require("./actions/send-event") @@ -167,5 +167,29 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", async event => { if (event.state_key[0] !== "@") return if (utils.eventSenderIsFromDiscord(event.state_key)) return - db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.state_key, event.content.displayname || null, event.content.avatar_url || null) + if (event.content.membership === "leave" || event.content.membership === "ban") { + // Member is gone + db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) + } else { + // Member is here + db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?") + .run( + event.room_id, event.state_key, + event.content.displayname || null, event.content.avatar_url || null, + event.content.displayname || null, event.content.avatar_url || null + ) + } +})) + +sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_levels", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all() + const newPower = event.content.users || {} + for (const mxid of existingPower) { + db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(newPower[mxid] || 0, event.room_id, mxid) + } })) diff --git a/matrix/api.js b/matrix/api.js index 82b1c10..65ec891 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -70,6 +70,7 @@ async function inviteToRoom(roomID, mxidToInvite, mxid) { } async function leaveRoom(roomID, mxid) { + console.log(`[api] leave: ${roomID}: ${mxid}`) await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) } diff --git a/matrix/power.js b/matrix/power.js new file mode 100644 index 0000000..5dac550 --- /dev/null +++ b/matrix/power.js @@ -0,0 +1,29 @@ +// @ts-check + +const {db, from} = require("../passthrough") +const api = require("./api") +const reg = require("./read-registration") +const ks = require("./kstate") +const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") + +// Migrate reg.ooye.invite setting to database +for (const mxid of reg.ooye.invite) { + db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") +} + +// Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. +const rows = from("member_cache").join("member_power", "mxid") + .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") + .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") + .all() + +;(async () => { + for (const row of rows) { + const kstate = await roomToKState(row.room_id) + const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}}) + await applyKStateDiffToRoom(row.room_id, diff) + // There is a listener on m.room.power_levels to do this same update, + // but we update it here anyway since the homeserver does not always deliver the event round-trip. + db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid) + } +})() diff --git a/start.js b/start.js index 2b6e3df..c7ae0eb 100644 --- a/start.js +++ b/start.js @@ -34,6 +34,7 @@ discord.snow.requestHandler.on("requestError", data => { await migrate.migrate(db) await discord.cloud.connect() console.log("Discord gateway started") + require("./matrix/power.js") require("./stdin") })() diff --git a/types.d.ts b/types.d.ts index 6036867..c710e12 100644 --- a/types.d.ts +++ b/types.d.ts @@ -209,6 +209,36 @@ export namespace Event { name?: string } + export type M_Power_Levels = { + /** The level required to ban a user. Defaults to 50 if unspecified. */ + ban?: number, + /** The level required to send specific event types. This is a mapping from event type to power level required. */ + events?: { + [event_id: string]: number + }, + /** The default level required to send message events. Can be overridden by the `events` key. Defaults to 0 if unspecified. */ + events_default?: number, + /** The level required to invite a user. Defaults to 0 if unspecified. */ + invite?: number, + /** The level required to kick a user. Defaults to 50 if unspecified. */ + kick?: number, + /** The power level requirements for specific notification types. This is a mapping from `key` to power level for that notifications key. */ + notifications?: { + room: number, + [key: string]: number + }, + /** The level required to redact an event sent by another user. Defaults to 50 if unspecified. */ + redact?: number, + /** The default level required to send state events. Can be overridden by the `events` key. Defaults to 50 if unspecified. */ + state_default?: number, + /** The power levels for specific users. This is a mapping from `user_id` to power level for that user. */ + users?: { + [mxid: string]: number + }, + /**The power level for users in the room whose `user_id` is not mentioned in the `users` key. Defaults to 0 if unspecified. */ + users_default?: number + } + export type M_Reaction = { "m.relates_to": { rel_type: "m.annotation" From b8793dae0f7e33bf7cb74d98d843c1369825a662 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 26 Aug 2024 01:42:56 +1200 Subject: [PATCH 052/346] Sync new power system to newly created rooms --- d2m/actions/create-room.js | 4 +++- d2m/actions/create-space.js | 7 +++++-- test/ooye-test-data.sql | 3 +++ test/test.js | 1 - 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 051e889..322f8be 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -142,6 +142,8 @@ async function channelToKState(channel, guild) { const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) + const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all() + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, @@ -161,7 +163,7 @@ async function channelToKState(channel, guild) { notifications: { room: everyoneCanMentionEveryone ? 0 : 20 }, - users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {}) + users: globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {}) }, "chat.schildi.hide_ui/read_receipts": { hidden: true diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index dbefd6d..44d0206 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -31,6 +31,8 @@ async function createSpace(guild, kstate) { const topic = kstate["m.room.topic/"]?.topic || undefined assert(name) + const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all() + const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => { return api.createRoom({ name, @@ -40,7 +42,7 @@ async function createSpace(guild, kstate) { events_default: 100, // space can only be managed by bridge invite: 0 // any existing member can invite others }, - invite: reg.ooye.invite, + invite: globalAdmins, topic, creation_content: { type: "m.space" @@ -58,6 +60,7 @@ async function createSpace(guild, kstate) { */ async function guildToKState(guild, privacyLevel) { assert.equal(typeof privacyLevel, "number") + const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all() const guildKState = { "m.room.name/": {name: guild.name}, "m.room.avatar/": { @@ -68,7 +71,7 @@ async function guildToKState(guild, privacyLevel) { "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]}, "m.room.join_rules/": {join_rule: createRoom.PRIVACY_ENUMS.SPACE_JOIN_RULES[privacyLevel]}, - "m.room.power_levels/": {users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {})} + "m.room.power_levels/": {users: globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})} // used in guild initial creation postApplyPowerLevels } return guildKState diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 328c4b3..1fb9e24 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -139,6 +139,9 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES ('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL); +INSERT INTO member_power (mxid, room_id, power_level) VALUES +('@test_auto_invite:example.org', '*', 100); + INSERT INTO lottie (sticker_id, mxc_url) VALUES ('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR'); diff --git a/test/test.js b/test/test.js index 396dea9..b5977f1 100644 --- a/test/test.js +++ b/test/test.js @@ -20,7 +20,6 @@ const db = new sqlite(":memory:") const reg = require("../matrix/read-registration") reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" -reg.ooye.invite = ["@test_auto_invite:example.org"] reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" From be405d3eed0bc905efe11f3fcc2cced1a6683ce9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 27 Aug 2024 02:17:10 +1200 Subject: [PATCH 053/346] Highly experimental message interactions --- addbot.js | 5 +- d2m/discord-packets.js | 6 ++ discord/interactions/bridge.js | 121 ++++++++++++++++++++++++++++ discord/interactions/invite.js | 113 ++++++++++++++++++++++++++ discord/interactions/matrix-info.js | 48 +++++++++++ discord/interactions/permissions.js | 108 +++++++++++++++++++++++++ discord/register-interactions.js | 90 +++++++++++++++++++++ discord/utils.js | 3 +- start.js | 1 + 9 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 discord/interactions/bridge.js create mode 100644 discord/interactions/invite.js create mode 100644 discord/interactions/matrix-info.js create mode 100644 discord/interactions/permissions.js create mode 100644 discord/register-interactions.js diff --git a/addbot.js b/addbot.js index 667fbab..106ebc1 100644 --- a/addbot.js +++ b/addbot.js @@ -1,10 +1,10 @@ // @ts-check const config = require("./config") +const token = config.discordToken +const id = Buffer.from(token.split(".")[0], "base64").toString() function addbot() { - const token = config.discordToken - const id = Buffer.from(token.split(".")[0], "base64") return `Open this link to add the bot to a Discord server:\nhttps://discord.com/oauth2/authorize?client_id=${id}&scope=bot&permissions=1610883072 ` } @@ -12,4 +12,5 @@ if (process.argv.find(a => a.endsWith("addbot") || a.endsWith("addbot.js"))) { console.log(addbot()) } +module.exports.id = id module.exports.addbot = addbot diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 6ba839a..0981827 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -16,6 +16,8 @@ const utils = { // requiring this later so that the client is already constructed by the time event-dispatcher is loaded /** @type {typeof import("./event-dispatcher")} */ const eventDispatcher = sync.require("./event-dispatcher") + /** @type {import("../discord/register-interactions")} */ + const interactions = sync.require("../discord/register-interactions") // Client internals, keep track of the state we need if (message.t === "READY") { @@ -172,7 +174,11 @@ const utils = { } else if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { await eventDispatcher.onSomeReactionsRemoved(client, message.d) + + } else if (message.t === "INTERACTION_CREATE") { + await interactions.dispatchInteraction(message.d) } + } catch (e) { // Let OOYE try to handle errors too eventDispatcher.onError(client, e, message) diff --git a/discord/interactions/bridge.js b/discord/interactions/bridge.js new file mode 100644 index 0000000..53eee7d --- /dev/null +++ b/discord/interactions/bridge.js @@ -0,0 +1,121 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const Ty = require("../../types") +const {discord, sync, db, select, from, as} = require("../../passthrough") +const assert = require("assert/strict") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** @type {Map>} spaceID -> list of rooms */ +const cache = new Map() +/** @type {Map} roomID -> spaceID */ +const reverseCache = new Map() + +// Manage clearing the cache +sync.addTemporaryListener(as, "type:m.room.name", /** @param {Ty.Event.StateOuter} event */ async event => { + if (event.state_key !== "") return + const roomID = event.room_id + const spaceID = reverseCache.get(roomID) + if (!spaceID) return + const childRooms = await cache.get(spaceID) + if (!childRooms) return + if (event.content.name) { + const found = childRooms.find(r => r.value === roomID) + if (!found) return + found.name = event.content.name + } else { + cache.set(spaceID, Promise.resolve(childRooms.filter(r => r.value !== roomID))) + reverseCache.delete(roomID) + } +}) + +// Manage adding to the cache +async function getHierarchy(spaceID) { + return cache.get(spaceID) || (() => { + const entry = (async () => { + /** @type {{name: string, value: string}[]} */ + let childRooms = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.HierarchyPagination} */ + const res = await api.getHierarchy(spaceID, {from: nextBatch}) + for (const room of res.rooms) { + if (room.name) { + childRooms.push({name: room.name, value: room.room_id}) + reverseCache.set(room.room_id, spaceID) + } + } + nextBatch = res.next_batch + } while (nextBatch) + return childRooms + })() + cache.set(spaceID, entry) + return entry + })() +} + +/** @param {DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction */ +async function interactAutocomplete({id, token, data, guild_id}) { + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + if (!spaceID) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult, + data: { + choices: [ + { + name: `Error: This server needs to be bridged somewhere first...`, + value: "baby" + } + ] + } + }) + } + + let rooms = await getHierarchy(spaceID) + // @ts-ignore + rooms = rooms.filter(r => r.name.startsWith(data.options[0].value)) + + await discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult, + data: { + choices: rooms + } + }) +} + +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +async function interactSubmit({id, token, data, guild_id}) { + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + if (!spaceID) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Error: This server needs to be bridged somewhere first...", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Valid input. This would do something but it isn't implemented yet.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) +} + +/** @param {DiscordTypes.APIGuildInteraction} interaction */ +async function interact(interaction) { + if (interaction.type === DiscordTypes.InteractionType.ApplicationCommandAutocomplete) { + return interactAutocomplete(interaction) + } else if (interaction.type === DiscordTypes.InteractionType.ApplicationCommand) { + // @ts-ignore + return interactSubmit(interaction) + } +} + +module.exports.interact = interact diff --git a/discord/interactions/invite.js b/discord/interactions/invite.js new file mode 100644 index 0000000..0590be8 --- /dev/null +++ b/discord/interactions/invite.js @@ -0,0 +1,113 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const assert = require("assert/strict") +const {discord, sync, db, select, from} = require("../../passthrough") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +async function interact({id, token, data, channel, member, guild_id}) { + // Check guild is bridged + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + if (!spaceID || !roomID) return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This server isn't bridged to Matrix, so you can't invite Matrix users.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + + // Get named MXID + /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore + const options = data.options + const input = options?.[0].value || "" + const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] + if (!mxid) return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + + // Check for existing invite to the space + let spaceMember + try { + spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) + } catch (e) {} + if (spaceMember && spaceMember.membership === "invite") { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + // Invite Matrix user if not in space + if (!spaceMember || spaceMember.membership !== "join") { + await api.inviteToRoom(spaceID, mxid) + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `You invited \`${mxid}\` to the server.` + } + }) + } + + // The Matrix user *is* in the space, maybe we want to invite them to this channel? + let roomMember + try { + roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) + } catch (e) {} + if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, + flags: DiscordTypes.MessageFlags.Ephemeral, + components: [{ + type: DiscordTypes.ComponentType.ActionRow, + components: [{ + type: DiscordTypes.ComponentType.Button, + custom_id: "invite_channel", + style: DiscordTypes.ButtonStyle.Primary, + label: "Sure", + }] + }] + } + }) + } + + // The Matrix user *is* in the space and in the channel. + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `\`${mxid}\` is already in this server and this channel.`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) +} + +/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ +async function interactButton({id, token, data, channel, member, guild_id, message}) { + const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] + assert(mxid) + const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + await api.inviteToRoom(roomID, mxid) + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.UpdateMessage, + data: { + content: `You invited \`${mxid}\` to the channel.`, + flags: DiscordTypes.MessageFlags.Ephemeral, + components: [] + } + }) +} + +module.exports.interact = interact +module.exports.interactButton = interactButton diff --git a/discord/interactions/matrix-info.js b/discord/interactions/matrix-info.js new file mode 100644 index 0000000..9f0e9e1 --- /dev/null +++ b/discord/interactions/matrix-info.js @@ -0,0 +1,48 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db, select, from} = require("../../passthrough") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +async function interact({id, token, data}) { + const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") + .select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get() + + if (!message) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This message hasn't been bridged to Matrix.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + if (message.source === 1) { // from Discord + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `This message was bridged to [${message.nick || message.name}]() on Matrix.` + + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + // from Matrix + const event = await api.getEvent(message.room_id, message.event_id) + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `This message was bridged from [${message.nick || message.name}]() on Matrix.` + + `\nIt was originally sent by [${event.sender}]().` + + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) +} + +module.exports.interact = interact diff --git a/discord/interactions/permissions.js b/discord/interactions/permissions.js new file mode 100644 index 0000000..d30f632 --- /dev/null +++ b/discord/interactions/permissions.js @@ -0,0 +1,108 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const Ty = require("../../types") +const {discord, sync, db, select, from} = require("../../passthrough") +const assert = require("assert/strict") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +async function interact({data, channel, id, token, guild_id}) { + const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() + assert(row) + + // Can't operate on Discord users + if (row.source === 1) { // discord + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `This command is only meaningful for Matrix users.`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + // Get the message sender, the person that will be inspected/edited + const eventID = row.event_id + const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + assert(roomID) + const event = await api.getEvent(roomID, eventID) + const sender = event.sender + + // Get the space, where the power levels will be inspected/edited + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + assert(spaceID) + + // Get the power level + /** @type {Ty.Event.M_Power_Levels} */ + const powerLevelsContent = await api.getStateEvent(spaceID, "m.room.power_levels", "") + const userPower = powerLevelsContent.users?.[event.sender] || 0 + + // Administrators equal to the bot cannot be demoted + if (userPower >= 100) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `\`${sender}\` has administrator permissions. This cannot be edited.`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + await discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Showing permissions for \`${sender}\`. Click to edit.`, + flags: DiscordTypes.MessageFlags.Ephemeral, + components: [ + { + type: DiscordTypes.ComponentType.ActionRow, + components: [ + { + type: DiscordTypes.ComponentType.StringSelect, + custom_id: "permissions_edit", + options: [ + { + label: "Default", + value: "default", + default: userPower < 50 + }, { + label: "Moderator", + value: "moderator", + default: userPower >= 50 && userPower < 100 + } + ] + } + ] + } + ] + } + }) +} + +/** @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction */ +async function interactEdit({data, channel, id, token, guild_id, message}) { + // Get the person that will be inspected/edited + const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] + assert(mxid) + + // Get the space, where the power levels will be inspected/edited + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + assert(spaceID) + + // Do it + const permission = data.values[0] + const power = permission === "moderator" ? 50 : 0 + await api.setUserPower(spaceID, mxid, power) + // TODO: Cascade permissions through room hierarchy (make a helper for this already, geez...) + + // ACK + await discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.DeferredMessageUpdate + }) +} + +module.exports.interact = interact +module.exports.interactEdit = interactEdit diff --git a/discord/register-interactions.js b/discord/register-interactions.js new file mode 100644 index 0000000..79bcb14 --- /dev/null +++ b/discord/register-interactions.js @@ -0,0 +1,90 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db, select} = require("../passthrough") +const {id} = require("../addbot") + +const matrixInfo = sync.require("./interactions/matrix-info.js") +const invite = sync.require("./interactions/invite.js") +const permissions = sync.require("./interactions/permissions.js") +const bridge = sync.require("./interactions/bridge.js") + +discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ + name: "Matrix info", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.Message, +}, { + name: "Permissions", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.Message, + default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers) +}, { + name: "invite", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.ChatInput, + description: "Invite a Matrix user to this Discord server", + default_member_permissions: String(DiscordTypes.PermissionFlagsBits.CreateInstantInvite), + options: [ + { + type: DiscordTypes.ApplicationCommandOptionType.String, + description: "The Matrix user to invite, e.g. @username:example.org", + name: "user" + } + ] +}, { + name: "bridge", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.ChatInput, + description: "Start bridging this channel to a Matrix room.", + default_member_permissions: String(DiscordTypes.PermissionFlagsBits.ManageChannels), + options: [ + { + type: DiscordTypes.ApplicationCommandOptionType.String, + description: "Destination room to bridge to.", + name: "room", + autocomplete: true + } + ] +}]) + +async function dispatchInteraction(interaction) { + const id = interaction.data.custom_id || interaction.data.name + try { + console.log(interaction) + if (id === "Matrix info") { + await matrixInfo.interact(interaction) + } else if (id === "invite") { + await invite.interact(interaction) + } else if (id === "invite_channel") { + await invite.interactButton(interaction) + } else if (id === "Permissions") { + await permissions.interact(interaction) + } else if (id === "permissions_edit") { + await permissions.interactEdit(interaction) + } else if (id === "bridge") { + await bridge.interact(interaction) + } else { + throw new Error(`Unknown interaction ${id}`) + } + } catch (e) { + let stackLines = null + if (e.stack) { + stackLines = e.stack.split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) + } + } + discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Interaction failed: **${id}**` + + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` + + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } +} + +module.exports.dispatchInteraction = dispatchInteraction diff --git a/discord/utils.js b/discord/utils.js index 6e95d17..57e563f 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -102,11 +102,10 @@ function isWebhookMessage(message) { } /** - * Ephemeral messages can be generated if a slash command is attached to the same bot that OOYE is running on * @param {Pick} message */ function isEphemeralMessage(message) { - return message.flags && (message.flags & (1 << 6)); + return message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral) } /** @param {string} snowflake */ diff --git a/start.js b/start.js index c7ae0eb..63f5c57 100644 --- a/start.js +++ b/start.js @@ -8,6 +8,7 @@ const config = require("./config") const passthrough = require("./passthrough") const db = new sqlite("db/ooye.db") +/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() Object.assign(passthrough, {config, sync, db}) From 89696fd161b9740717d42918dbb042e17eee607b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 27 Aug 2024 19:02:53 +1200 Subject: [PATCH 054/346] Update dependencies --- package-lock.json | 213 +++++++++++++++++----------------------------- package.json | 3 + 2 files changed, 82 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bd117a..ec10c03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,9 @@ "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", "supertape": "^10.4.0" + }, + "engines": { + "node": ">=20" } }, "../in-your-element": { @@ -642,6 +645,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -659,6 +663,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -670,13 +675,15 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -694,6 +701,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -757,6 +765,7 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -930,10 +939,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.46.tgz", + "integrity": "sha512-vnRgMS7W6cKa1/0G3/DTtQYpVrZ8c0Xm6UkLaVFrb9jtcVC3okokW09Ki1Qdrj9ISokszD69nY4WDLRlvHlhAA==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -1046,10 +1056,11 @@ ] }, "node_modules/better-sqlite3": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.1.2.tgz", - "integrity": "sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz", + "integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -1247,13 +1258,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.10.tgz", - "integrity": "sha512-WcxpGc8lTQ24nn9L8WflyrU+sHE7StTXu8NeEYzBevMThbYRWVDQOuzca7iiGyaNWZmFu3Iv0nLbUjFFIOQ5CQ==", + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.10.11.tgz", + "integrity": "sha512-A3lN0o404la7ryWIxN73gW2ehC0RO4h0yCA2grtOtPh8rNTd6+R2U4llyJlb61HlyOFrEVJ7AbOoFblVSmkrtw==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.37.93", - "snowtransfer": "^0.10.6" + "discord-api-types": "^0.37.98", + "snowtransfer": "^0.10.7" }, "engines": { "node": ">=14.8.0" @@ -1453,9 +1464,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.95", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.95.tgz", - "integrity": "sha512-EuEO4vu8+rtWbrVufDtYFH5dm40Wo55jBWEdwyvav1r3XiM51PzAnZ/BUaHmfqYLEooJs+3Tn56o/Vnp1qLMJg==", + "version": "0.37.98", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.98.tgz", + "integrity": "sha512-xsH4UwmnCQl4KjAf01/p9ck9s+/vDqzHbUxPOBzo8fcVUa/hQG6qInD7Cr44KAuCM+xCxGJFSAUx450pBrX0+g==", "license": "MIT" }, "node_modules/domino": { @@ -1467,7 +1478,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1652,24 +1664,22 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": "20 || >=22" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -1839,16 +1849,14 @@ } }, "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": "20 || >=22" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -1912,6 +1920,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -1970,15 +1985,16 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2122,30 +2138,22 @@ "dev": true }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", - "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -2491,13 +2499,13 @@ } }, "node_modules/snowtransfer": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.10.6.tgz", - "integrity": "sha512-vRIHhV0YXeQ7LdnU+gwaX6ovoUbh5ajAA5etQY5lguySGZlYc3S2+GJ+EOTkVYDHtzGNOiBAUxR4Kn4iSHp+QQ==", + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.10.7.tgz", + "integrity": "sha512-lXUYp6jOou0DI8uFl3Dh78KD1gVa3dNbUt2TK6RW39mHenAR6XpoPoydUNXCWvdxi6uGU6zQ1yNICZpKjF6wMA==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.37.93", - "undici": "^6.19.5" + "discord-api-types": "^0.37.98", + "undici": "^6.19.8" }, "engines": { "node": ">=14.18.0" @@ -2583,6 +2591,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2597,6 +2606,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2606,6 +2616,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2655,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2667,6 +2679,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2696,10 +2709,11 @@ } }, "node_modules/supertape": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.7.2.tgz", - "integrity": "sha512-pBWvzBtefVgokjJRk2ks+6q8/Sy1fIIvMBL8nXYaXk3100cgcPsw4iYhFSW41tg/cEe4IOlV/G+FZFN4kSMtOg==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.7.3.tgz", + "integrity": "sha512-t/zv0sev+5261g9KampNVL7io8UQ7zmouRWt9/UU+Yr7Ap0MqBKlyDFFvkzcfADT+O6bXZMW5x3nzYzSU+LAYg==", "dev": true, + "license": "MIT", "dependencies": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^2.0.0", @@ -2715,7 +2729,7 @@ "cli-progress": "^3.8.2", "flatted": "^3.3.1", "fullstore": "^3.0.0", - "glob": "^11.0.0", + "glob": "^10.0.0", "jest-diff": "^29.0.1", "once": "^1.4.0", "resolve": "^1.17.0", @@ -2810,78 +2824,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -2978,9 +2920,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.19.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz", - "integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", "license": "MIT", "engines": { "node": ">=18.17" @@ -3076,6 +3018,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3093,6 +3036,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3102,6 +3046,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, diff --git a/package.json b/package.json index 8fc862c..bbdde1e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ ], "author": "Cadence, PapiOphidian", "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=20" + }, "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", "@cloudrac3r/discord-markdown": "^2.6.3", From 3a84658e8b935ac1634c0af49425b008b16ed4a4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 27 Aug 2024 19:10:38 +1200 Subject: [PATCH 055/346] Make a helper for the room hierarchy --- d2m/actions/create-space.js | 15 +++------------ discord/interactions/bridge.js | 24 +++++++++--------------- matrix/api.js | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 44d0206..b15cba6 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -185,19 +185,10 @@ async function syncSpaceFully(guildID) { const spaceDiff = ks.diffKState(spaceKState, guildKState) await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) - /** @type {string[]} room IDs */ - let childRooms = [] - /** @type {string | undefined} */ - let nextBatch = undefined - do { - /** @type {Ty.HierarchyPagination} */ - const res = await api.getHierarchy(spaceID, {from: nextBatch}) - childRooms.push(...res.rooms.map(room => room.room_id)) - nextBatch = res.next_batch - } while (nextBatch) + const childRooms = await api.getFullHierarchy(spaceID) - for (const roomID of childRooms) { - const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get() + for (const {room_id} of childRooms) { + const channelID = select("channel_room", "channel_id", {room_id}).pluck().get() if (!channelID) continue if (discord.channels.has(channelID)) { await createRoom.syncRoom(channelID) diff --git a/discord/interactions/bridge.js b/discord/interactions/bridge.js index 53eee7d..ee33bfd 100644 --- a/discord/interactions/bridge.js +++ b/discord/interactions/bridge.js @@ -32,24 +32,18 @@ sync.addTemporaryListener(as, "type:m.room.name", /** @param {Ty.Event.StateOute }) // Manage adding to the cache -async function getHierarchy(spaceID) { +async function getCachedHierarchy(spaceID) { return cache.get(spaceID) || (() => { const entry = (async () => { + const result = await api.getFullHierarchy(spaceID) /** @type {{name: string, value: string}[]} */ - let childRooms = [] - /** @type {string | undefined} */ - let nextBatch = undefined - do { - /** @type {Ty.HierarchyPagination} */ - const res = await api.getHierarchy(spaceID, {from: nextBatch}) - for (const room of res.rooms) { - if (room.name) { - childRooms.push({name: room.name, value: room.room_id}) - reverseCache.set(room.room_id, spaceID) - } + const childRooms = [] + for (const room of result) { + if (room.name) { + childRooms.push({name: room.name, value: room.room_id}) + reverseCache.set(room.room_id, spaceID) } - nextBatch = res.next_batch - } while (nextBatch) + } return childRooms })() cache.set(spaceID, entry) @@ -74,7 +68,7 @@ async function interactAutocomplete({id, token, data, guild_id}) { }) } - let rooms = await getHierarchy(spaceID) + let rooms = await getCachedHierarchy(spaceID) // @ts-ignore rooms = rooms.filter(r => r.name.startsWith(data.options[0].value)) diff --git a/matrix/api.js b/matrix/api.js index 65ec891..7d8ea9f 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -135,6 +135,24 @@ function getHierarchy(roomID, pagination) { return mreq.mreq("GET", path) } +/** + * Like `getHierarchy` but collects all pages for you. + * @param {string} roomID + */ +async function getFullHierarchy(roomID) { + /** @type {Ty.R.Hierarchy[]} */ + let rooms = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.HierarchyPagination} */ + const res = await getHierarchy(roomID, {from: nextBatch}) + rooms.push(...res.rooms) + nextBatch = res.next_batch + } while (nextBatch) + return rooms +} + /** * @param {string} roomID * @param {string} eventID @@ -254,6 +272,7 @@ module.exports.getAllState = getAllState module.exports.getStateEvent = getStateEvent module.exports.getJoinedMembers = getJoinedMembers module.exports.getHierarchy = getHierarchy +module.exports.getFullHierarchy = getFullHierarchy module.exports.getRelations = getRelations module.exports.sendState = sendState module.exports.sendEvent = sendEvent From ddb211f8f3aae75f85d3b5a7fae5218c1d8adaf5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:17:34 +1200 Subject: [PATCH 056/346] Make permissions command apply recursively --- d2m/discord-client.js | 3 ++ discord/interactions/permissions.js | 53 ++++++++++++++++++++--------- discord/register-interactions.js | 29 +++++++--------- matrix/api.js | 16 +++++++++ start.js | 4 --- 5 files changed, 69 insertions(+), 36 deletions(-) diff --git a/d2m/discord-client.js b/d2m/discord-client.js index 80dcbcf..ace8481 100644 --- a/d2m/discord-client.js +++ b/d2m/discord-client.js @@ -57,6 +57,9 @@ class DiscordClient { addEventLogger("error", "Error") addEventLogger("disconnected", "Disconnected") addEventLogger("ready", "Ready") + this.snow.requestHandler.on("requestError", (requestID, error) => { + console.error("request error:", error) + }) } } diff --git a/discord/interactions/permissions.js b/discord/interactions/permissions.js index d30f632..82c3d3c 100644 --- a/discord/interactions/permissions.js +++ b/discord/interactions/permissions.js @@ -5,23 +5,27 @@ const Ty = require("../../types") const {discord, sync, db, select, from} = require("../../passthrough") const assert = require("assert/strict") + /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -async function interact({data, channel, id, token, guild_id}) { +/** + * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction + * @returns {Promise} + */ +async function _interact({data, channel, guild_id}) { const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() assert(row) // Can't operate on Discord users if (row.source === 1) { // discord - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `This command is only meaningful for Matrix users.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } // Get the message sender, the person that will be inspected/edited @@ -42,16 +46,16 @@ async function interact({data, channel, id, token, guild_id}) { // Administrators equal to the bot cannot be demoted if (userPower >= 100) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${sender}\` has administrator permissions. This cannot be edited.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } - await discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `Showing permissions for \`${sender}\`. Click to edit.`, @@ -79,30 +83,47 @@ async function interact({data, channel, id, token, guild_id}) { } ] } - }) + } } -/** @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction */ -async function interactEdit({data, channel, id, token, guild_id, message}) { +/** + * @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction + */ +async function interactEdit({data, id, token, guild_id, message}) { // Get the person that will be inspected/edited const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) + const permission = data.values[0] + const power = permission === "moderator" ? 50 : 0 + + await discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.UpdateMessage, + data: { + content: `Updating \`${mxid}\` to **${permission}**, please wait...`, + components: [] + } + }) + // Get the space, where the power levels will be inspected/edited const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() assert(spaceID) // Do it - const permission = data.values[0] - const power = permission === "moderator" ? 50 : 0 - await api.setUserPower(spaceID, mxid, power) - // TODO: Cascade permissions through room hierarchy (make a helper for this already, geez...) + await api.setUserPowerCascade(spaceID, mxid, power) // ACK - await discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.DeferredMessageUpdate + await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, { + content: `Updated \`${mxid}\` to **${permission}**.`, + components: [] }) } +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) +} + module.exports.interact = interact module.exports.interactEdit = interactEdit +module.exports._interact = _interact diff --git a/discord/register-interactions.js b/discord/register-interactions.js index 79bcb14..a6e4332 100644 --- a/discord/register-interactions.js +++ b/discord/register-interactions.js @@ -48,23 +48,23 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ }]) async function dispatchInteraction(interaction) { - const id = interaction.data.custom_id || interaction.data.name + const interactionId = interaction.data.custom_id || interaction.data.name try { console.log(interaction) - if (id === "Matrix info") { + if (interactionId === "Matrix info") { await matrixInfo.interact(interaction) - } else if (id === "invite") { + } else if (interactionId === "invite") { await invite.interact(interaction) - } else if (id === "invite_channel") { + } else if (interactionId === "invite_channel") { await invite.interactButton(interaction) - } else if (id === "Permissions") { + } else if (interactionId === "Permissions") { await permissions.interact(interaction) - } else if (id === "permissions_edit") { + } else if (interactionId === "permissions_edit") { await permissions.interactEdit(interaction) - } else if (id === "bridge") { + } else if (interactionId === "bridge") { await bridge.interact(interaction) } else { - throw new Error(`Unknown interaction ${id}`) + throw new Error(`Unknown interaction ${interactionId}`) } } catch (e) { let stackLines = null @@ -75,14 +75,11 @@ async function dispatchInteraction(interaction) { stackLines = stackLines.slice(0, cloudstormLine - 2) } } - discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `Interaction failed: **${id}**` - + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` - + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, - flags: DiscordTypes.MessageFlags.Ephemeral - } + await discord.snow.interaction.createFollowupMessage(id, interaction.token, { + content: `Interaction failed: **${interactionId}**` + + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` + + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, + flags: DiscordTypes.MessageFlags.Ephemeral }) } } diff --git a/matrix/api.js b/matrix/api.js index 7d8ea9f..e94a1a5 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -260,6 +260,21 @@ async function setUserPower(roomID, mxid, power) { return powerLevels } +/** + * Set a user's power level for a whole room hierarchy. + * @param {string} roomID + * @param {string} mxid + * @param {number} power + */ +async function setUserPowerCascade(roomID, mxid, power) { + assert(roomID[0] === "!") + assert(mxid[0] === "@") + const rooms = await getFullHierarchy(roomID) + for (const room of rooms) { + await setUserPower(room.room_id, mxid, power) + } +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -281,3 +296,4 @@ module.exports.sendTyping = sendTyping module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.setUserPower = setUserPower +module.exports.setUserPowerCascade = setUserPowerCascade diff --git a/start.js b/start.js index 63f5c57..e218819 100644 --- a/start.js +++ b/start.js @@ -27,10 +27,6 @@ passthrough.select = orm.select sync.require("./m2d/event-dispatcher") -discord.snow.requestHandler.on("requestError", data => { - console.error("request error", data) -}) - ;(async () => { await migrate.migrate(db) await discord.cloud.connect() From 78a17b2de9e80ceb567c518e648972ef630914d9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:17:54 +1200 Subject: [PATCH 057/346] Update formatting of matrix info command --- discord/interactions/matrix-info.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/discord/interactions/matrix-info.js b/discord/interactions/matrix-info.js index 9f0e9e1..fac3804 100644 --- a/discord/interactions/matrix-info.js +++ b/discord/interactions/matrix-info.js @@ -7,7 +7,8 @@ const {discord, sync, db, select, from} = require("../../passthrough") const api = sync.require("../../matrix/api") /** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -async function interact({id, token, data}) { +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact({id, token, guild_id, channel, data}) { const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") .select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get() @@ -21,12 +22,15 @@ async function interact({id, token, data}) { }) } + const idInfo = `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\`` + if (message.source === 1) { // from Discord + const userID = data.resolved.messages[data.target_id].author.id return discord.snow.interaction.createInteractionResponse(id, token, { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This message was bridged to [${message.nick || message.name}]() on Matrix.` - + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord to [${message.nick || message.name}]() on Matrix.` + + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } }) @@ -37,9 +41,8 @@ async function interact({id, token, data}) { return discord.snow.interaction.createInteractionResponse(id, token, { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This message was bridged from [${message.nick || message.name}]() on Matrix.` - + `\nIt was originally sent by [${event.sender}]().` - + `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + content: `Bridged [${event.sender}]()'s message in [${message.nick || message.name}]() on Matrix to https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord.` + + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } }) From 4b7593d630e30ef5f1dfb2f3307461aac8d1ef61 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:18:06 +1200 Subject: [PATCH 058/346] Make invite command more testable --- discord/interactions/invite.js | 54 ++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/discord/interactions/invite.js b/discord/interactions/invite.js index 0590be8..689ea1a 100644 --- a/discord/interactions/invite.js +++ b/discord/interactions/invite.js @@ -7,31 +7,34 @@ const {discord, sync, db, select, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ -async function interact({id, token, data, channel, member, guild_id}) { +/** + * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction + * @returns {Promise} + */ +async function _interact({data, channel, guild_id}) { // Check guild is bridged const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() - if (!spaceID || !roomID) return discord.snow.interaction.createInteractionResponse(id, token, { + if (!spaceID || !roomID) return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This server isn't bridged to Matrix, so you can't invite Matrix users.", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options const input = options?.[0].value || "" const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] - if (!mxid) return discord.snow.interaction.createInteractionResponse(id, token, { + if (!mxid) return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } // Check for existing invite to the space let spaceMember @@ -39,24 +42,24 @@ async function interact({id, token, data, channel, member, guild_id}) { spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) } catch (e) {} if (spaceMember && spaceMember.membership === "invite") { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } // Invite Matrix user if not in space if (!spaceMember || spaceMember.membership !== "join") { await api.inviteToRoom(spaceID, mxid) - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `You invited \`${mxid}\` to the server.` } - }) + } } // The Matrix user *is* in the space, maybe we want to invite them to this channel? @@ -65,7 +68,7 @@ async function interact({id, token, data, channel, member, guild_id}) { roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) } catch (e) {} if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, @@ -80,34 +83,49 @@ async function interact({id, token, data, channel, member, guild_id}) { }] }] } - }) + } } // The Matrix user *is* in the space and in the channel. - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${mxid}\` is already in this server and this channel.`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } -/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ -async function interactButton({id, token, data, channel, member, guild_id, message}) { +/** + * @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction + * @returns {Promise} + */ +async function _interactButton({channel, message}) { const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() await api.inviteToRoom(roomID, mxid) - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.UpdateMessage, data: { content: `You invited \`${mxid}\` to the channel.`, flags: DiscordTypes.MessageFlags.Ephemeral, components: [] } - }) + } +} + +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) +} + +/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ +async function interactButton(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction)) } module.exports.interact = interact module.exports.interactButton = interactButton +module.exports._interact = _interact +module.exports._interactButton = _interactButton From 42bfd034cf4b1bc9d97924c247d47976dc801d4d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 00:50:48 +1200 Subject: [PATCH 059/346] Bridge command author metadata to Matrix --- d2m/converters/message-to-event.js | 5 +++++ discord/interactions/bridge.js | 2 +- discord/utils.js | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 1e77d9d..d4b580f 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -229,6 +229,11 @@ async function messageToEvent(message, guild, options = {}, di) { }] } + if (message.type === DiscordTypes.MessageType.ChatInputCommand && message.interaction_metadata && "name" in message.interaction_metadata) { + // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top. + message.content = `> ↪️ <@${message.interaction_metadata.user.id}> used \`/${message.interaction_metadata.name}\`\n${message.content}` + } + /** @type {{room?: boolean, user_ids?: string[]}} We should consider the following scenarios for mentions: diff --git a/discord/interactions/bridge.js b/discord/interactions/bridge.js index ee33bfd..b2d1ac0 100644 --- a/discord/interactions/bridge.js +++ b/discord/interactions/bridge.js @@ -39,7 +39,7 @@ async function getCachedHierarchy(spaceID) { /** @type {{name: string, value: string}[]} */ const childRooms = [] for (const room of result) { - if (room.name) { + if (room.name && !room.name.match(/^\[[⛓️🔊]\]/) && room.room_type !== "m.space") { childRooms.push({name: room.name, value: room.room_id}) reverseCache.set(room.room_id, spaceID) } diff --git a/discord/utils.js b/discord/utils.js index 57e563f..865b2e3 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -97,8 +97,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) { * @param {DiscordTypes.APIMessage} message */ function isWebhookMessage(message) { - const isInteractionResponse = message.type === 20 - return message.webhook_id && !isInteractionResponse + return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand } /** From a6c961984d39b207a04c7065b46b3dd088be931e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:31:57 +1200 Subject: [PATCH 060/346] An emoji can be a single character --- d2m/converters/message-to-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index d4b580f..e6044b6 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -368,7 +368,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter. // So we scan the message ahead of time for all its emojis and ensure they are in the DB. - const emojiMatches = [...content.matchAll(/<(a?):([^:>]{2,64}):([0-9]+)>/g)] + const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)] await Promise.all(emojiMatches.map(match => { const id = match[3] const name = match[2] From 818311bcb4cbca013e66c71c5c0cb7b410b994b0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:35:53 +1200 Subject: [PATCH 061/346] Use kstate $url feature for channel icons --- d2m/actions/create-room.js | 3 +-- d2m/actions/create-space.js | 1 - d2m/actions/create-space.test.js | 1 - test/data.js | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 322f8be..2dd62e0 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -120,8 +120,7 @@ async function channelToKState(channel, guild) { if (customAvatar) { avatarEventContent.url = customAvatar } else if (guild.icon) { - avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API + avatarEventContent.url = {$url: file.guildIcon(guild)} } let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index b15cba6..a9b8448 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -65,7 +65,6 @@ async function guildToKState(guild, privacyLevel) { "m.room.name/": {name: guild.name}, "m.room.avatar/": { $if: guild.icon, - discord_path: file.guildIcon(guild), url: {$url: file.guildIcon(guild)} }, "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, diff --git a/d2m/actions/create-space.test.js b/d2m/actions/create-space.test.js index b1c1f06..c4111db 100644 --- a/d2m/actions/create-space.test.js +++ b/d2m/actions/create-space.test.js @@ -14,7 +14,6 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))), { "m.room.avatar/": { - discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" }, "m.room.guest_access/": { diff --git a/test/data.js b/test/data.js index 771c183..456033a 100644 --- a/test/data.js +++ b/test/data.js @@ -38,8 +38,7 @@ module.exports = { }] }, "m.room.avatar/": { - discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", - url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"} }, "m.room.power_levels/": { events: { From 607fd3808a8c1ebec7d77e17c7d086582a2d8b73 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 01:47:47 +1200 Subject: [PATCH 062/346] Fix bigint/number type in orm WHERE --- db/orm-defs.d.ts | 1 + db/orm.js | 4 ++-- m2d/actions/redact.js | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index e481f95..7484d76 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -114,3 +114,4 @@ export type AllKeys = U extends any ? keyof U : never export type PickTypeOf> = T extends { [k in K]?: any } ? T[K] : never export type Merge = {[x in AllKeys]: PickTypeOf} export type Nullable = {[k in keyof T]: T[k] | null} +export type Numberish = {[k in keyof T]: T[k] extends number ? (number | bigint) : T[k]} diff --git a/db/orm.js b/db/orm.js index 09e4bc7..601a7a0 100644 --- a/db/orm.js +++ b/db/orm.js @@ -8,7 +8,7 @@ const U = require("./orm-defs") * @template {keyof U.Models[Table]} Col * @param {Table} table * @param {Col[] | Col} cols - * @param {Partial} where + * @param {Partial>} where * @param {string} [e] */ function select(table, cols, where = {}, e = "") { @@ -108,7 +108,7 @@ class From { } /** - * @param {Partial} conditions + * @param {Partial>} conditions */ where(conditions) { const wheres = Object.entries(conditions).map(([col, value]) => { diff --git a/m2d/actions/redact.js b/m2d/actions/redact.js index 7569df4..ffbb261 100644 --- a/m2d/actions/redact.js +++ b/m2d/actions/redact.js @@ -25,7 +25,6 @@ async function deleteMessage(event) { */ async function removeReaction(event) { const hash = utils.getEventIDHash(event.redacts) - // TODO: this works but fix the type const row = from("reaction").join("message_channel", "message_id").select("channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get() if (!row) return await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji) From 71c553a9cf5ae8ffca22263c62fb7805fd4fcb8a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:05:40 +1200 Subject: [PATCH 063/346] Test cases for bridging author command metadata --- .../message-to-event.embeds.test.js | 35 +++++++++++++++++++ d2m/converters/message-to-event.js | 11 ++++-- test/ooye-test-data.sql | 3 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index 61a0822..05e3b5d 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -6,6 +6,13 @@ const Ty = require("../../types") test("message2event embeds: nothing but a field", async t => { const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {}) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", "m.mentions": {}, msgtype: "m.notice", @@ -143,6 +150,13 @@ test("message2event embeds: crazy html is all escaped", async t => { test("message2event embeds: title without url", async t => { const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", @@ -155,6 +169,13 @@ test("message2event embeds: title without url", async t => { test("message2event embeds: url without title", async t => { const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| I condone pirating music!", @@ -167,6 +188,13 @@ test("message2event embeds: url without title", async t => { test("message2event embeds: author without url", async t => { const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| ## Amanda\n| \n| I condone pirating music!", @@ -179,6 +207,13 @@ test("message2event embeds: author without url", async t => { test("message2event embeds: author url without name", async t => { const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general) t.deepEqual(events, [{ + $type: "m.room.message", + body: "> ↪️ @papiophidian: used `/stats`", + format: "org.matrix.custom.html", + formatted_body: "
↪️ @papiophidian used /stats
", + "m.mentions": {}, + msgtype: "m.text", + }, { $type: "m.room.message", msgtype: "m.notice", body: "| I condone pirating music!", diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index e6044b6..b86293e 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -32,7 +32,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) { /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() - const username = message.mentions.find(ment => ment.id === node.id)?.username || node.id + const interaction = message.interaction_metadata || message.interaction + const username = message.mentions.find(ment => ment.id === node.id)?.username + || (interaction?.user.id === node.id ? interaction.user.username : null) + || node.id if (mxid && useHTML) { return `@${username}` } else { @@ -229,9 +232,11 @@ async function messageToEvent(message, guild, options = {}, di) { }] } - if (message.type === DiscordTypes.MessageType.ChatInputCommand && message.interaction_metadata && "name" in message.interaction_metadata) { + const interaction = message.interaction_metadata || message.interaction + if (message.type === DiscordTypes.MessageType.ChatInputCommand && interaction && "name" in interaction) { // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top. - message.content = `> ↪️ <@${message.interaction_metadata.user.id}> used \`/${message.interaction_metadata.name}\`\n${message.content}` + if (message.content) message.content = `\n${message.content}` + message.content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${message.content}` } /** diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1fb9e24..370c6aa 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -23,7 +23,8 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES ('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), ('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), ('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'), -('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'); +('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'), +('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe'); INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); From 2c27879afb2a308439c66404a0a638215c9493b1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:09:27 +1200 Subject: [PATCH 064/346] Add another async/await, just to be safe --- d2m/discord-packets.js | 2 +- d2m/event-dispatcher.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 0981827..ed47fae 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -181,7 +181,7 @@ const utils = { } catch (e) { // Let OOYE try to handle errors too - eventDispatcher.onError(client, e, message) + await eventDispatcher.onError(client, e, message) } } } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 7f27b77..57cb72c 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -50,7 +50,7 @@ module.exports = { * @param {Error} e * @param {import("cloudstorm").IGatewayMessage} gatewayMessage */ - onError(client, e, gatewayMessage) { + async onError(client, e, gatewayMessage) { console.error("hit event-dispatcher's error handler with this exception:") console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later console.error(`while handling this ${gatewayMessage.t} gateway event:`) @@ -83,7 +83,7 @@ module.exports = { builder.addLine(`Error trace:\n${stackLines.join("\n")}`, `
Error trace
${stackLines.join("\n")}
`) } builder.addLine("", `
Original payload
${util.inspect(gatewayMessage.d, false, 4, false)}
`) - api.sendEvent(roomID, "m.room.message", { + await api.sendEvent(roomID, "m.room.message", { ...builder.get(), "moe.cadence.ooye.error": { source: "discord", From 5d91f999f2144a263101ef2e824b8375f27992bf Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 02:53:27 +1200 Subject: [PATCH 065/346] Make power migration testable --- matrix/power.js | 21 +++++++++++++-------- start.js | 3 ++- test/ooye-test-data.sql | 28 +++++++++++++++------------- test/test.js | 2 ++ 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/matrix/power.js b/matrix/power.js index 5dac550..cd2b8cb 100644 --- a/matrix/power.js +++ b/matrix/power.js @@ -1,7 +1,6 @@ // @ts-check const {db, from} = require("../passthrough") -const api = require("./api") const reg = require("./read-registration") const ks = require("./kstate") const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") @@ -11,13 +10,16 @@ for (const mxid of reg.ooye.invite) { db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") } -// Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. -const rows = from("member_cache").join("member_power", "mxid") - .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") - .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") - .all() +/** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */ +function _getAffectedRooms() { + return from("member_cache").join("member_power", "mxid") + .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") + .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") + .all() +} -;(async () => { +async function applyPower() { + const rows = _getAffectedRooms() for (const row of rows) { const kstate = await roomToKState(row.room_id) const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}}) @@ -26,4 +28,7 @@ const rows = from("member_cache").join("member_power", "mxid") // but we update it here anyway since the homeserver does not always deliver the event round-trip. db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid) } -})() +} + +module.exports._getAffectedRooms = _getAffectedRooms +module.exports.applyPower = applyPower diff --git a/start.js b/start.js index e218819..1ece1dd 100644 --- a/start.js +++ b/start.js @@ -25,13 +25,14 @@ const orm = sync.require("./db/orm") passthrough.from = orm.from passthrough.select = orm.select +const power = require("./matrix/power.js") sync.require("./m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) await discord.cloud.connect() console.log("Discord gateway started") - require("./matrix/power.js") + await power.applyPower() require("./stdin") })() diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 370c6aa..4666b4d 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -126,19 +126,21 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'), ('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'); -INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES -('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), -('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'), -('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL), -('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko'), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP'), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), -('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL); +INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), +('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc', 0), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL, 0), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL, 0), +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0), +('!BpMdOUkWWhFxmTrENV:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 100); INSERT INTO member_power (mxid, room_id, power_level) VALUES ('@test_auto_invite:example.org', '*', 100); diff --git a/test/test.js b/test/test.js index b5977f1..796ff68 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" +reg.ooye.invite = [] const sync = new HeatSync({watchFS: false}) @@ -116,6 +117,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/file.test") + require("../matrix/power.test") require("../matrix/read-registration.test") require("../matrix/txnid.test") require("../d2m/actions/create-room.test") From 5af7d6bf421995f74b8aff4d62262498167f7410 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 11:51:02 +1200 Subject: [PATCH 066/346] Add test for a generated embed on a reply --- d2m/converters/edit-to-changes.js | 3 +- d2m/converters/edit-to-changes.test.js | 41 +++++++ test/data.js | 149 +++++++++++++++++++++++++ test/ooye-test-data.sql | 14 ++- 4 files changed, 201 insertions(+), 6 deletions(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index c80bb49..6bb2210 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -24,14 +24,13 @@ function eventCanBeEdited(ev) { /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message - * IMPORTANT: This may not have all the normal fields! The API documentation doesn't provide possible types, just says it's all optional! - * Since I don't have a spec, I will have to capture some real traffic and add it as test cases... I hope they don't change anything later... * @param {import("discord-api-types/v10").APIGuild} guild * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ async function editToChanges(message, guild, api) { // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). // If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. + // Since an update in August 2024, the system always provides the full data of message updates. I'll leave in the old code since it won't cause problems. const isGeneratedEmbed = !("content" in message) diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index b00a2c4..1d8b730 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -270,3 +270,44 @@ test("edit2changes: generated embed", async t => { t.equal(senderMxid, "@_ooye_cadence:cadence.moe") t.equal(called, 1) }) + +test("edit2changes: generated embed on a reply", async t => { + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_on_reply, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", + newContent: { + $type: "m.room.message", + // Unfortunately the edited message doesn't include the message_reference field. Fine. Whatever. It looks normal if you're using a good client. + body: "> a Discord user: [Replied-to message content wasn't provided by Discord]" + + "\n\n* https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + format: "org.matrix.custom.html", + formatted_body: "
In reply to a Discord user
[Replied-to message content wasn't provided by Discord]
* https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + "m.mentions": {}, + "m.new_content": { + body: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + format: "org.matrix.custom.html", + formatted_body: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + "m.mentions": {}, + msgtype: "m.text", + }, + "m.relates_to": { + event_id: "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", + rel_type: "m.replace", + }, + msgtype: "m.text", + }, + }]) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "| ## Matrix - Decentralised and secure communication https://matrix.to/" + + "\n| \n| You're invited to talk on Matrix. If you don't already have a client this link will help you pick one, and join the conversation. If you already have one, this link will help you join the conversation", + format: "org.matrix.custom.html", + formatted_body: `

Matrix - Decentralised and secure communication` + + `

You're invited to talk on Matrix. If you don't already have a client this link will help you pick one, and join the conversation. If you already have one, this link will help you join the conversation

`, + "m.mentions": {} + }]) + t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet. + t.equal(senderMxid, "@_ooye_cadence:cadence.moe") +}) diff --git a/test/data.js b/test/data.js index 456033a..058a992 100644 --- a/test/data.js +++ b/test/data.js @@ -1990,6 +1990,91 @@ module.exports = { edited_timestamp: null, flags: 0, components: [] + }, + embed_will_be_generated_on_reply: { + type: 19, + content: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [ + { + type: "link", + url: "https://matrix.to/", + title: "Matrix - Decentralised and secure communication", + description: "You're invited to talk on Matrix. If you don't already have a client this link will help you pick one, and join the conversation. If you already have one, this link will help you join the conversation", + reference_id: "1278002262400176128", + thumbnail: { + url: "https://matrix.org/blog/img/matrix-logo.png", + proxy_url: "https://images-ext-1.discordapp.net/external/3yPmfN-_U_7Xn8hLSG77nY9IvdtITH0GPrB6OX3JjEI/https/matrix.org/blog/img/matrix-logo.png", + width: 800, + height: 400, + placeholder: "OAgOBIComJeHeId/dXgAAAAAAA==", + placeholder_version: 1 + } + } + ], + timestamp: "2024-08-27T14:44:43.490000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1278002262400176128", + channel_id: "1100319550446252084", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 0, + channel_id: "1100319550446252084", + message_id: "1278001833876525057", + guild_id: "1100319549670301727" + }, + position: 0, + referenced_message: { + type: 0, + content: "b", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2024-08-27T14:43:01.322000+00:00", + edited_timestamp: "2024-08-27T14:43:06.277000+00:00", + flags: 0, + components: [], + id: "1278001833876525057", + channel_id: "1100319550446252084", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false + } } }, pk_message: { @@ -3709,6 +3794,70 @@ module.exports = { ], guild_id: "112760669178241024", id: "1210387798297682020" + }, + embed_generated_on_reply: { + attachments: [], + author: { + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + avatar_decoration_data: null, + clan: null, + discriminator: "0", + global_name: "cadence", + id: "772659086046658620", + public_flags: 0, + username: "cadence.worm" + }, + channel_id: "1100319550446252084", + components: [], + content: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + edited_timestamp: null, + embeds: [ + { + description: "You're invited to talk on Matrix. If you don't already have a client this link will help you pick one, and join the conversation. If you already have one, this link will help you join the conversation", + reference_id: "1278002262400176128", + thumbnail: { + height: 400, + placeholder: "OAgOBIComJeHeId/dXgAAAAAAA==", + placeholder_version: 1, + proxy_url: "https://images-ext-1.discordapp.net/external/3yPmfN-_U_7Xn8hLSG77nY9IvdtITH0GPrB6OX3JjEI/https/matrix.org/blog/img/matrix-logo.png", + url: "https://matrix.org/blog/img/matrix-logo.png", + width: 800 + }, + title: "Matrix - Decentralised and secure communication", + type: "link", + url: "https://matrix.to/" + } + ], + flags: 0, + guild_id: "1100319549670301727", + id: "1278002262400176128", + member: { + avatar: null, + banner: null, + communication_disabled_until: null, + deaf: false, + flags: 0, + joined_at: "2023-04-25T07:17:03.696000+00:00", + mute: false, + nick: "worm", + pending: false, + premium_since: null, + roles: [] + }, + mention_everyone: false, + mention_roles: [], + mentions: [], + message_reference: { + channel_id: "1100319550446252084", + guild_id: "1100319549670301727", + message_id: "1278001833876525057", + type: 0 + }, + pinned: false, + position: 0, + timestamp: "2024-08-27T14:44:43.490000+00:00", + tts: false, + type: 19 } }, special_message: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 4666b4d..07be17f 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -24,13 +24,15 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES ('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), ('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'), ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'), -('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe'); +('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe'), +('772659086046658620', 'cadence', '_ooye_cadence', '@_ooye_cadence:cadence.moe'); INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES -('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL); +('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL), +('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); INSERT INTO message_channel (message_id, channel_id) VALUES ('1106366167788044450', '122155380120748034'), @@ -57,7 +59,9 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1207486471489986620', '1160894080998461480'), ('1210387798297682020', '112760669178241024'), ('1273204543739396116', '687028734322147344'), -('1273743950028607530', '1100319550446252084'); +('1273743950028607530', '1100319550446252084'), +('1278002262400176128', '1100319550446252084'), +('1278001833876525057', '1100319550446252084'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -94,7 +98,9 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0), ('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1), ('$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU', 'm.room.message', 'm.text', '1273204543739396116', 0, 0, 0), -('$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4', 'm.room.message', 'm.text', '1273743950028607530', 0, 0, 0); +('$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4', 'm.room.message', 'm.text', '1273743950028607530', 0, 0, 0), +('$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF', 'm.room.message', 'm.text', '1278002262400176128', 0, 0, 1), +('$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM', 'm.room.message', 'm.text', '1278001833876525057', 0, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), From a5518e562fda217dc2f898ff90e0b34999402f0f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 12:10:59 +1200 Subject: [PATCH 067/346] Fix reading property that was always missing --- m2d/converters/event-to-message.js | 7 +++--- m2d/converters/event-to-message.test.js | 30 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 2984e8e..6b762b4 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -206,11 +206,10 @@ function getCodeContent(preCode) { */ function convertEmoji(mxcUrl, nameForGuess, allowSpriteSheetIndicator, allowLink) { // Get the known emoji from the database. - let row - if (mxcUrl) row = select("emoji", ["emoji_id", "name", "animated"], {mxc_url: mxcUrl}).get() + if (mxcUrl) var row = select("emoji", ["emoji_id", "name", "animated"], {mxc_url: mxcUrl}).get() // Now we have to search all servers to see if we're able to send this emoji. if (row) { - const found = [...discord.guilds.values()].find(g => g.emojis.find(e => e.id === row.id)) + const found = [...discord.guilds.values()].find(g => g.emojis.find(e => e.id === row?.emoji_id)) if (!found) row = null } // Or, if we don't have an emoji right now, we search for the name instead. @@ -220,7 +219,7 @@ function convertEmoji(mxcUrl, nameForGuess, allowSpriteSheetIndicator, allowLink /** @type {{name: string, id: string, animated: number}[]} */ // @ts-ignore const emojis = guild.emojis - const found = emojis.find(e => e.id === row?.id || e.name?.toLowerCase() === nameForGuessLower) + const found = emojis.find(e => e.name?.toLowerCase() === nameForGuessLower) if (found) { row = { animated: found.animated, diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index b404e59..a802d28 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -3794,6 +3794,36 @@ test("event2message: static emojis work", async t => { ) }) +test("event2message: emojis in other servers are reused if they have the same title text", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: ":hippo:", + format: "org.matrix.custom.html", + formatted_body: '\":hippo:\"' + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!CzvdIdUQXgUjDVKxeU:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "<:hippo:230201364309868544>", + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: animated emojis work", async t => { t.deepEqual( await eventToMessage({ From 9e9f4819c71d1fae3e1f32475ec102b0220d6f45 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 12:51:28 +1200 Subject: [PATCH 068/346] Make sure url starts with http:// or https:// --- scripts/seed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/seed.js b/scripts/seed.js index 2ab3e02..a3ffbbe 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -59,6 +59,7 @@ async function uploadAutoEmoji(guild, name, filename) { assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash") const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString() assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") + assert.match(reg.url, /^https?:/, "url must start with http:// or https://") console.log("✅ Configuration looks good...") // database ddl... From 65de8aaf98f8f5e1d3e7d4257f6fedbcb0892355 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 13:01:59 +1200 Subject: [PATCH 069/346] Code coverage --- d2m/converters/message-to-event.test.js | 38 +++++++++++++++++ m2d/converters/event-to-message.js | 2 +- m2d/converters/event-to-message.test.js | 32 ++++++++++++++ test/data.js | 57 +++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index fbfe06d..02fb4bb 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -64,6 +64,44 @@ test("message2event: simple user mention", async t => { test("message2event: simple room mention", async t => { let called = 0 const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {}, { + api: { + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, + async getJoinedMembers(roomID) { + called++ + t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") + return { + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} + } + } + } + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "#worm-farm", + format: "org.matrix.custom.html", + formatted_body: '#worm-farm' + }]) + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") +}) + +test("message2event: nicked room mention", async t => { + let called = 0 + const events = await messageToEvent(data.message.nicked_room_mention, data.guild.general, {}, { api: { async getStateEvent(roomID, type, key) { called++ diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 6b762b4..5234755 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -685,7 +685,7 @@ async function eventToMessage(event, guild, di) { let preNode if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) { if (preNode.firstChild?.nodeName === "CODE") { - const ext = (preNode.firstChild.className.match(/language-(\S+)/) || [null, "txt"])[1] + const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt" const filename = `inline_code.${ext}` // Build the replacement node const replacementCode = doc.createElement("code") diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index a802d28..d6a20cd 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -772,6 +772,38 @@ test("event2message: code blocks are uploaded as attachments instead if they con ) }) +test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (default to txt file extension)", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: 'So if you run code like this
System.out.println("```");
it should print a markdown formatted code block' + }, + event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", + room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "So if you run code like this `[inline_code.txt]` it should print a markdown formatted code block", + attachments: [{id: "0", filename: "inline_code.txt"}], + pendingFiles: [{name: "inline_code.txt", buffer: Buffer.from('System.out.println("```");')}], + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: characters are encoded properly in code blocks", async t => { t.deepEqual( await eventToMessage({ diff --git a/test/data.js b/test/data.js index 058a992..18aaa1a 100644 --- a/test/data.js +++ b/test/data.js @@ -450,6 +450,63 @@ module.exports = { components: [] }, simple_room_mention: { + type: 0, + tts: false, + timestamp: "2023-07-10T20:04:25.939000+00:00", + referenced_message: null, + pinned: false, + nonce: "1128054139385806848", + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", "118924814567211009", + "204427286542417920", "199995902742626304", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1123518980456452097", + "1040735082610167858", "695946570482450442", + "1123460940935991296", "849737964090556488" + ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2015-11-11T09:55:40.321000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1128054143064494233", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "<#1100319550446252084>", + components: [], + channel_id: "266767590641238027", + author: { + username: "kumaccino", + public_flags: 128, + id: "113340068197859328", + global_name: "kumaccino", + discriminator: "0", + avatar_decoration: null, + avatar: "b48302623a12bc7c59a71328f72ccb39" + }, + attachments: [], + guild_id: "112760669178241024" + }, + nicked_room_mention: { type: 0, tts: false, timestamp: "2023-07-10T20:04:25.939000+00:00", From ee266f844a673f47e6328dd1022e5ee18e107de6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Aug 2024 17:18:50 +1200 Subject: [PATCH 070/346] Move reaction_part to new events when possible --- d2m/actions/edit-message.js | 2 +- d2m/converters/edit-to-changes.js | 12 +++++++++++- d2m/converters/edit-to-changes.test.js | 20 +++++++++++++++++--- test/ooye-test-data.sql | 3 +++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index d52fcbd..d85f925 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -53,7 +53,7 @@ async function editMessage(message, guild, row) { const sendNewEventParts = new Set() for (const promotion of promotions) { if ("eventID" in promotion) { - db.prepare(`UPDATE event_message SET ${promotion.column} = 0 WHERE event_id = ?`).run(promotion.eventID) + db.prepare(`UPDATE event_message SET ${promotion.column} = ? WHERE event_id = ?`).run(promotion.value ?? 0, promotion.eventID) } else if ("nextEvent" in promotion) { sendNewEventParts.add(promotion.column) } diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 6bb2210..f93c510 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -122,7 +122,7 @@ async function editToChanges(message, guild, api) { eventsToReplace = eventsToReplace.filter(eventCanBeEdited) // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. - /** @type {({column: string, eventID: string} | {column: string, nextEvent: true})[]} */ + /** @type {({column: string, eventID: string, value?: number} | {column: string, nextEvent: true})[]} */ const promotions = [] for (const column of ["part", "reaction_part"]) { const candidatesForParts = unchangedEvents.concat(eventsToReplace) @@ -142,6 +142,16 @@ async function editToChanges(message, guild, api) { promotions.push({column, nextEvent: true}) } } + // If adding events, try to keep reactions attached to the bottom of the group (unless reactions have already been added) + if (eventsToSend.length && !promotions.length) { + const existingReaction = select("reaction", "message_id", {message_id: message.id}).pluck().get() + if (!existingReaction) { + const existingPartZero = candidatesForParts.find(p => p.old.reaction_part === 0) + assert(existingPartZero) // will exist because a reaction_part=0 always exists and no events are being removed + promotions.push({column: "reaction_part", eventID: existingPartZero.old.event_id, value: 1}) // update the current reaction_part to 1 + promotions.push({column: "reaction_part", nextEvent: true}) // the newly created event will have reaction_part = 0 + } + } } // Removing unnecessary properties before returning diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 1d8b730..effb777 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -109,7 +109,7 @@ test("edit2changes: change file type", async t => { t.deepEqual(promotions, [{column: "part", nextEvent: true}, {column: "reaction_part", nextEvent: true}]) }) -test("edit2changes: add caption back to that image", async t => { +test("edit2changes: add caption back to that image (due to it having a reaction, the reaction_part will not be moved)", async t => { const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, [{ @@ -266,7 +266,14 @@ test("edit2changes: generated embed", async t => { + `
  • Both players present their best five-or-less-card pok...
  • `, "m.mentions": {} }]) - t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet. + t.deepEqual(promotions, [{ + "column": "reaction_part", + "eventID": "$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0", + "value": 1, + }, { + "column": "reaction_part", + "nextEvent": true, + }]) t.equal(senderMxid, "@_ooye_cadence:cadence.moe") t.equal(called, 1) }) @@ -308,6 +315,13 @@ test("edit2changes: generated embed on a reply", async t => { + `

    You're invited to talk on Matrix. If you don't already have a client this link will help you pick one, and join the conversation. If you already have one, this link will help you join the conversation

    `, "m.mentions": {} }]) - t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet. + t.deepEqual(promotions, [{ + "column": "reaction_part", + "eventID": "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", + "value": 1, + }, { + "column": "reaction_part", + "nextEvent": true, + }]) t.equal(senderMxid, "@_ooye_cadence:cadence.moe") }) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 07be17f..2c23561 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -148,6 +148,9 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) V ('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0), ('!BpMdOUkWWhFxmTrENV:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 100); +INSERT INTO reaction (hashed_event_id, message_id, encoded_emoji) VALUES +(5162930312280790092, '1141501302736695317', '%F0%9F%90%88'); + INSERT INTO member_power (mxid, room_id, power_level) VALUES ('@test_auto_invite:example.org', '*', 100); From 7afe3e75910f06b532b7d54427a8e39712f1ada9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 31 Aug 2024 16:18:33 +1200 Subject: [PATCH 071/346] Copy power levels from spaces to new rooms --- d2m/actions/create-room.js | 15 ++++++-- d2m/actions/create-room.test.js | 60 +++++++++++++++++++++++++---- matrix/power.test.js | 12 ++++++ test/data.js | 67 +++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 matrix/power.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 2dd62e0..a6b61cd 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -2,6 +2,7 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") +const Ty = require("../../types") const reg = require("../../matrix/read-registration") const passthrough = require("../../passthrough") @@ -89,8 +90,9 @@ function convertNameAndTopic(channel, guild, customName) { * Async because it may create the guild and/or upload the guild icon to mxc. * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel * @param {DiscordTypes.APIGuild} guild + * @param {{api: {getStateEvent: typeof api.getStateEvent}}} di simple-as-nails dependency injection for the matrix API */ -async function channelToKState(channel, guild) { +async function channelToKState(channel, guild, di) { // @ts-ignore const parentChannel = discord.channels.get(channel.parent_id) /** Used for membership/permission checks. */ @@ -142,6 +144,11 @@ async function channelToKState(channel, guild) { const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all() + const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {}) + + /** @type {Ty.Event.M_Power_Levels} */ + const spacePowerEvent = await di.api.getStateEvent(guildSpaceID, "m.room.power_levels", "") + const spacePower = spacePowerEvent.users const channelKState = { "m.room.name/": {name: convertedName}, @@ -162,7 +169,7 @@ async function channelToKState(channel, guild) { notifications: { room: everyoneCanMentionEveryone ? 0 : 20 }, - users: globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {}) + users: {...spacePower, ...globalAdminPower} }, "chat.schildi.hide_ui/read_receipts": { hidden: true @@ -311,7 +318,7 @@ async function _syncRoom(channelID, shouldActuallySync) { if (!existing) { const creation = (async () => { - const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild) + const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild, {api}) const roomID = await createRoom(channel, guild, spaceID, channelKState, privacyLevel) inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row return roomID @@ -328,7 +335,7 @@ async function _syncRoom(channelID, shouldActuallySync) { console.log(`[room sync] to matrix: ${channel.name}`) - const {spaceID, channelKState} = await channelToKState(channel, guild) // calling this in both branches because we don't want to calculate this if not syncing + const {spaceID, channelKState} = await channelToKState(channel, guild, {api}) // calling this in both branches because we don't want to calculate this if not syncing // sync channel state to room const roomKState = await roomToKState(roomID) diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index 2a76e2e..d4391a9 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -9,45 +9,89 @@ const testData = require("../../test/data") const passthrough = require("../../passthrough") const {db} = passthrough + test("channel2room: discoverable privacy room", async t => { + let called = 0 + async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return {users: {"@example:matrix.org": 50}} + } db.prepare("UPDATE guild_space SET privacy_level = 2").run() t.deepEqual( - kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), + kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)), Object.assign({}, testData.room.general, { "m.room.guest_access/": {guest_access: "forbidden"}, "m.room.join_rules/": {join_rule: "public"}, - "m.room.history_visibility/": {history_visibility: "world_readable"} + "m.room.history_visibility/": {history_visibility: "world_readable"}, + "m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"]) }) ) + t.equal(called, 1) }) test("channel2room: linkable privacy room", async t => { + let called = 0 + async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return {users: {"@example:matrix.org": 50}} + } db.prepare("UPDATE guild_space SET privacy_level = 1").run() t.deepEqual( - kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), + kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)), Object.assign({}, testData.room.general, { "m.room.guest_access/": {guest_access: "forbidden"}, - "m.room.join_rules/": {join_rule: "public"} + "m.room.join_rules/": {join_rule: "public"}, + "m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"]) }) ) + t.equal(called, 1) }) test("channel2room: invite-only privacy room", async t => { + let called = 0 + async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return {users: {"@example:matrix.org": 50}} + } db.prepare("UPDATE guild_space SET privacy_level = 0").run() t.deepEqual( - kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), - testData.room.general + kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)), + Object.assign({}, testData.room.general, { + "m.room.power_levels/": mixin({users: {"@example:matrix.org": 50}}, testData.room.general["m.room.power_levels/"]) + }) ) + t.equal(called, 1) }) test("channel2room: room where limited people can mention everyone", async t => { + let called = 0 + async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return {users: {"@example:matrix.org": 50}} + } const limitedGuild = mixin({}, testData.guild.general) limitedGuild.roles[0].permissions = (BigInt(limitedGuild.roles[0].permissions) - 131072n).toString() - const limitedRoom = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}}}) + const limitedRoom = mixin({}, testData.room.general, {"m.room.power_levels/": { + notifications: {room: 20}, + users: {"@example:matrix.org": 50} + }}) t.deepEqual( - kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild).then(x => x.channelKState)), + kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild, {api: {getStateEvent}}).then(x => x.channelKState)), limitedRoom ) + t.equal(called, 1) }) test("convertNameAndTopic: custom name and topic", t => { diff --git a/matrix/power.test.js b/matrix/power.test.js new file mode 100644 index 0000000..5423c4f --- /dev/null +++ b/matrix/power.test.js @@ -0,0 +1,12 @@ +// @ts-check + +const {test} = require("supertape") +const power = require("./power") + +test("power: get affected rooms", t => { + t.deepEqual(power._getAffectedRooms(), [{ + mxid: "@test_auto_invite:example.org", + power_level: 100, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + }]) +}) diff --git a/test/data.js b/test/data.js index 18aaa1a..ffe2cc9 100644 --- a/test/data.js +++ b/test/data.js @@ -4065,6 +4065,73 @@ module.exports = { guild_id: "372271956562542592" } }, + reply_with_missing_referenced_message: { + type: 19, + content: "there must have been 2 different thumbnails used - the other one was this I think: https://lostmediawiki.com/w/images/6/6d/YOUTUBE_HACKED%21.jpg", + mentions: [ + { + id: "112816036671184896", + username: "accavish", + avatar: "0efadd2b09568c89e81f47d321c1db9f", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "Yuv.yuv", + avatar_decoration_data: null, + banner_color: null, + clan: null + } + ], + mention_roles: [], + attachments: [], + embeds: [ + { + type: "image", + url: "https://lostmediawiki.com/w/images/6/6d/YOUTUBE_HACKED%21.jpg", + reference_id: "1277081824962809919", + thumbnail: { + url: "https://lostmediawiki.com/w/images/6/6d/YOUTUBE_HACKED%21.jpg", + proxy_url: "https://images-ext-1.discordapp.net/external/DqHSi6Hsvkn8CeYqcpNsgcPun_yykRNdKzyPTLxkrJ8/https/lostmediawiki.com/w/images/6/6d/YOUTUBE_HACKED%2521.jpg", + width: 898, + height: 459, + placeholder: "MPcFLIoEgndse3pydlUHeZSQZw==", + placeholder_version: 1 + } + } + ], + timestamp: "2024-08-25T01:47:14.104000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1277081824962809919", + channel_id: "112760669178241024", + author: { + id: "628334893109215263", + username: "thecracksoverhead", + avatar: "e4eaad082f5ff0359cafa8d3ad5ddd4f", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "jdl", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 0, + channel_id: "112760669178241024", + message_id: "1277081326008143934", + guild_id: "112760669178241024" + }, + position: 0 + } }, interaction_message: { thinking_interaction_without_bot_user: { From a2e3f06e8e65949cb52dd8049d364ae6b2a80d40 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 31 Aug 2024 17:02:46 +1200 Subject: [PATCH 072/346] Unbridge rooms when their channel is deleted --- d2m/actions/create-room.js | 20 ++++++++++++++------ d2m/discord-packets.js | 3 +++ d2m/event-dispatcher.js | 13 +++++++++++++ discord/register-interactions.js | 2 +- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index a6b61cd..51eef6c 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -370,11 +370,16 @@ async function _unbridgeRoom(channelID) { /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) assert.ok(channel) - return unbridgeDeletedChannel(channel.id, channel.guild_id) + assert.ok(channel.guild_id) + return unbridgeDeletedChannel(channel, channel.guild_id) } -async function unbridgeDeletedChannel(channelID, guildID) { - const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() +/** + * @param {DiscordTypes.APIGuildChannel} channel + * @param {string} guildID + */ +async function unbridgeDeletedChannel(channel, guildID) { + const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() assert.ok(roomID) const spaceID = select("guild_space", "space_id", {guild_id: guildID}).pluck().get() assert.ok(spaceID) @@ -384,7 +389,11 @@ async function unbridgeDeletedChannel(channelID, guildID) { await api.sendState(spaceID, "m.space.child", roomID, {}) // remove declaration that the room is bridged - await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channelID}`, {}) + await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {}) + if ("topic" in channel) { + // previously the Matrix topic would say the channel ID. we should remove that + await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""}) + } // send a notification in the room await api.sendEvent(roomID, "m.room.message", { @@ -396,8 +405,7 @@ async function unbridgeDeletedChannel(channelID, guildID) { await api.leaveRoom(roomID) // delete room from database - const {changes} = db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channelID) - assert.equal(changes, 1) + db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) } /** diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index ed47fae..f619f2b 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -147,6 +147,9 @@ const utils = { } else if (message.t === "CHANNEL_PINS_UPDATE") { await eventDispatcher.onChannelPinsUpdate(client, message.d) + } else if (message.t === "CHANNEL_DELETE") { + await eventDispatcher.onChannelDelete(client, message.d) + } else if (message.t === "THREAD_CREATE") { // @ts-ignore await eventDispatcher.onThreadCreate(client, message.d) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 57cb72c..235248c 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -230,6 +230,19 @@ module.exports = { await updatePins.updatePins(data.channel_id, roomID, convertedTimestamp) }, + /** + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayChannelDeleteDispatchData} channel + */ + async onChannelDelete(client, channel) { + const guildID = channel["guild_id"] + if (!guildID) return // channel must have been a DM channel or something + const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + if (!roomID) return // channel wasn't being bridged in the first place + // @ts-ignore + await createRoom.unbridgeDeletedChannel(channel, guildID) + }, + /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageCreateDispatchData} message diff --git a/discord/register-interactions.js b/discord/register-interactions.js index a6e4332..553c6db 100644 --- a/discord/register-interactions.js +++ b/discord/register-interactions.js @@ -17,7 +17,7 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Permissions", contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message, - default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers) + default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles) }, { name: "invite", contexts: [DiscordTypes.InteractionContextType.Guild], From 62edffbae0cd3aa40ee4cdfce9501c671af134cb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 31 Aug 2024 17:44:56 +1200 Subject: [PATCH 073/346] Add reaction viewer to right click menu --- d2m/actions/remove-reaction.js | 11 +----- discord/interactions/reactions.js | 58 +++++++++++++++++++++++++++++++ discord/register-interactions.js | 7 ++++ matrix/api.js | 21 +++++++++++ 4 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 discord/interactions/reactions.js diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index 95fc0aa..d991f08 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -23,16 +23,7 @@ async function removeSomeReactions(data) { const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get() if (!eventIDForMessage) return - /** @type {Ty.Event.Outer[]} */ - let reactions = [] - /** @type {string | undefined} */ - let nextBatch = undefined - do { - /** @type {Ty.Pagination>} */ - const res = await api.getRelations(roomID, eventIDForMessage, {from: nextBatch}, "m.annotation") - reactions = reactions.concat(res.chunk) - nextBatch = res.next_batch - } while (nextBatch) + const reactions = await api.getFullRelations(roomID, eventIDForMessage, "m.annotation") // Run the proper strategy and any strategy-specific database changes const removals = await diff --git a/discord/interactions/reactions.js b/discord/interactions/reactions.js new file mode 100644 index 0000000..67f3a68 --- /dev/null +++ b/discord/interactions/reactions.js @@ -0,0 +1,58 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db, select, from} = require("../../passthrough") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../../m2d/converters/utils")} */ +const utils = sync.require("../../m2d/converters/utils") + +/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact({id, token, data}) { + const row = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") + .select("event_id", "room_id").where({message_id: data.target_id}).get() + if (!row) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This message hasn't been bridged to Matrix.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") + + /** @type {Map} */ + const inverted = new Map() + for (const reaction of reactions) { + if (utils.eventSenderIsFromDiscord(reaction.sender)) continue + const key = reaction.content["m.relates_to"].key + const displayname = select("member_cache", "displayname", {mxid: reaction.sender, room_id: row.room_id}).pluck().get() || reaction.sender + if (!inverted.has(key)) inverted.set(key, []) + // @ts-ignore + inverted.get(key).push(displayname) + } + + if (inverted.size === 0) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Nobody from Matrix reacted to this message.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) +} + +module.exports.interact = interact diff --git a/discord/register-interactions.js b/discord/register-interactions.js index 553c6db..026ac7a 100644 --- a/discord/register-interactions.js +++ b/discord/register-interactions.js @@ -8,6 +8,7 @@ const matrixInfo = sync.require("./interactions/matrix-info.js") const invite = sync.require("./interactions/invite.js") const permissions = sync.require("./interactions/permissions.js") const bridge = sync.require("./interactions/bridge.js") +const reactions = sync.require("./interactions/reactions.js") discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Matrix info", @@ -18,6 +19,10 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message, default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles) +}, { + name: "Reactions", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.Message }, { name: "invite", contexts: [DiscordTypes.InteractionContextType.Guild], @@ -63,6 +68,8 @@ async function dispatchInteraction(interaction) { await permissions.interactEdit(interaction) } else if (interactionId === "bridge") { await bridge.interact(interaction) + } else if (interactionId === "Reactions") { + await reactions.interact(interaction) } else { throw new Error(`Unknown interaction ${interactionId}`) } diff --git a/matrix/api.js b/matrix/api.js index e94a1a5..0d61207 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -169,6 +169,26 @@ function getRelations(roomID, eventID, pagination, relType) { return mreq.mreq("GET", path) } +/** + * Like `getRelations` but collects and filters all pages for you. + * @param {string} roomID + * @param {string} eventID + * @param {string?} [relType] type of relations to filter, e.g. "m.annotation" for reactions + */ +async function getFullRelations(roomID, eventID, relType) { + /** @type {Ty.Event.Outer[]} */ + let reactions = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.Pagination>} */ + const res = await getRelations(roomID, eventID, {from: nextBatch}, relType) + reactions = reactions.concat(res.chunk) + nextBatch = res.next_batch + } while (nextBatch) + return reactions +} + /** * @param {string} roomID * @param {string} type @@ -289,6 +309,7 @@ module.exports.getJoinedMembers = getJoinedMembers module.exports.getHierarchy = getHierarchy module.exports.getFullHierarchy = getFullHierarchy module.exports.getRelations = getRelations +module.exports.getFullRelations = getFullRelations module.exports.sendState = sendState module.exports.sendEvent = sendEvent module.exports.redactEvent = redactEvent From a218774c13b9f879100d48889c427e8ce2af5e28 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 1 Sep 2024 15:14:26 +1200 Subject: [PATCH 074/346] Only moderators can change room avatar --- d2m/actions/create-room.js | 3 --- test/data.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 51eef6c..bdb2cf2 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -163,9 +163,6 @@ async function channelToKState(channel, guild, di) { /** @type {{join_rule: string, [x: string]: any}} */ "m.room.join_rules/": join_rules, "m.room.power_levels/": { - events: { - "m.room.avatar": 0 - }, notifications: { room: everyoneCanMentionEveryone ? 0 : 20 }, diff --git a/test/data.js b/test/data.js index ffe2cc9..c8217c2 100644 --- a/test/data.js +++ b/test/data.js @@ -41,9 +41,6 @@ module.exports = { url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"} }, "m.room.power_levels/": { - events: { - "m.room.avatar": 0 - }, users: { "@test_auto_invite:example.org": 100 }, From c689ec00b740a4e5284c585fb9cbcbef03b2fbb7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 1 Sep 2024 15:26:25 +1200 Subject: [PATCH 075/346] Fix trying to access database before migrations ran --- matrix/power.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix/power.js b/matrix/power.js index cd2b8cb..7ffa425 100644 --- a/matrix/power.js +++ b/matrix/power.js @@ -5,11 +5,6 @@ const reg = require("./read-registration") const ks = require("./kstate") const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") -// Migrate reg.ooye.invite setting to database -for (const mxid of reg.ooye.invite) { - db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") -} - /** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */ function _getAffectedRooms() { return from("member_cache").join("member_power", "mxid") @@ -19,6 +14,11 @@ function _getAffectedRooms() { } async function applyPower() { + // Migrate reg.ooye.invite setting to database + for (const mxid of reg.ooye.invite) { + db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") + } + const rows = _getAffectedRooms() for (const row of rows) { const kstate = await roomToKState(row.room_id) From 1037fe0276e696f926ce35a85d4487130078a56a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 1 Sep 2024 15:32:41 +1200 Subject: [PATCH 076/346] Only apply power to bridged rooms --- matrix/power.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix/power.js b/matrix/power.js index 7ffa425..a6fd063 100644 --- a/matrix/power.js +++ b/matrix/power.js @@ -7,7 +7,9 @@ const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-roo /** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */ function _getAffectedRooms() { - return from("member_cache").join("member_power", "mxid") + return from("member_cache") + .join("member_power", "mxid") + .join("channel_room", "room_id") // only include rooms that are bridged .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") .all() From e0bb19bfaba3d8eff463a7783d0bcccced3b8374 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Sep 2024 00:15:00 +1200 Subject: [PATCH 077/346] /bridge: search substrings rather than startswith --- discord/interactions/bridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions/bridge.js b/discord/interactions/bridge.js index b2d1ac0..1fbc57e 100644 --- a/discord/interactions/bridge.js +++ b/discord/interactions/bridge.js @@ -70,7 +70,7 @@ async function interactAutocomplete({id, token, data, guild_id}) { let rooms = await getCachedHierarchy(spaceID) // @ts-ignore - rooms = rooms.filter(r => r.name.startsWith(data.options[0].value)) + rooms = rooms.filter(r => r.name.includes(data.options[0].value)) await discord.snow.interaction.createInteractionResponse(id, token, { type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult, From 37f3a59d8ee781252678a40f1e69c3b592e337be Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 5 Sep 2024 15:36:43 +1200 Subject: [PATCH 078/346] Interactive initial setup --- d2m/actions/create-room.js | 4 +- d2m/actions/create-space.js | 4 +- d2m/actions/register-pk-user.js | 2 +- d2m/actions/register-user.js | 2 +- d2m/converters/message-to-event.js | 2 +- d2m/converters/thread-to-announcement.js | 3 +- d2m/converters/user-to-mxid.js | 4 +- d2m/converters/user-to-mxid.test.js | 2 +- discord/discord-command-handler.js | 2 +- m2d/converters/utils.js | 2 +- m2d/event-dispatcher.js | 3 +- matrix/appservice.js | 2 +- matrix/matrix-command-handler.js | 2 +- matrix/mreq.js | 5 +- matrix/power.js | 2 +- matrix/read-registration.js | 85 ++++++++++++++++++++++-- matrix/read-registration.test.js | 2 +- package-lock.json | 41 ++++++++++++ package.json | 2 + readme.md | 6 +- scripts/migrate-from-old-bridge.js | 3 +- scripts/seed.js | 82 +++++++++++++++++++++-- test/test.js | 4 +- types.d.ts | 26 ++++++++ 24 files changed, 249 insertions(+), 43 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index bdb2cf2..509d0ff 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -3,7 +3,7 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough @@ -372,7 +372,7 @@ async function _unbridgeRoom(channelID) { } /** - * @param {DiscordTypes.APIGuildChannel} channel + * @param {{id: string, topic?: string?}} channel * @param {string} guildID */ async function unbridgeDeletedChannel(channel, guildID) { diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index a9b8448..ec1677e 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -4,7 +4,7 @@ const assert = require("assert").strict const {isDeepStrictEqual} = require("util") const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough @@ -192,7 +192,7 @@ async function syncSpaceFully(guildID) { if (discord.channels.has(channelID)) { await createRoom.syncRoom(channelID) } else { - await createRoom.unbridgeDeletedChannel(channelID, guildID) + await createRoom.unbridgeDeletedChannel({id: channelID}, guildID) } } diff --git a/d2m/actions/register-pk-user.js b/d2m/actions/register-pk-user.js index 2ead4ff..411fcf8 100644 --- a/d2m/actions/register-pk-user.js +++ b/d2m/actions/register-pk-user.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert") -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") const fetch = require("node-fetch").default diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 6bd4661..b914d41 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert").strict -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const DiscordTypes = require("discord-api-types/v10") const mixin = require("@cloudrac3r/mixin-deep") diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index b86293e..4614082 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -18,7 +18,7 @@ const lottie = sync.require("../actions/lottie") const mxUtils = sync.require("../../m2d/converters/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) diff --git a/d2m/converters/thread-to-announcement.js b/d2m/converters/thread-to-announcement.js index 9d9302a..11a067f 100644 --- a/d2m/converters/thread-to-announcement.js +++ b/d2m/converters/thread-to-announcement.js @@ -4,10 +4,9 @@ const assert = require("assert").strict const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../../matrix/read-registration")} */ -const reg = sync.require("../../matrix/read-registration.js") /** @type {import("../../m2d/converters/utils")} */ const mxUtils = sync.require("../../m2d/converters/utils") +const {reg} = require("../../matrix/read-registration.js") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 97b655e..a619b36 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert") -const registration = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {select} = passthrough @@ -26,7 +26,7 @@ function downcaseUsername(user) { // remove leading and trailing dashes and underscores... .replace(/(?:^[_-]*|[_-]*$)/g, "") // If requested, also make the Discord user ID part of the username - if (registration.ooye.include_user_id_in_mxid) { + if (reg.ooye.include_user_id_in_mxid) { downcased = user.id + "_" + downcased } // The new length must be at least 2 characters (in other words, it should have some content) diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 4576205..5787b64 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -46,7 +46,7 @@ test("user2name: works on special user", t => { }) test("user2name: includes ID if requested in config", t => { - const reg = require("../../matrix/read-registration") + const {reg} = require("../../matrix/read-registration") reg.ooye.include_user_id_in_mxid = true t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles") t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") diff --git a/discord/discord-command-handler.js b/discord/discord-command-handler.js index f69346f..a434bf9 100644 --- a/discord/discord-command-handler.js +++ b/discord/discord-command-handler.js @@ -3,7 +3,7 @@ const assert = require("assert").strict const util = require("util") const DiscordTypes = require("discord-api-types/v10") -const reg = require("../matrix/read-registration") +const {reg} = require("../matrix/read-registration") const {addbot} = require("../addbot") const {discord, sync, db, select} = require("../passthrough") diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 9d7a5e2..437b5c0 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -1,6 +1,6 @@ // @ts-check -const reg = require("../../matrix/read-registration") +const {reg} = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) const assert = require("assert").strict /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 7aa3b70..301dcc4 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -20,8 +20,7 @@ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") const utils = sync.require("./converters/utils") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") -/** @type {import("../matrix/read-registration")}) */ -const reg = sync.require("../matrix/read-registration") +const {reg} = require("../matrix/read-registration") let lastReportedEvent = 0 diff --git a/matrix/appservice.js b/matrix/appservice.js index b239084..67f16ee 100644 --- a/matrix/appservice.js +++ b/matrix/appservice.js @@ -1,6 +1,6 @@ // @ts-check -const reg = require("../matrix/read-registration") +const {reg} = require("../matrix/read-registration") const {AppService} = require("@cloudrac3r/in-your-element") const as = new AppService(reg) as.listen() diff --git a/matrix/matrix-command-handler.js b/matrix/matrix-command-handler.js index 36a7649..7ad84f5 100644 --- a/matrix/matrix-command-handler.js +++ b/matrix/matrix-command-handler.js @@ -14,7 +14,7 @@ const mxUtils = sync.require("../m2d/converters/utils") const dUtils = sync.require("../discord/utils") /** @type {import("./kstate")} */ const ks = sync.require("./kstate") -const reg = require("./read-registration") +const {reg} = require("./read-registration") const PREFIXES = ["//", "/"] diff --git a/matrix/mreq.js b/matrix/mreq.js index d6a21ae..5e9abc4 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -5,10 +5,7 @@ const mixin = require("@cloudrac3r/mixin-deep") const stream = require("stream") const getStream = require("get-stream") -const passthrough = require("../passthrough") -const { sync } = passthrough -/** @type {import("./read-registration")} */ -const reg = sync.require("./read-registration.js") +const {reg} = require("./read-registration.js") const baseUrl = `${reg.ooye.server_origin}/_matrix` diff --git a/matrix/power.js b/matrix/power.js index a6fd063..3e613dd 100644 --- a/matrix/power.js +++ b/matrix/power.js @@ -1,7 +1,7 @@ // @ts-check const {db, from} = require("../passthrough") -const reg = require("./read-registration") +const {reg} = require("./read-registration") const ks = require("./kstate") const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") diff --git a/matrix/read-registration.js b/matrix/read-registration.js index c431907..7172273 100644 --- a/matrix/read-registration.js +++ b/matrix/read-registration.js @@ -1,14 +1,85 @@ // @ts-check const fs = require("fs") +const crypto = require("crypto") const assert = require("assert").strict +const path = require("path") const yaml = require("js-yaml") -/** @ts-ignore @type {import("../types").AppServiceRegistrationConfig} */ -const reg = yaml.load(fs.readFileSync("registration.yaml", "utf8")) -reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line -assert(reg.ooye.max_file_size) -assert(reg.ooye.namespace_prefix) -assert(reg.ooye.server_name) +const registrationFilePath = path.join(process.cwd(), "registration.yaml") -module.exports = reg +/** @param {import("../types").AppServiceRegistrationConfig} reg */ +function checkRegistration(reg) { + reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line + assert(reg.ooye?.max_file_size) + assert(reg.ooye?.namespace_prefix) + assert(reg.ooye?.server_name) + assert(reg.sender_localpart?.startsWith(reg.ooye.namespace_prefix), "appservice's localpart must be in the namespace it controls") + assert(reg.ooye?.server_origin.match(/^https?:\/\//), "server origin must start with http or https") + assert.notEqual(reg.ooye?.server_origin.slice(-1), "/", "server origin must not end in slash") + assert.match(reg.url, /^https?:/, "url must start with http:// or https://") +} + +/** @param {import("../types").AppServiceRegistrationConfig} reg */ +function writeRegistration(reg) { + fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2)) +} + +/** @returns {import("../types").InitialAppServiceRegistrationConfig} reg */ +function getTemplateRegistration() { + return { + id: crypto.randomBytes(16).toString("hex"), + as_token: crypto.randomBytes(16).toString("hex"), + hs_token: crypto.randomBytes(16).toString("hex"), + namespaces: { + users: [{ + exclusive: true, + regex: "@_ooye_.*:cadence.moe" + }], + aliases: [{ + exclusive: true, + regex: "#_ooye_.*:cadence.moe" + }] + }, + protocols: [ + "discord" + ], + sender_localpart: "_ooye_bot", + rate_limited: false, + ooye: { + namespace_prefix: "_ooye_", + max_file_size: 5000000, + content_length_workaround: false, + include_user_id_in_mxid: false, + invite: [] + } + } +} + +function readRegistration() { + /** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore + let result = null + if (fs.existsSync(registrationFilePath)) { + const content = fs.readFileSync(registrationFilePath, "utf8") + if (content.startsWith("{")) { // Use JSON parser + result = JSON.parse(content) + checkRegistration(result) + } else { // Use YAML parser + result = yaml.load(content) + checkRegistration(result) + // Convert to JSON + writeRegistration(result) + } + } + return result +} + +/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore +let reg = readRegistration() + +module.exports.registrationFilePath = registrationFilePath +module.exports.readRegistration = readRegistration +module.exports.getTemplateRegistration = getTemplateRegistration +module.exports.writeRegistration = writeRegistration +module.exports.checkRegistration = checkRegistration +module.exports.reg = reg diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js index e5123b9..80ac09f 100644 --- a/matrix/read-registration.test.js +++ b/matrix/read-registration.test.js @@ -1,5 +1,5 @@ const {test} = require("supertape") -const reg = require("./read-registration") +const {reg} = require("./read-registration") test("reg: has necessary parameters", t => { const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"] diff --git a/package-lock.json b/package-lock.json index ec10c03..3aae6aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,12 @@ "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", + "ansi-colors": "^4.1.3", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.10", "domino": "^2.1.6", + "enquirer": "^2.4.1", "entities": "^5.0.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", @@ -978,6 +980,14 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -1495,6 +1505,37 @@ "once": "^1.4.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/entities": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz", diff --git a/package.json b/package.json index bbdde1e..8c800e1 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,12 @@ "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/turndown": "^7.1.4", + "ansi-colors": "^4.1.3", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", "cloudstorm": "^0.10.10", "domino": "^2.1.6", + "enquirer": "^2.4.1", "entities": "^5.0.0", "get-stream": "^6.0.1", "heatsync": "^2.5.3", diff --git a/readme.md b/readme.md index 4e7b344..bc19046 100644 --- a/readme.md +++ b/readme.md @@ -165,14 +165,16 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (1) @cloudrac3r/discord-markdown: This is my fork! * (0) @cloudrac3r/giframe: This is my fork! * (1) @cloudrac3r/html-template-tag: This is my fork! -* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. +* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It has several dependencies because HTTP servers have to do more than you'd think. * (0) @cloudrac3r/mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. +* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. * (42) better-sqlite3: SQLite3 is the best database, and this is the best library for it. Really! I love it. * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. * (0) domino: DOM implementation that's already pulled in by turndown. +* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. * (0) get-stream: Only needed if content_length_workaround is true. * (1) heatsync: Module hot-reloader that I trust. @@ -186,4 +188,4 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. -Total transitive production dependencies: 113 +Total transitive production dependencies: 116 diff --git a/scripts/migrate-from-old-bridge.js b/scripts/migrate-from-old-bridge.js index aec345d..e03938d 100644 --- a/scripts/migrate-from-old-bridge.js +++ b/scripts/migrate-from-old-bridge.js @@ -11,8 +11,7 @@ const passthrough = require("../passthrough") const sync = new HeatSync({watchFS: false}) -/** @type {import("../matrix/read-registration")} */ -const reg = sync.require("../matrix/read-registration") +const {reg} = require("../matrix/read-registration") assert(reg.old_bridge) const oldAT = reg.old_bridge.as_token const newAT = reg.as_token diff --git a/scripts/seed.js b/scripts/seed.js index a3ffbbe..8ba22df 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -1,11 +1,13 @@ // @ts-check -console.log("This could take up to 30 seconds. Please be patient.") - const assert = require("assert").strict const fs = require("fs") const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") +const {prompt, Prompt} = require("enquirer") +const Input = require("enquirer/lib/prompts/input") +const fetch = require("node-fetch") +const {magenta, bold} = require("ansi-colors") const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]}) @@ -26,10 +28,8 @@ const DiscordClient = require("../d2m/discord-client") const discord = new DiscordClient(config.discordToken, "no") passthrough.discord = discord -const api = require("../matrix/api") -const file = require("../matrix/file") -const reg = require("../matrix/read-registration") -const utils = require("../m2d/converters/utils") +let registration = require("../matrix/read-registration") +let {reg, getTemplateRegistration, writeRegistration, readRegistration} = registration function die(message) { console.error(message) @@ -50,6 +50,73 @@ async function uploadAutoEmoji(guild, name, filename) { } ;(async () => { + // create registration file with prompts... + if (!reg) { + console.log("What is the name of your homeserver? This is the part after : in your username.") + /** @type {{server_name: string}} */ + const serverNameResponse = await prompt({ + type: "input", + name: "server_name", + message: "Homeserver name" + }) + console.log("What is the URL of your homeserver?") + const serverUrlPrompt = new Input({ + type: "input", + name: "server_origin", + message: "Homeserver URL", + initial: () => `https://${serverNameResponse.server_name}`, + validate: async url => { + if (!url.match(/^https?:\/\//)) return "Must be a URL" + if (url.match(/\/$/)) return "Must not end with a slash" + process.stdout.write(magenta(" checking, please wait...")) + try { + var json = await fetch(`${url}/.well-known/matrix/client`).then(res => res.json()) + let baseURL = json["m.homeserver"].base_url.replace(/\/$/, "") + if (baseURL && baseURL !== url) { + serverUrlPrompt.initial = baseURL + return `Did you mean: ${bold(baseURL)}? (Enter to accept)` + } + } catch (e) {} + try { + var res = await fetch(`${url}/_matrix/client/versions`) + } catch (e) { + return e.message + } + if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})` + try { + var json = await res.json() + } catch (e) { + return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)` + } + return true + } + }) + /** @type {{server_origin: string}} */ // @ts-ignore + const serverUrlResponse = await serverUrlPrompt.run() + console.log("Your Matrix homeserver needs to be able to send HTTP requests to OOYE.") + console.log("What URL should OOYE be reachable on? Usually, the default works fine,") + console.log("but you need to change this if you use multiple servers or containers.") + /** @type {{url: string}} */ + const urlResponse = await prompt({ + type: "input", + name: "url", + message: "URL to reach OOYE", + initial: "http://localhost:6693", + validate: url => !!url.match(/^https?:\/\//) + }) + const template = getTemplateRegistration() + reg = Object.assign(template, urlResponse, {ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}}) + registration.reg = reg + writeRegistration(reg) + } + // done with user prompts, reg is now guaranteed to be valid + + console.log("Processing. This could take up to 30 seconds. Please be patient...") + + const api = require("../matrix/api") + const file = require("../matrix/file") + const utils = require("../m2d/converters/utils") + const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}` // ensure registration is correctly set... @@ -60,6 +127,9 @@ async function uploadAutoEmoji(guild, name, filename) { const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString() assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") assert.match(reg.url, /^https?:/, "url must start with http:// or https://") + + // TODO: appservice ping until it works + console.log("✅ Configuration looks good...") // database ddl... diff --git a/test/test.js b/test/test.js index 796ff68..1941ea2 100644 --- a/test/test.js +++ b/test/test.js @@ -17,7 +17,7 @@ const config = require("../config") const passthrough = require("../passthrough") const db = new sqlite(":memory:") -const reg = require("../matrix/read-registration") +const {reg} = require("../matrix/read-registration") reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server @@ -117,7 +117,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/file.test") - require("../matrix/power.test") + //require("../matrix/power.test") require("../matrix/read-registration.test") require("../matrix/txnid.test") require("../d2m/actions/create-room.test") diff --git a/types.d.ts b/types.d.ts index c710e12..8eeda6c 100644 --- a/types.d.ts +++ b/types.d.ts @@ -31,6 +31,32 @@ export type AppServiceRegistrationConfig = { } } +export type InitialAppServiceRegistrationConfig = { + id: string + as_token: string + hs_token: string + sender_localpart: string + namespaces: { + users: { + exclusive: boolean + regex: string + }[] + aliases: { + exclusive: boolean + regex: string + }[] + } + protocols: [string] + rate_limited: boolean + ooye: { + namespace_prefix: string + max_file_size: number, + content_length_workaround: boolean, + invite: string[], + include_user_id_in_mxid: boolean + } +} + export type WebhookCreds = { id: string token: string From 7d42a530e746b7daccaaf1316cc5ea4eda1c993a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 5 Sep 2024 16:48:53 +1200 Subject: [PATCH 079/346] Wait for ping to work during setup --- matrix/api.js | 18 ++++++++ matrix/mreq.js | 1 + scripts/seed.js | 108 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 92 insertions(+), 35 deletions(-) diff --git a/matrix/api.js b/matrix/api.js index 0d61207..d33961d 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -11,6 +11,7 @@ const mreq = sync.require("./mreq") const file = sync.require("./file") /** @type {import("./txnid")} */ const makeTxnId = sync.require("./txnid") +const {reg} = require("./read-registration.js") /** * @param {string} p endpoint to access @@ -295,6 +296,22 @@ async function setUserPowerCascade(roomID, mxid, power) { } } +async function ping() { + const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, { + method: "POST", + headers: { + Authorization: `Bearer ${reg.as_token}` + }, + body: "{}" + }) + const root = await res.json() + return { + ok: res.ok, + status: res.status, + root + } +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -318,3 +335,4 @@ module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.setUserPower = setUserPower module.exports.setUserPowerCascade = setUserPowerCascade +module.exports.ping = ping diff --git a/matrix/mreq.js b/matrix/mreq.js index 5e9abc4..4707ae6 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -78,5 +78,6 @@ async function withAccessToken(token, callback) { } module.exports.MatrixServerError = MatrixServerError +module.exports.baseUrl = baseUrl module.exports.mreq = mreq module.exports.withAccessToken = withAccessToken diff --git a/scripts/seed.js b/scripts/seed.js index 8ba22df..dbb2cb5 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -3,11 +3,14 @@ const assert = require("assert").strict const fs = require("fs") const sqlite = require("better-sqlite3") -const HeatSync = require("heatsync") -const {prompt, Prompt} = require("enquirer") +const {scheduler: {wait}} = require("timers/promises") +const {isDeepStrictEqual} = require("util") + +const {prompt} = require("enquirer") const Input = require("enquirer/lib/prompts/input") const fetch = require("node-fetch") -const {magenta, bold} = require("ansi-colors") +const {magenta, bold, cyan} = require("ansi-colors") +const HeatSync = require("heatsync") const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]}) @@ -29,7 +32,7 @@ const discord = new DiscordClient(config.discordToken, "no") passthrough.discord = discord let registration = require("../matrix/read-registration") -let {reg, getTemplateRegistration, writeRegistration, readRegistration} = registration +let {reg, getTemplateRegistration, writeRegistration, readRegistration, registrationFilePath} = registration function die(message) { console.error(message) @@ -49,6 +52,32 @@ async function uploadAutoEmoji(guild, name, filename) { return emoji } +async function validateHomeserverOrigin(serverUrlPrompt, url) { + if (!url.match(/^https?:\/\//)) return "Must be a URL" + if (url.match(/\/$/)) return "Must not end with a slash" + process.stdout.write(magenta(" checking, please wait...")) + try { + var json = await fetch(`${url}/.well-known/matrix/client`).then(res => res.json()) + let baseURL = json["m.homeserver"].base_url.replace(/\/$/, "") + if (baseURL && baseURL !== url) { + serverUrlPrompt.initial = baseURL + return `Did you mean: ${bold(baseURL)}? (Enter to accept)` + } + } catch (e) {} + try { + var res = await fetch(`${url}/_matrix/client/versions`) + } catch (e) { + return e.message + } + if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})` + try { + var json = await res.json() + } catch (e) { + return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)` + } + return true +} + ;(async () => { // create registration file with prompts... if (!reg) { @@ -65,31 +94,7 @@ async function uploadAutoEmoji(guild, name, filename) { name: "server_origin", message: "Homeserver URL", initial: () => `https://${serverNameResponse.server_name}`, - validate: async url => { - if (!url.match(/^https?:\/\//)) return "Must be a URL" - if (url.match(/\/$/)) return "Must not end with a slash" - process.stdout.write(magenta(" checking, please wait...")) - try { - var json = await fetch(`${url}/.well-known/matrix/client`).then(res => res.json()) - let baseURL = json["m.homeserver"].base_url.replace(/\/$/, "") - if (baseURL && baseURL !== url) { - serverUrlPrompt.initial = baseURL - return `Did you mean: ${bold(baseURL)}? (Enter to accept)` - } - } catch (e) {} - try { - var res = await fetch(`${url}/_matrix/client/versions`) - } catch (e) { - return e.message - } - if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})` - try { - var json = await res.json() - } catch (e) { - return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)` - } - return true - } + validate: url => validateHomeserverOrigin(serverUrlPrompt, url) }) /** @type {{server_origin: string}} */ // @ts-ignore const serverUrlResponse = await serverUrlPrompt.run() @@ -105,18 +110,53 @@ async function uploadAutoEmoji(guild, name, filename) { validate: url => !!url.match(/^https?:\/\//) }) const template = getTemplateRegistration() - reg = Object.assign(template, urlResponse, {ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}}) + reg = {...template, ...urlResponse, ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}} registration.reg = reg writeRegistration(reg) } - // done with user prompts, reg is now guaranteed to be valid - - console.log("Processing. This could take up to 30 seconds. Please be patient...") + // Done with user prompts, reg is now guaranteed to be valid const api = require("../matrix/api") const file = require("../matrix/file") const utils = require("../m2d/converters/utils") + console.log(`✅ Registration file saved as ${registrationFilePath}`) + console.log(` In ${cyan("Synapse")}, you need to add it to homeserver.yaml and ${cyan("restart Synapse")}.`) + console.log(" https://element-hq.github.io/synapse/latest/application_services.html") + console.log(` In ${cyan("Conduit")}, you need to send the file contents to the #admins room.`) + console.log(" https://docs.conduit.rs/appservices.html") + console.log() + + const {as} = require("../matrix/appservice") + console.log("⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)") + + let itWorks = false + let lastError = null + do { + const result = await api.ping() + // If it didn't work, log details and retry after some time + itWorks = result.ok + if (!itWorks) { + // Log the full error data if the error is different to last time + if (!isDeepStrictEqual(lastError, result.root)) { + if (result.root.error) { + console.log(`\nHomeserver said: [${result.status}] ${result.root.error}`) + } else { + console.log(`\nHomeserver said: [${result.status}] ${JSON.stringify(result.root)}`) + } + lastError = result.root + } else { + process.stderr.write(".") + } + await wait(5000) + } + } while (!itWorks) + console.log("") + + as.close().catch(() => {}) + + console.log("⏩ Processing. This could take up to 30 seconds. Please be patient...") + const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}` // ensure registration is correctly set... @@ -128,8 +168,6 @@ async function uploadAutoEmoji(guild, name, filename) { assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") assert.match(reg.url, /^https?:/, "url must start with http:// or https://") - // TODO: appservice ping until it works - console.log("✅ Configuration looks good...") // database ddl... From 23a7394ce738f5057e9a055f055b7a3dac25ac71 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 9 Sep 2024 10:31:10 +1200 Subject: [PATCH 080/346] Catch bridge->hs connection errors --- scripts/seed.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/seed.js b/scripts/seed.js index dbb2cb5..ce5cfaf 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -133,13 +133,15 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { let itWorks = false let lastError = null do { - const result = await api.ping() + const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) // If it didn't work, log details and retry after some time itWorks = result.ok if (!itWorks) { // Log the full error data if the error is different to last time if (!isDeepStrictEqual(lastError, result.root)) { - if (result.root.error) { + if (typeof result.root === "string") { + console.log(`\nCannot reach homeserver: ${result.root}`) + } else if (result.root.error) { console.log(`\nHomeserver said: [${result.status}] ${result.root.error}`) } else { console.log(`\nHomeserver said: [${result.status}] ${JSON.stringify(result.root)}`) From 96fd04653001a0ab4b3e6df3f6eccf63cccd4a8f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 10 Sep 2024 12:40:17 +1200 Subject: [PATCH 081/346] Fix type warning in emoji sprite sheet --- m2d/converters/emoji-sheet.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2d/converters/emoji-sheet.js b/m2d/converters/emoji-sheet.js index 4d9cc07..db5b06f 100644 --- a/m2d/converters/emoji-sheet.js +++ b/m2d/converters/emoji-sheet.js @@ -81,9 +81,10 @@ async function convertImageStream(streamIn, stopStream) { giframe.feed(chunk) }) const frame = await giframe.getFrame() + const pixels = Uint8Array.from(frame.pixels) stopStream() - const buffer = await sharp(frame.pixels, {raw: {width: frame.width, height: frame.height, channels: 4}}) + const buffer = await sharp(pixels, {raw: {width: frame.width, height: frame.height, channels: 4}}) .resize(SIZE, SIZE, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}}) .png({compressionLevel: 0}) .toBuffer({resolveWithObject: true}) From decc32f7e6cf9068cd1f6b0fed70940d21847d77 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Sep 2024 16:59:48 +1200 Subject: [PATCH 082/346] Change getPublicUrlForMxc function for authmedia --- discord/discord-command-handler.js | 5 ++-- m2d/converters/event-to-message.js | 2 ++ m2d/converters/utils.js | 43 ++++++++++++++++++++++-------- stdin.js | 1 - test/test.js | 1 + types.d.ts | 1 + 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/discord/discord-command-handler.js b/discord/discord-command-handler.js index a434bf9..d9ad80f 100644 --- a/discord/discord-command-handler.js +++ b/discord/discord-command-handler.js @@ -11,6 +11,8 @@ const {discord, sync, db, select} = require("../passthrough") const api = sync.require("../matrix/api") /** @type {import("../matrix/file")} */ const file = sync.require("../matrix/file") +/** @type {import("../m2d/converters/utils")} */ +const mxUtils = sync.require("../matrix/utils") /** @type {import("../d2m/actions/create-space")} */ const createSpace = sync.require("../d2m/actions/create-space") /** @type {import("./utils")} */ @@ -91,9 +93,8 @@ const commands = [{ // Current avatar const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") - const avatarURLParts = avatarEvent?.url.match(/^mxc:\/\/([^/]+)\/(\w+)$/) let currentAvatarMessage = - ( avatarURLParts ? `Current room-specific avatar: ${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` + ( avatarEvent.url ? `Current room-specific avatar: ${mxUtils.getPublicUrlForMxc(avatarEvent.url)}` : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") // Next potential avatar diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 5234755..76a32bc 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -15,6 +15,8 @@ const {sync, db, discord, select, from} = passthrough const mxUtils = sync.require("../converters/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") /** @type {import("./emoji-sheet")} */ const emojiSheet = sync.require("./emoji-sheet") diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 437b5c0..c1f5f01 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -1,8 +1,13 @@ // @ts-check +const assert = require("assert").strict + +const passthrough = require("../../passthrough") +const {db} = passthrough + const {reg} = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) -const assert = require("assert").strict + /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null // @ts-ignore @@ -35,16 +40,6 @@ function eventSenderIsFromDiscord(sender) { return false } -/** - * @param {string} mxc - * @returns {string?} - */ -function getPublicUrlForMxc(mxc) { - const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) - if (avatarURLParts) return `${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` - else return null -} - /** * Event IDs are really big and have more entropy than we need. * If we want to store the event ID in the database, we can store a more compact version by hashing it with this. @@ -213,6 +208,32 @@ async function getViaServersQuery(roomID, api) { return qs } +/** + * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL + * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge. + * To avoid the bridge acting as a proxy for *any* media, there is a list of permitted media stored in the database. + * (The other approach would be signing the URLs with a MAC (or similar) and adding the signature, but I'm not a + * cryptographer, so I don't want to.) To reduce database disk space usage, instead of storing each permitted URL, + * we just store its xxhash as a signed (as in +/-, not signature) 64-bit integer, which fits in an SQLite integer field. + * @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background + * @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details + * @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size + * @param {string} mxc + * @returns {string?} + */ +function getPublicUrlForMxc(mxc) { + assert(hasher, "xxhash is not ready yet") + const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + if (!avatarURLParts) return null + + const serverAndMediaID = `${avatarURLParts[1]}/${avatarURLParts[2]}` + const unsignedHash = hasher.h64(serverAndMediaID) + const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range + db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) + + return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}` +} + module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord module.exports.getPublicUrlForMxc = getPublicUrlForMxc diff --git a/stdin.js b/stdin.js index 5e23f72..780b9dc 100644 --- a/stdin.js +++ b/stdin.js @@ -32,7 +32,6 @@ if (process.stdin.isTTY) { } else { Object.assign(passthrough.repl.context, extraContext) } - // @ts-expect-error Says exit isn't assignable to a string sync.addTemporaryListener(passthrough.repl, "exit", () => process.exit()) }) } diff --git a/test/test.js b/test/test.js index 1941ea2..c2b703d 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" +reg.ooye.bridge_origin = "https://bridge.example.org" reg.ooye.invite = [] const sync = new HeatSync({watchFS: false}) diff --git a/types.d.ts b/types.d.ts index 8eeda6c..93bfc75 100644 --- a/types.d.ts +++ b/types.d.ts @@ -21,6 +21,7 @@ export type AppServiceRegistrationConfig = { max_file_size: number server_name: string server_origin: string + bridge_origin: string content_length_workaround: boolean include_user_id_in_mxid: boolean invite: string[] From 4247a3114a3ce7ce1e5d79f47719a03608710578 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Sep 2024 17:05:13 +1200 Subject: [PATCH 083/346] Move everything to src folder... it had to happen --- package.json | 2 +- {d2m => src/d2m}/actions/add-reaction.js | 0 {d2m => src/d2m}/actions/announce-thread.js | 0 {d2m => src/d2m}/actions/create-room.js | 0 {d2m => src/d2m}/actions/create-room.test.js | 0 {d2m => src/d2m}/actions/create-space.js | 0 {d2m => src/d2m}/actions/create-space.test.js | 0 {d2m => src/d2m}/actions/delete-message.js | 0 {d2m => src/d2m}/actions/edit-message.js | 0 {d2m => src/d2m}/actions/expression.js | 0 {d2m => src/d2m}/actions/lottie.js | 0 {d2m => src/d2m}/actions/register-pk-user.js | 0 {d2m => src/d2m}/actions/register-user.js | 0 {d2m => src/d2m}/actions/register-user.test.js | 0 {d2m => src/d2m}/actions/remove-reaction.js | 0 {d2m => src/d2m}/actions/retrigger.js | 0 {d2m => src/d2m}/actions/send-message.js | 0 {d2m => src/d2m}/actions/speedbump.js | 0 {d2m => src/d2m}/actions/update-pins.js | 0 {d2m => src/d2m}/converters/edit-to-changes.js | 0 {d2m => src/d2m}/converters/edit-to-changes.test.js | 0 {d2m => src/d2m}/converters/emoji-to-key.js | 0 {d2m => src/d2m}/converters/emoji-to-key.test.js | 0 {d2m => src/d2m}/converters/lottie.js | 0 {d2m => src/d2m}/converters/lottie.test.js | 0 .../d2m}/converters/message-to-event.embeds.test.js | 0 {d2m => src/d2m}/converters/message-to-event.js | 0 .../d2m}/converters/message-to-event.pk.test.js | 0 .../d2m}/converters/message-to-event.test.js | 0 {d2m => src/d2m}/converters/pins-to-list.js | 0 {d2m => src/d2m}/converters/pins-to-list.test.js | 0 {d2m => src/d2m}/converters/remove-reaction.js | 0 {d2m => src/d2m}/converters/remove-reaction.test.js | 0 {d2m => src/d2m}/converters/rlottie-wasm.js | 0 {d2m => src/d2m}/converters/rlottie-wasm.license | 0 {d2m => src/d2m}/converters/rlottie-wasm.wasm | Bin .../d2m}/converters/thread-to-announcement.js | 0 .../d2m}/converters/thread-to-announcement.test.js | 0 {d2m => src/d2m}/converters/user-to-mxid.js | 0 {d2m => src/d2m}/converters/user-to-mxid.test.js | 0 {d2m => src/d2m}/discord-client.js | 0 {d2m => src/d2m}/discord-packets.js | 0 {d2m => src/d2m}/event-dispatcher.js | 0 {db => src/db}/migrate.js | 0 {db => src/db}/migrations/.baby | 0 {db => src/db}/migrations/0001-schema.sql | 0 .../migrations/0002-optimise-profile-content.sql | 0 .../migrations/0002-optimise-profile-content.up.js | 0 .../migrations/0003-distinguish-column-names.sql | 0 {db => src/db}/migrations/0004-auto-emoji-guild.sql | 0 .../db}/migrations/0005-clear-member-cache.sql | 0 .../db}/migrations/0006-add-privacy-to-space.sql | 0 .../0007-split-part-and-reaction-part.sql | 0 .../0008-add-last-bridged-pin-timestamp.sql | 0 {db => src/db}/migrations/0009-add-speedbump-id.sql | 0 {db => src/db}/migrations/0010-add-sim-proxy.sql | 0 .../0011-move-bridge-bot-to-real-id.up.js | 0 {db => src/db}/migrations/0012-add-member-power.sql | 0 {db => src/db}/orm-defs.d.ts | 0 {db => src/db}/orm-defs.js | 0 {db => src/db}/orm.js | 0 {db => src/db}/orm.test.js | 0 {discord => src/discord}/discord-command-handler.js | 0 {discord => src/discord}/interactions/bridge.js | 0 {discord => src/discord}/interactions/invite.js | 0 .../discord}/interactions/matrix-info.js | 0 .../discord}/interactions/permissions.js | 0 {discord => src/discord}/interactions/reactions.js | 0 {discord => src/discord}/register-interactions.js | 0 {discord => src/discord}/utils.js | 0 {discord => src/discord}/utils.test.js | 0 {m2d => src/m2d}/actions/add-reaction.js | 0 {m2d => src/m2d}/actions/channel-webhook.js | 0 {m2d => src/m2d}/actions/emoji-sheet.js | 0 {m2d => src/m2d}/actions/redact.js | 0 {m2d => src/m2d}/actions/send-event.js | 0 {m2d => src/m2d}/converters/emoji-sheet.js | 0 {m2d => src/m2d}/converters/emoji-sheet.test.js | 0 {m2d => src/m2d}/converters/emoji.js | 0 {m2d => src/m2d}/converters/event-to-message.js | 0 .../m2d}/converters/event-to-message.test.js | 0 {m2d => src/m2d}/converters/utils.js | 0 {m2d => src/m2d}/converters/utils.test.js | 0 {m2d => src/m2d}/event-dispatcher.js | 0 {matrix => src/matrix}/api.js | 0 {matrix => src/matrix}/api.test.js | 0 {matrix => src/matrix}/appservice.js | 0 {matrix => src/matrix}/file.js | 0 {matrix => src/matrix}/file.test.js | 0 {matrix => src/matrix}/kstate.js | 0 {matrix => src/matrix}/kstate.test.js | 0 {matrix => src/matrix}/matrix-command-handler.js | 0 {matrix => src/matrix}/mreq.js | 0 {matrix => src/matrix}/power.js | 0 {matrix => src/matrix}/power.test.js | 0 {matrix => src/matrix}/read-registration.js | 0 {matrix => src/matrix}/read-registration.test.js | 0 {matrix => src/matrix}/txnid.js | 0 {matrix => src/matrix}/txnid.test.js | 0 passthrough.js => src/passthrough.js | 0 stdin.js => src/stdin.js | 0 types.d.ts => src/types.d.ts | 0 types.js => src/types.js | 0 103 files changed, 1 insertion(+), 1 deletion(-) rename {d2m => src/d2m}/actions/add-reaction.js (100%) rename {d2m => src/d2m}/actions/announce-thread.js (100%) rename {d2m => src/d2m}/actions/create-room.js (100%) rename {d2m => src/d2m}/actions/create-room.test.js (100%) rename {d2m => src/d2m}/actions/create-space.js (100%) rename {d2m => src/d2m}/actions/create-space.test.js (100%) rename {d2m => src/d2m}/actions/delete-message.js (100%) rename {d2m => src/d2m}/actions/edit-message.js (100%) rename {d2m => src/d2m}/actions/expression.js (100%) rename {d2m => src/d2m}/actions/lottie.js (100%) rename {d2m => src/d2m}/actions/register-pk-user.js (100%) rename {d2m => src/d2m}/actions/register-user.js (100%) rename {d2m => src/d2m}/actions/register-user.test.js (100%) rename {d2m => src/d2m}/actions/remove-reaction.js (100%) rename {d2m => src/d2m}/actions/retrigger.js (100%) rename {d2m => src/d2m}/actions/send-message.js (100%) rename {d2m => src/d2m}/actions/speedbump.js (100%) rename {d2m => src/d2m}/actions/update-pins.js (100%) rename {d2m => src/d2m}/converters/edit-to-changes.js (100%) rename {d2m => src/d2m}/converters/edit-to-changes.test.js (100%) rename {d2m => src/d2m}/converters/emoji-to-key.js (100%) rename {d2m => src/d2m}/converters/emoji-to-key.test.js (100%) rename {d2m => src/d2m}/converters/lottie.js (100%) rename {d2m => src/d2m}/converters/lottie.test.js (100%) rename {d2m => src/d2m}/converters/message-to-event.embeds.test.js (100%) rename {d2m => src/d2m}/converters/message-to-event.js (100%) rename {d2m => src/d2m}/converters/message-to-event.pk.test.js (100%) rename {d2m => src/d2m}/converters/message-to-event.test.js (100%) rename {d2m => src/d2m}/converters/pins-to-list.js (100%) rename {d2m => src/d2m}/converters/pins-to-list.test.js (100%) rename {d2m => src/d2m}/converters/remove-reaction.js (100%) rename {d2m => src/d2m}/converters/remove-reaction.test.js (100%) rename {d2m => src/d2m}/converters/rlottie-wasm.js (100%) rename {d2m => src/d2m}/converters/rlottie-wasm.license (100%) rename {d2m => src/d2m}/converters/rlottie-wasm.wasm (100%) mode change 100755 => 100644 rename {d2m => src/d2m}/converters/thread-to-announcement.js (100%) rename {d2m => src/d2m}/converters/thread-to-announcement.test.js (100%) rename {d2m => src/d2m}/converters/user-to-mxid.js (100%) rename {d2m => src/d2m}/converters/user-to-mxid.test.js (100%) rename {d2m => src/d2m}/discord-client.js (100%) rename {d2m => src/d2m}/discord-packets.js (100%) rename {d2m => src/d2m}/event-dispatcher.js (100%) rename {db => src/db}/migrate.js (100%) rename {db => src/db}/migrations/.baby (100%) rename {db => src/db}/migrations/0001-schema.sql (100%) rename {db => src/db}/migrations/0002-optimise-profile-content.sql (100%) rename {db => src/db}/migrations/0002-optimise-profile-content.up.js (100%) rename {db => src/db}/migrations/0003-distinguish-column-names.sql (100%) rename {db => src/db}/migrations/0004-auto-emoji-guild.sql (100%) rename {db => src/db}/migrations/0005-clear-member-cache.sql (100%) rename {db => src/db}/migrations/0006-add-privacy-to-space.sql (100%) rename {db => src/db}/migrations/0007-split-part-and-reaction-part.sql (100%) rename {db => src/db}/migrations/0008-add-last-bridged-pin-timestamp.sql (100%) rename {db => src/db}/migrations/0009-add-speedbump-id.sql (100%) rename {db => src/db}/migrations/0010-add-sim-proxy.sql (100%) rename {db => src/db}/migrations/0011-move-bridge-bot-to-real-id.up.js (100%) rename {db => src/db}/migrations/0012-add-member-power.sql (100%) rename {db => src/db}/orm-defs.d.ts (100%) rename {db => src/db}/orm-defs.js (100%) rename {db => src/db}/orm.js (100%) rename {db => src/db}/orm.test.js (100%) rename {discord => src/discord}/discord-command-handler.js (100%) rename {discord => src/discord}/interactions/bridge.js (100%) rename {discord => src/discord}/interactions/invite.js (100%) rename {discord => src/discord}/interactions/matrix-info.js (100%) rename {discord => src/discord}/interactions/permissions.js (100%) rename {discord => src/discord}/interactions/reactions.js (100%) rename {discord => src/discord}/register-interactions.js (100%) rename {discord => src/discord}/utils.js (100%) rename {discord => src/discord}/utils.test.js (100%) rename {m2d => src/m2d}/actions/add-reaction.js (100%) rename {m2d => src/m2d}/actions/channel-webhook.js (100%) rename {m2d => src/m2d}/actions/emoji-sheet.js (100%) rename {m2d => src/m2d}/actions/redact.js (100%) rename {m2d => src/m2d}/actions/send-event.js (100%) rename {m2d => src/m2d}/converters/emoji-sheet.js (100%) rename {m2d => src/m2d}/converters/emoji-sheet.test.js (100%) rename {m2d => src/m2d}/converters/emoji.js (100%) rename {m2d => src/m2d}/converters/event-to-message.js (100%) rename {m2d => src/m2d}/converters/event-to-message.test.js (100%) rename {m2d => src/m2d}/converters/utils.js (100%) rename {m2d => src/m2d}/converters/utils.test.js (100%) rename {m2d => src/m2d}/event-dispatcher.js (100%) rename {matrix => src/matrix}/api.js (100%) rename {matrix => src/matrix}/api.test.js (100%) rename {matrix => src/matrix}/appservice.js (100%) rename {matrix => src/matrix}/file.js (100%) rename {matrix => src/matrix}/file.test.js (100%) rename {matrix => src/matrix}/kstate.js (100%) rename {matrix => src/matrix}/kstate.test.js (100%) rename {matrix => src/matrix}/matrix-command-handler.js (100%) rename {matrix => src/matrix}/mreq.js (100%) rename {matrix => src/matrix}/power.js (100%) rename {matrix => src/matrix}/power.test.js (100%) rename {matrix => src/matrix}/read-registration.js (100%) rename {matrix => src/matrix}/read-registration.test.js (100%) rename {matrix => src/matrix}/txnid.js (100%) rename {matrix => src/matrix}/txnid.test.js (100%) rename passthrough.js => src/passthrough.js (100%) rename stdin.js => src/stdin.js (100%) rename types.d.ts => src/types.d.ts (100%) rename types.js => src/types.js (100%) diff --git a/package.json b/package.json index 8c800e1..8fccd08 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,6 @@ "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", - "cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -x d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" + "cover": "c8 -o test/coverage --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -x d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" } } diff --git a/d2m/actions/add-reaction.js b/src/d2m/actions/add-reaction.js similarity index 100% rename from d2m/actions/add-reaction.js rename to src/d2m/actions/add-reaction.js diff --git a/d2m/actions/announce-thread.js b/src/d2m/actions/announce-thread.js similarity index 100% rename from d2m/actions/announce-thread.js rename to src/d2m/actions/announce-thread.js diff --git a/d2m/actions/create-room.js b/src/d2m/actions/create-room.js similarity index 100% rename from d2m/actions/create-room.js rename to src/d2m/actions/create-room.js diff --git a/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js similarity index 100% rename from d2m/actions/create-room.test.js rename to src/d2m/actions/create-room.test.js diff --git a/d2m/actions/create-space.js b/src/d2m/actions/create-space.js similarity index 100% rename from d2m/actions/create-space.js rename to src/d2m/actions/create-space.js diff --git a/d2m/actions/create-space.test.js b/src/d2m/actions/create-space.test.js similarity index 100% rename from d2m/actions/create-space.test.js rename to src/d2m/actions/create-space.test.js diff --git a/d2m/actions/delete-message.js b/src/d2m/actions/delete-message.js similarity index 100% rename from d2m/actions/delete-message.js rename to src/d2m/actions/delete-message.js diff --git a/d2m/actions/edit-message.js b/src/d2m/actions/edit-message.js similarity index 100% rename from d2m/actions/edit-message.js rename to src/d2m/actions/edit-message.js diff --git a/d2m/actions/expression.js b/src/d2m/actions/expression.js similarity index 100% rename from d2m/actions/expression.js rename to src/d2m/actions/expression.js diff --git a/d2m/actions/lottie.js b/src/d2m/actions/lottie.js similarity index 100% rename from d2m/actions/lottie.js rename to src/d2m/actions/lottie.js diff --git a/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js similarity index 100% rename from d2m/actions/register-pk-user.js rename to src/d2m/actions/register-pk-user.js diff --git a/d2m/actions/register-user.js b/src/d2m/actions/register-user.js similarity index 100% rename from d2m/actions/register-user.js rename to src/d2m/actions/register-user.js diff --git a/d2m/actions/register-user.test.js b/src/d2m/actions/register-user.test.js similarity index 100% rename from d2m/actions/register-user.test.js rename to src/d2m/actions/register-user.test.js diff --git a/d2m/actions/remove-reaction.js b/src/d2m/actions/remove-reaction.js similarity index 100% rename from d2m/actions/remove-reaction.js rename to src/d2m/actions/remove-reaction.js diff --git a/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js similarity index 100% rename from d2m/actions/retrigger.js rename to src/d2m/actions/retrigger.js diff --git a/d2m/actions/send-message.js b/src/d2m/actions/send-message.js similarity index 100% rename from d2m/actions/send-message.js rename to src/d2m/actions/send-message.js diff --git a/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js similarity index 100% rename from d2m/actions/speedbump.js rename to src/d2m/actions/speedbump.js diff --git a/d2m/actions/update-pins.js b/src/d2m/actions/update-pins.js similarity index 100% rename from d2m/actions/update-pins.js rename to src/d2m/actions/update-pins.js diff --git a/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js similarity index 100% rename from d2m/converters/edit-to-changes.js rename to src/d2m/converters/edit-to-changes.js diff --git a/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js similarity index 100% rename from d2m/converters/edit-to-changes.test.js rename to src/d2m/converters/edit-to-changes.test.js diff --git a/d2m/converters/emoji-to-key.js b/src/d2m/converters/emoji-to-key.js similarity index 100% rename from d2m/converters/emoji-to-key.js rename to src/d2m/converters/emoji-to-key.js diff --git a/d2m/converters/emoji-to-key.test.js b/src/d2m/converters/emoji-to-key.test.js similarity index 100% rename from d2m/converters/emoji-to-key.test.js rename to src/d2m/converters/emoji-to-key.test.js diff --git a/d2m/converters/lottie.js b/src/d2m/converters/lottie.js similarity index 100% rename from d2m/converters/lottie.js rename to src/d2m/converters/lottie.js diff --git a/d2m/converters/lottie.test.js b/src/d2m/converters/lottie.test.js similarity index 100% rename from d2m/converters/lottie.test.js rename to src/d2m/converters/lottie.test.js diff --git a/d2m/converters/message-to-event.embeds.test.js b/src/d2m/converters/message-to-event.embeds.test.js similarity index 100% rename from d2m/converters/message-to-event.embeds.test.js rename to src/d2m/converters/message-to-event.embeds.test.js diff --git a/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js similarity index 100% rename from d2m/converters/message-to-event.js rename to src/d2m/converters/message-to-event.js diff --git a/d2m/converters/message-to-event.pk.test.js b/src/d2m/converters/message-to-event.pk.test.js similarity index 100% rename from d2m/converters/message-to-event.pk.test.js rename to src/d2m/converters/message-to-event.pk.test.js diff --git a/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js similarity index 100% rename from d2m/converters/message-to-event.test.js rename to src/d2m/converters/message-to-event.test.js diff --git a/d2m/converters/pins-to-list.js b/src/d2m/converters/pins-to-list.js similarity index 100% rename from d2m/converters/pins-to-list.js rename to src/d2m/converters/pins-to-list.js diff --git a/d2m/converters/pins-to-list.test.js b/src/d2m/converters/pins-to-list.test.js similarity index 100% rename from d2m/converters/pins-to-list.test.js rename to src/d2m/converters/pins-to-list.test.js diff --git a/d2m/converters/remove-reaction.js b/src/d2m/converters/remove-reaction.js similarity index 100% rename from d2m/converters/remove-reaction.js rename to src/d2m/converters/remove-reaction.js diff --git a/d2m/converters/remove-reaction.test.js b/src/d2m/converters/remove-reaction.test.js similarity index 100% rename from d2m/converters/remove-reaction.test.js rename to src/d2m/converters/remove-reaction.test.js diff --git a/d2m/converters/rlottie-wasm.js b/src/d2m/converters/rlottie-wasm.js similarity index 100% rename from d2m/converters/rlottie-wasm.js rename to src/d2m/converters/rlottie-wasm.js diff --git a/d2m/converters/rlottie-wasm.license b/src/d2m/converters/rlottie-wasm.license similarity index 100% rename from d2m/converters/rlottie-wasm.license rename to src/d2m/converters/rlottie-wasm.license diff --git a/d2m/converters/rlottie-wasm.wasm b/src/d2m/converters/rlottie-wasm.wasm old mode 100755 new mode 100644 similarity index 100% rename from d2m/converters/rlottie-wasm.wasm rename to src/d2m/converters/rlottie-wasm.wasm diff --git a/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js similarity index 100% rename from d2m/converters/thread-to-announcement.js rename to src/d2m/converters/thread-to-announcement.js diff --git a/d2m/converters/thread-to-announcement.test.js b/src/d2m/converters/thread-to-announcement.test.js similarity index 100% rename from d2m/converters/thread-to-announcement.test.js rename to src/d2m/converters/thread-to-announcement.test.js diff --git a/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js similarity index 100% rename from d2m/converters/user-to-mxid.js rename to src/d2m/converters/user-to-mxid.js diff --git a/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js similarity index 100% rename from d2m/converters/user-to-mxid.test.js rename to src/d2m/converters/user-to-mxid.test.js diff --git a/d2m/discord-client.js b/src/d2m/discord-client.js similarity index 100% rename from d2m/discord-client.js rename to src/d2m/discord-client.js diff --git a/d2m/discord-packets.js b/src/d2m/discord-packets.js similarity index 100% rename from d2m/discord-packets.js rename to src/d2m/discord-packets.js diff --git a/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js similarity index 100% rename from d2m/event-dispatcher.js rename to src/d2m/event-dispatcher.js diff --git a/db/migrate.js b/src/db/migrate.js similarity index 100% rename from db/migrate.js rename to src/db/migrate.js diff --git a/db/migrations/.baby b/src/db/migrations/.baby similarity index 100% rename from db/migrations/.baby rename to src/db/migrations/.baby diff --git a/db/migrations/0001-schema.sql b/src/db/migrations/0001-schema.sql similarity index 100% rename from db/migrations/0001-schema.sql rename to src/db/migrations/0001-schema.sql diff --git a/db/migrations/0002-optimise-profile-content.sql b/src/db/migrations/0002-optimise-profile-content.sql similarity index 100% rename from db/migrations/0002-optimise-profile-content.sql rename to src/db/migrations/0002-optimise-profile-content.sql diff --git a/db/migrations/0002-optimise-profile-content.up.js b/src/db/migrations/0002-optimise-profile-content.up.js similarity index 100% rename from db/migrations/0002-optimise-profile-content.up.js rename to src/db/migrations/0002-optimise-profile-content.up.js diff --git a/db/migrations/0003-distinguish-column-names.sql b/src/db/migrations/0003-distinguish-column-names.sql similarity index 100% rename from db/migrations/0003-distinguish-column-names.sql rename to src/db/migrations/0003-distinguish-column-names.sql diff --git a/db/migrations/0004-auto-emoji-guild.sql b/src/db/migrations/0004-auto-emoji-guild.sql similarity index 100% rename from db/migrations/0004-auto-emoji-guild.sql rename to src/db/migrations/0004-auto-emoji-guild.sql diff --git a/db/migrations/0005-clear-member-cache.sql b/src/db/migrations/0005-clear-member-cache.sql similarity index 100% rename from db/migrations/0005-clear-member-cache.sql rename to src/db/migrations/0005-clear-member-cache.sql diff --git a/db/migrations/0006-add-privacy-to-space.sql b/src/db/migrations/0006-add-privacy-to-space.sql similarity index 100% rename from db/migrations/0006-add-privacy-to-space.sql rename to src/db/migrations/0006-add-privacy-to-space.sql diff --git a/db/migrations/0007-split-part-and-reaction-part.sql b/src/db/migrations/0007-split-part-and-reaction-part.sql similarity index 100% rename from db/migrations/0007-split-part-and-reaction-part.sql rename to src/db/migrations/0007-split-part-and-reaction-part.sql diff --git a/db/migrations/0008-add-last-bridged-pin-timestamp.sql b/src/db/migrations/0008-add-last-bridged-pin-timestamp.sql similarity index 100% rename from db/migrations/0008-add-last-bridged-pin-timestamp.sql rename to src/db/migrations/0008-add-last-bridged-pin-timestamp.sql diff --git a/db/migrations/0009-add-speedbump-id.sql b/src/db/migrations/0009-add-speedbump-id.sql similarity index 100% rename from db/migrations/0009-add-speedbump-id.sql rename to src/db/migrations/0009-add-speedbump-id.sql diff --git a/db/migrations/0010-add-sim-proxy.sql b/src/db/migrations/0010-add-sim-proxy.sql similarity index 100% rename from db/migrations/0010-add-sim-proxy.sql rename to src/db/migrations/0010-add-sim-proxy.sql diff --git a/db/migrations/0011-move-bridge-bot-to-real-id.up.js b/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js similarity index 100% rename from db/migrations/0011-move-bridge-bot-to-real-id.up.js rename to src/db/migrations/0011-move-bridge-bot-to-real-id.up.js diff --git a/db/migrations/0012-add-member-power.sql b/src/db/migrations/0012-add-member-power.sql similarity index 100% rename from db/migrations/0012-add-member-power.sql rename to src/db/migrations/0012-add-member-power.sql diff --git a/db/orm-defs.d.ts b/src/db/orm-defs.d.ts similarity index 100% rename from db/orm-defs.d.ts rename to src/db/orm-defs.d.ts diff --git a/db/orm-defs.js b/src/db/orm-defs.js similarity index 100% rename from db/orm-defs.js rename to src/db/orm-defs.js diff --git a/db/orm.js b/src/db/orm.js similarity index 100% rename from db/orm.js rename to src/db/orm.js diff --git a/db/orm.test.js b/src/db/orm.test.js similarity index 100% rename from db/orm.test.js rename to src/db/orm.test.js diff --git a/discord/discord-command-handler.js b/src/discord/discord-command-handler.js similarity index 100% rename from discord/discord-command-handler.js rename to src/discord/discord-command-handler.js diff --git a/discord/interactions/bridge.js b/src/discord/interactions/bridge.js similarity index 100% rename from discord/interactions/bridge.js rename to src/discord/interactions/bridge.js diff --git a/discord/interactions/invite.js b/src/discord/interactions/invite.js similarity index 100% rename from discord/interactions/invite.js rename to src/discord/interactions/invite.js diff --git a/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js similarity index 100% rename from discord/interactions/matrix-info.js rename to src/discord/interactions/matrix-info.js diff --git a/discord/interactions/permissions.js b/src/discord/interactions/permissions.js similarity index 100% rename from discord/interactions/permissions.js rename to src/discord/interactions/permissions.js diff --git a/discord/interactions/reactions.js b/src/discord/interactions/reactions.js similarity index 100% rename from discord/interactions/reactions.js rename to src/discord/interactions/reactions.js diff --git a/discord/register-interactions.js b/src/discord/register-interactions.js similarity index 100% rename from discord/register-interactions.js rename to src/discord/register-interactions.js diff --git a/discord/utils.js b/src/discord/utils.js similarity index 100% rename from discord/utils.js rename to src/discord/utils.js diff --git a/discord/utils.test.js b/src/discord/utils.test.js similarity index 100% rename from discord/utils.test.js rename to src/discord/utils.test.js diff --git a/m2d/actions/add-reaction.js b/src/m2d/actions/add-reaction.js similarity index 100% rename from m2d/actions/add-reaction.js rename to src/m2d/actions/add-reaction.js diff --git a/m2d/actions/channel-webhook.js b/src/m2d/actions/channel-webhook.js similarity index 100% rename from m2d/actions/channel-webhook.js rename to src/m2d/actions/channel-webhook.js diff --git a/m2d/actions/emoji-sheet.js b/src/m2d/actions/emoji-sheet.js similarity index 100% rename from m2d/actions/emoji-sheet.js rename to src/m2d/actions/emoji-sheet.js diff --git a/m2d/actions/redact.js b/src/m2d/actions/redact.js similarity index 100% rename from m2d/actions/redact.js rename to src/m2d/actions/redact.js diff --git a/m2d/actions/send-event.js b/src/m2d/actions/send-event.js similarity index 100% rename from m2d/actions/send-event.js rename to src/m2d/actions/send-event.js diff --git a/m2d/converters/emoji-sheet.js b/src/m2d/converters/emoji-sheet.js similarity index 100% rename from m2d/converters/emoji-sheet.js rename to src/m2d/converters/emoji-sheet.js diff --git a/m2d/converters/emoji-sheet.test.js b/src/m2d/converters/emoji-sheet.test.js similarity index 100% rename from m2d/converters/emoji-sheet.test.js rename to src/m2d/converters/emoji-sheet.test.js diff --git a/m2d/converters/emoji.js b/src/m2d/converters/emoji.js similarity index 100% rename from m2d/converters/emoji.js rename to src/m2d/converters/emoji.js diff --git a/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js similarity index 100% rename from m2d/converters/event-to-message.js rename to src/m2d/converters/event-to-message.js diff --git a/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js similarity index 100% rename from m2d/converters/event-to-message.test.js rename to src/m2d/converters/event-to-message.test.js diff --git a/m2d/converters/utils.js b/src/m2d/converters/utils.js similarity index 100% rename from m2d/converters/utils.js rename to src/m2d/converters/utils.js diff --git a/m2d/converters/utils.test.js b/src/m2d/converters/utils.test.js similarity index 100% rename from m2d/converters/utils.test.js rename to src/m2d/converters/utils.test.js diff --git a/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js similarity index 100% rename from m2d/event-dispatcher.js rename to src/m2d/event-dispatcher.js diff --git a/matrix/api.js b/src/matrix/api.js similarity index 100% rename from matrix/api.js rename to src/matrix/api.js diff --git a/matrix/api.test.js b/src/matrix/api.test.js similarity index 100% rename from matrix/api.test.js rename to src/matrix/api.test.js diff --git a/matrix/appservice.js b/src/matrix/appservice.js similarity index 100% rename from matrix/appservice.js rename to src/matrix/appservice.js diff --git a/matrix/file.js b/src/matrix/file.js similarity index 100% rename from matrix/file.js rename to src/matrix/file.js diff --git a/matrix/file.test.js b/src/matrix/file.test.js similarity index 100% rename from matrix/file.test.js rename to src/matrix/file.test.js diff --git a/matrix/kstate.js b/src/matrix/kstate.js similarity index 100% rename from matrix/kstate.js rename to src/matrix/kstate.js diff --git a/matrix/kstate.test.js b/src/matrix/kstate.test.js similarity index 100% rename from matrix/kstate.test.js rename to src/matrix/kstate.test.js diff --git a/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js similarity index 100% rename from matrix/matrix-command-handler.js rename to src/matrix/matrix-command-handler.js diff --git a/matrix/mreq.js b/src/matrix/mreq.js similarity index 100% rename from matrix/mreq.js rename to src/matrix/mreq.js diff --git a/matrix/power.js b/src/matrix/power.js similarity index 100% rename from matrix/power.js rename to src/matrix/power.js diff --git a/matrix/power.test.js b/src/matrix/power.test.js similarity index 100% rename from matrix/power.test.js rename to src/matrix/power.test.js diff --git a/matrix/read-registration.js b/src/matrix/read-registration.js similarity index 100% rename from matrix/read-registration.js rename to src/matrix/read-registration.js diff --git a/matrix/read-registration.test.js b/src/matrix/read-registration.test.js similarity index 100% rename from matrix/read-registration.test.js rename to src/matrix/read-registration.test.js diff --git a/matrix/txnid.js b/src/matrix/txnid.js similarity index 100% rename from matrix/txnid.js rename to src/matrix/txnid.js diff --git a/matrix/txnid.test.js b/src/matrix/txnid.test.js similarity index 100% rename from matrix/txnid.test.js rename to src/matrix/txnid.test.js diff --git a/passthrough.js b/src/passthrough.js similarity index 100% rename from passthrough.js rename to src/passthrough.js diff --git a/stdin.js b/src/stdin.js similarity index 100% rename from stdin.js rename to src/stdin.js diff --git a/types.d.ts b/src/types.d.ts similarity index 100% rename from types.d.ts rename to src/types.d.ts diff --git a/types.js b/src/types.js similarity index 100% rename from types.js rename to src/types.js From 915c34646afe220c83077dfdd3d92d88f1ece355 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Sep 2024 17:11:35 +1200 Subject: [PATCH 084/346] Fix references from moving all those files --- .gitignore | 2 +- src/d2m/actions/create-room.test.js | 2 +- src/d2m/actions/create-space.test.js | 2 +- src/d2m/actions/register-user.test.js | 2 +- src/d2m/converters/edit-to-changes.test.js | 2 +- src/d2m/converters/emoji-to-key.test.js | 2 +- .../message-to-event.embeds.test.js | 2 +- .../converters/message-to-event.pk.test.js | 2 +- src/d2m/converters/message-to-event.test.js | 2 +- src/d2m/converters/pins-to-list.test.js | 2 +- .../converters/thread-to-announcement.test.js | 2 +- src/d2m/converters/user-to-mxid.test.js | 2 +- .../0011-move-bridge-bot-to-real-id.up.js | 2 +- src/db/migrations/0013-media-proxy.sql | 8 +++ src/db/orm.test.js | 2 +- src/discord/discord-command-handler.js | 4 +- src/discord/register-interactions.js | 2 +- src/discord/utils.test.js | 2 +- src/m2d/converters/event-to-message.test.js | 2 +- src/stdin.js | 4 +- start.js | 18 +++--- test/test.js | 58 +++++++++---------- 22 files changed, 67 insertions(+), 59 deletions(-) create mode 100644 src/db/migrations/0013-media-proxy.sql diff --git a/.gitignore b/.gitignore index 9c175d8..3781b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ node_modules config.js registration.yaml coverage -db/ooye.db* +src/db/ooye.db* test/res/* !test/res/lottie* diff --git a/src/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js index d4391a9..a1766dd 100644 --- a/src/d2m/actions/create-room.test.js +++ b/src/d2m/actions/create-room.test.js @@ -4,7 +4,7 @@ const mixin = require("@cloudrac3r/mixin-deep") const {channelToKState, _convertNameAndTopic} = require("./create-room") const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") -const testData = require("../../test/data") +const testData = require("../../../test/data") const passthrough = require("../../passthrough") const {db} = passthrough diff --git a/src/d2m/actions/create-space.test.js b/src/d2m/actions/create-space.test.js index c4111db..cb4d90a 100644 --- a/src/d2m/actions/create-space.test.js +++ b/src/d2m/actions/create-space.test.js @@ -4,7 +4,7 @@ const mixin = require("@cloudrac3r/mixin-deep") const {guildToKState, ensureSpace} = require("./create-space") const {kstateStripConditionals, kstateUploadMxc} = require("../../matrix/kstate") const {test} = require("supertape") -const testData = require("../../test/data") +const testData = require("../../../test/data") const passthrough = require("../../passthrough") const {db} = passthrough diff --git a/src/d2m/actions/register-user.test.js b/src/d2m/actions/register-user.test.js index 96c73aa..353c89f 100644 --- a/src/d2m/actions/register-user.test.js +++ b/src/d2m/actions/register-user.test.js @@ -1,6 +1,6 @@ const {_memberToStateContent} = require("./register-user") const {test} = require("supertape") -const testData = require("../../test/data") +const testData = require("../../../test/data") test("member2state: without member nick or avatar", async t => { t.deepEqual( diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index effb777..b561bb1 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -1,6 +1,6 @@ const {test} = require("supertape") const {editToChanges} = require("./edit-to-changes") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") test("edit2changes: edit by webhook", async t => { diff --git a/src/d2m/converters/emoji-to-key.test.js b/src/d2m/converters/emoji-to-key.test.js index 5af046c..544eada 100644 --- a/src/d2m/converters/emoji-to-key.test.js +++ b/src/d2m/converters/emoji-to-key.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const {emojiToKey} = require("./emoji-to-key") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") test("emoji2key: unicode emoji works", async t => { diff --git a/src/d2m/converters/message-to-event.embeds.test.js b/src/d2m/converters/message-to-event.embeds.test.js index 05e3b5d..ef7e9b8 100644 --- a/src/d2m/converters/message-to-event.embeds.test.js +++ b/src/d2m/converters/message-to-event.embeds.test.js @@ -1,6 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") test("message2event embeds: nothing but a field", async t => { diff --git a/src/d2m/converters/message-to-event.pk.test.js b/src/d2m/converters/message-to-event.pk.test.js index c490469..ce83d54 100644 --- a/src/d2m/converters/message-to-event.pk.test.js +++ b/src/d2m/converters/message-to-event.pk.test.js @@ -1,6 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") /** diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 02fb4bb..3df1130 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1,6 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") /** diff --git a/src/d2m/converters/pins-to-list.test.js b/src/d2m/converters/pins-to-list.test.js index 8a6daea..7ee89b6 100644 --- a/src/d2m/converters/pins-to-list.test.js +++ b/src/d2m/converters/pins-to-list.test.js @@ -1,5 +1,5 @@ const {test} = require("supertape") -const data = require("../../test/data") +const data = require("../../../test/data") const {pinsToList} = require("./pins-to-list") test("pins2list: converts known IDs, ignores unknown IDs", t => { diff --git a/src/d2m/converters/thread-to-announcement.test.js b/src/d2m/converters/thread-to-announcement.test.js index 3bbca20..471cd94 100644 --- a/src/d2m/converters/thread-to-announcement.test.js +++ b/src/d2m/converters/thread-to-announcement.test.js @@ -1,6 +1,6 @@ const {test} = require("supertape") const {threadToAnnouncement} = require("./thread-to-announcement") -const data = require("../../test/data") +const data = require("../../../test/data") const Ty = require("../../types") /** diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index 5787b64..86f151b 100644 --- a/src/d2m/converters/user-to-mxid.test.js +++ b/src/d2m/converters/user-to-mxid.test.js @@ -1,7 +1,7 @@ const {test} = require("supertape") const tryToCatch = require("try-to-catch") const assert = require("assert") -const data = require("../../test/data") +const data = require("../../../test/data") const {userToSimName} = require("./user-to-mxid") test("user2name: cannot create user for a webhook", async t => { diff --git a/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js b/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js index 1808fcd..6bcf2c7 100644 --- a/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js +++ b/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js @@ -10,7 +10,7 @@ */ module.exports = async function(db) { - const config = require("../../config") + const config = require("../../../config") const id = Buffer.from(config.discordToken.split(".")[0], "base64").toString() db.prepare("UPDATE OR REPLACE sim SET user_id = ? WHERE user_id = '0'").run(id) } diff --git a/src/db/migrations/0013-media-proxy.sql b/src/db/migrations/0013-media-proxy.sql new file mode 100644 index 0000000..d55f059 --- /dev/null +++ b/src/db/migrations/0013-media-proxy.sql @@ -0,0 +1,8 @@ +BEGIN TRANSACTION; + +CREATE TABLE "media_proxy" ( + "permitted_hash" INTEGER NOT NULL, + PRIMARY KEY("permitted_hash") +) WITHOUT ROWID; + +COMMIT; diff --git a/src/db/orm.test.js b/src/db/orm.test.js index 3d4e8ef..a53cc66 100644 --- a/src/db/orm.test.js +++ b/src/db/orm.test.js @@ -1,7 +1,7 @@ // @ts-check const {test} = require("supertape") -const data = require("../test/data") +const data = require("../../test/data") const {db, select, from} = require("../passthrough") diff --git a/src/discord/discord-command-handler.js b/src/discord/discord-command-handler.js index d9ad80f..f0a4b49 100644 --- a/src/discord/discord-command-handler.js +++ b/src/discord/discord-command-handler.js @@ -4,7 +4,7 @@ const assert = require("assert").strict const util = require("util") const DiscordTypes = require("discord-api-types/v10") const {reg} = require("../matrix/read-registration") -const {addbot} = require("../addbot") +const {addbot} = require("../../addbot") const {discord, sync, db, select} = require("../passthrough") /** @type {import("../matrix/api")}) */ @@ -12,7 +12,7 @@ const api = sync.require("../matrix/api") /** @type {import("../matrix/file")} */ const file = sync.require("../matrix/file") /** @type {import("../m2d/converters/utils")} */ -const mxUtils = sync.require("../matrix/utils") +const mxUtils = sync.require("../m2d/converters/utils") /** @type {import("../d2m/actions/create-space")} */ const createSpace = sync.require("../d2m/actions/create-space") /** @type {import("./utils")} */ diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index 026ac7a..c66bef3 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -2,7 +2,7 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, db, select} = require("../passthrough") -const {id} = require("../addbot") +const {id} = require("../../addbot") const matrixInfo = sync.require("./interactions/matrix-info.js") const invite = sync.require("./interactions/invite.js") diff --git a/src/discord/utils.test.js b/src/discord/utils.test.js index 815c0a1..7c5f0c8 100644 --- a/src/discord/utils.test.js +++ b/src/discord/utils.test.js @@ -1,6 +1,6 @@ const DiscordTypes = require("discord-api-types/v10") const {test} = require("supertape") -const data = require("../test/data") +const data = require("../../test/data") const utils = require("./utils") test("is webhook message: identifies bot interaction response as not a message", t => { diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index d6a20cd..b674845 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -3,7 +3,7 @@ const fs = require("fs") const {test} = require("supertape") const {eventToMessage} = require("./event-to-message") const {convertImageStream} = require("./emoji-sheet") -const data = require("../../test/data") +const data = require("../../../test/data") const {MatrixServerError} = require("../../matrix/mreq") const {select, discord} = require("../../passthrough") diff --git a/src/stdin.js b/src/stdin.js index 780b9dc..cabd8ce 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -2,12 +2,12 @@ const repl = require("repl") const util = require("util") -const {addbot} = require("./addbot") +const {addbot} = require("../addbot") const passthrough = require("./passthrough") const {discord, config, sync, db} = passthrough -const data = sync.require("./test/data") +const data = sync.require("../test/data") const createSpace = sync.require("./d2m/actions/create-space") const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") diff --git a/start.js b/start.js index 1ece1dd..d6bd4c9 100644 --- a/start.js +++ b/start.js @@ -1,32 +1,32 @@ // @ts-check const sqlite = require("better-sqlite3") -const migrate = require("./db/migrate") +const migrate = require("./src/db/migrate") const HeatSync = require("heatsync") const config = require("./config") -const passthrough = require("./passthrough") -const db = new sqlite("db/ooye.db") +const passthrough = require("./src/passthrough") +const db = new sqlite("src/db/ooye.db") /** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() Object.assign(passthrough, {config, sync, db}) -const DiscordClient = require("./d2m/discord-client") +const DiscordClient = require("./src/d2m/discord-client") const discord = new DiscordClient(config.discordToken, "full") passthrough.discord = discord -const {as} = require("./matrix/appservice") +const {as} = require("./src/matrix/appservice") passthrough.as = as -const orm = sync.require("./db/orm") +const orm = sync.require("./src/db/orm") passthrough.from = orm.from passthrough.select = orm.select -const power = require("./matrix/power.js") -sync.require("./m2d/event-dispatcher") +const power = require("./src/matrix/power.js") +sync.require("./src/m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) @@ -34,5 +34,5 @@ sync.require("./m2d/event-dispatcher") console.log("Discord gateway started") await power.applyPower() - require("./stdin") + require("./src/stdin") })() diff --git a/test/test.js b/test/test.js index c2b703d..c898ad1 100644 --- a/test/test.js +++ b/test/test.js @@ -4,7 +4,7 @@ const fs = require("fs") const {join} = require("path") const stp = require("stream").promises const sqlite = require("better-sqlite3") -const migrate = require("../db/migrate") +const migrate = require("../src/db/migrate") const HeatSync = require("heatsync") const {test} = require("supertape") const data = require("./data") @@ -14,10 +14,10 @@ const fetch = require("node-fetch") const {green} = require("colorette") const config = require("../config") -const passthrough = require("../passthrough") +const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") -const {reg} = require("../matrix/read-registration") +const {reg} = require("../src/matrix/read-registration") reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server @@ -48,11 +48,11 @@ const discord = { Object.assign(passthrough, { discord, config, sync, db }) -const orm = sync.require("../db/orm") +const orm = sync.require("../src/db/orm") passthrough.from = orm.from passthrough.select = orm.select -const file = sync.require("../matrix/file") +const file = sync.require("../src/matrix/file") /* c8 ignore next */ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) } @@ -113,28 +113,28 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not db.exec(fs.readFileSync(join(__dirname, "ooye-test-data.sql"), "utf8")) - require("../db/orm.test") - require("../discord/utils.test") - require("../matrix/kstate.test") - require("../matrix/api.test") - require("../matrix/file.test") - //require("../matrix/power.test") - require("../matrix/read-registration.test") - require("../matrix/txnid.test") - require("../d2m/actions/create-room.test") - require("../d2m/actions/create-space.test") - require("../d2m/actions/register-user.test") - require("../d2m/converters/edit-to-changes.test") - require("../d2m/converters/emoji-to-key.test") - require("../d2m/converters/lottie.test") - require("../d2m/converters/message-to-event.test") - require("../d2m/converters/message-to-event.embeds.test") - require("../d2m/converters/message-to-event.pk.test") - require("../d2m/converters/pins-to-list.test") - require("../d2m/converters/remove-reaction.test") - require("../d2m/converters/thread-to-announcement.test") - require("../d2m/converters/user-to-mxid.test") - require("../m2d/converters/event-to-message.test") - require("../m2d/converters/utils.test") - require("../m2d/converters/emoji-sheet.test") + require("../src/db/orm.test") + require("../src/discord/utils.test") + require("../src/matrix/kstate.test") + require("../src/matrix/api.test") + require("../src/matrix/file.test") + require("../src/matrix/power.test") + require("../src/matrix/read-registration.test") + require("../src/matrix/txnid.test") + require("../src/d2m/actions/create-room.test") + require("../src/d2m/actions/create-space.test") + require("../src/d2m/actions/register-user.test") + require("../src/d2m/converters/edit-to-changes.test") + require("../src/d2m/converters/emoji-to-key.test") + require("../src/d2m/converters/lottie.test") + require("../src/d2m/converters/message-to-event.test") + require("../src/d2m/converters/message-to-event.embeds.test") + require("../src/d2m/converters/message-to-event.pk.test") + require("../src/d2m/converters/pins-to-list.test") + require("../src/d2m/converters/remove-reaction.test") + require("../src/d2m/converters/thread-to-announcement.test") + require("../src/d2m/converters/user-to-mxid.test") + require("../src/m2d/converters/event-to-message.test") + require("../src/m2d/converters/utils.test") + require("../src/m2d/converters/emoji-sheet.test") })() From eaa3b87670f284d3abc4731211af82c9cd9a4609 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Sep 2024 17:18:15 +1200 Subject: [PATCH 085/346] Fix sticker names being accumulated --- src/d2m/actions/expression.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/actions/expression.js b/src/d2m/actions/expression.js index b7b5d5a..b81f4ed 100644 --- a/src/d2m/actions/expression.js +++ b/src/d2m/actions/expression.js @@ -66,7 +66,7 @@ async function stickersToState(stickers) { while (shortcodes.includes(shortcode)) shortcode = shortcode + "~" shortcodes.push(shortcode) - result.images[shortcodes] = { + result.images[shortcode] = { info: { mimetype: file.stickerFormat.get(sticker.format_type)?.mime || "image/png" }, From b45d0f30388bc736990b9d3f87909585dc3193fa Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Sep 2024 03:16:03 +1200 Subject: [PATCH 086/346] Barebones matrix media proxy --- package-lock.json | 4 +- package.json | 4 +- scripts/seed.js | 133 +++++++++++++++++++++--------- src/db/orm-defs.d.ts | 4 + src/matrix/read-registration.js | 7 +- src/types.d.ts | 3 + src/web/routes/download-matrix.js | 48 +++++++++++ src/web/server.js | 5 ++ start.js | 5 +- 9 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 src/web/routes/download-matrix.js create mode 100644 src/web/server.js diff --git a/package-lock.json b/package-lock.json index 3aae6aa..bf6f81c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "enquirer": "^2.4.1", "entities": "^5.0.0", "get-stream": "^6.0.1", + "h3": "^1.12.0", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", "minimist": "^1.2.8", @@ -34,7 +35,8 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", - "xxhash-wasm": "^1.0.2" + "xxhash-wasm": "^1.0.2", + "zod": "^3.23.8" }, "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.2", diff --git a/package.json b/package.json index 8fccd08..69101c8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "enquirer": "^2.4.1", "entities": "^5.0.0", "get-stream": "^6.0.1", + "h3": "^1.12.0", "heatsync": "^2.5.3", "js-yaml": "^4.1.0", "minimist": "^1.2.8", @@ -43,7 +44,8 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", - "xxhash-wasm": "^1.0.2" + "xxhash-wasm": "^1.0.2", + "zod": "^3.23.8" }, "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.2", diff --git a/scripts/seed.js b/scripts/seed.js index ce5cfaf..01fc3e3 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -3,48 +3,63 @@ const assert = require("assert").strict const fs = require("fs") const sqlite = require("better-sqlite3") -const {scheduler: {wait}} = require("timers/promises") +const {scheduler} = require("timers/promises") const {isDeepStrictEqual} = require("util") +const {createServer} = require("http") const {prompt} = require("enquirer") const Input = require("enquirer/lib/prompts/input") const fetch = require("node-fetch") const {magenta, bold, cyan} = require("ansi-colors") const HeatSync = require("heatsync") +const {SnowTransfer} = require("snowtransfer") +const {createApp, defineEventHandler, toNodeListener} = require("h3") const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]}) +// Move database file if it's still in the old location +if (fs.existsSync("db")) { + if (fs.existsSync("db/ooye.db")) { + fs.renameSync("db/ooye.db", "src/db/ooye.db") + } + const files = fs.readdirSync("db") + if (files.length) { + console.error("You must manually move or delete the files in the db folder:") + for (const file of files) { + console.error(file) + } + process.exit(1) + } + fs.rmSync("db", {recursive: true}) +} + const config = require("../config") -const passthrough = require("../passthrough") -const db = new sqlite("db/ooye.db") -const migrate = require("../db/migrate") +const passthrough = require("../src/passthrough") +const db = new sqlite("src/db/ooye.db") +const migrate = require("../src/db/migrate") const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { sync, config, db }) -const orm = sync.require("../db/orm") +const orm = sync.require("../src/db/orm") passthrough.from = orm.from passthrough.select = orm.select -const DiscordClient = require("../d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "no") -passthrough.discord = discord - -let registration = require("../matrix/read-registration") -let {reg, getTemplateRegistration, writeRegistration, readRegistration, registrationFilePath} = registration +let registration = require("../src/matrix/read-registration") +let {reg, getTemplateRegistration, writeRegistration, readRegistration, checkRegistration, registrationFilePath} = registration function die(message) { console.error(message) process.exit(1) } -async function uploadAutoEmoji(guild, name, filename) { +async function uploadAutoEmoji(snow, guild, name, filename) { let emoji = guild.emojis.find(e => e.name === name) if (!emoji) { console.log(` Uploading ${name}...`) const data = fs.readFileSync(filename, null) - emoji = await discord.snow.guildAssets.createEmoji(guild.id, {name, image: "data:image/png;base64," + data.toString("base64")}) + emoji = await snow.guildAssets.createEmoji(guild.id, {name, image: "data:image/png;base64," + data.toString("base64")}) } else { console.log(` Reusing ${name}...`) } @@ -88,46 +103,90 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { name: "server_name", message: "Homeserver name" }) + console.log("What is the URL of your homeserver?") - const serverUrlPrompt = new Input({ + const serverOriginPrompt = new Input({ type: "input", name: "server_origin", message: "Homeserver URL", initial: () => `https://${serverNameResponse.server_name}`, - validate: url => validateHomeserverOrigin(serverUrlPrompt, url) + validate: url => validateHomeserverOrigin(serverOriginPrompt, url) }) - /** @type {{server_origin: string}} */ // @ts-ignore - const serverUrlResponse = await serverUrlPrompt.run() - console.log("Your Matrix homeserver needs to be able to send HTTP requests to OOYE.") - console.log("What URL should OOYE be reachable on? Usually, the default works fine,") - console.log("but you need to change this if you use multiple servers or containers.") - /** @type {{url: string}} */ - const urlResponse = await prompt({ + /** @type {string} */ // @ts-ignore + const serverOrigin = await serverOriginPrompt.run() + + const app = createApp() + app.use(defineEventHandler(() => "Out Of Your Element is listening.\n")) + const server = createServer(toNodeListener(app)) + await server.listen(6693) + + console.log("OOYE has its own web server. It needs to be accessible on the public internet.") + console.log("You need to enter a public URL where you will be able to host this web server.") + console.log("OOYE listens on localhost:6693, so you will probably have to set up a reverse proxy.") + console.log("Now listening on port 6693. Feel free to send some test requests.") + /** @type {{bridge_origin: string}} */ + const bridgeOriginResponse = await prompt({ type: "input", - name: "url", + name: "bridge_origin", message: "URL to reach OOYE", - initial: "http://localhost:6693", - validate: url => !!url.match(/^https?:\/\//) + initial: () => `https://bridge.${serverNameResponse.server_name}`, + validate: async url => { + process.stdout.write(magenta(" checking, please wait...")) + try { + const res = await fetch(url) + if (res.status !== 200) return `Server returned status code ${res.status}` + const text = await res.text() + if (text !== "Out Of Your Element is listening.\n") return `Server does not point to OOYE` + return true + } catch (e) { + return e.message + } + } + }) + + await server.close() + + console.log("What is your Discord bot token?") + /** @type {{discord_token: string}} */ + const discordTokenResponse = await prompt({ + type: "input", + name: "discord_token", + message: "Bot token", + validate: async token => { + process.stdout.write(magenta(" checking, please wait...")) + try { + const snow = new SnowTransfer(token) + await snow.user.getSelf() + return true + } catch (e) { + return e.message + } + } }) const template = getTemplateRegistration() - reg = {...template, ...urlResponse, ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}} + reg = {...template, url: bridgeOriginResponse.bridge_origin, ooye: {...template.ooye, ...serverNameResponse, ...bridgeOriginResponse, server_origin: serverOrigin, ...discordTokenResponse}} registration.reg = reg + checkRegistration(reg) writeRegistration(reg) + console.log(`✅ Registration file saved as ${registrationFilePath}`) + } else { + console.log(`✅ Valid registration file found at ${registrationFilePath}`) } - - // Done with user prompts, reg is now guaranteed to be valid - const api = require("../matrix/api") - const file = require("../matrix/file") - const utils = require("../m2d/converters/utils") - - console.log(`✅ Registration file saved as ${registrationFilePath}`) console.log(` In ${cyan("Synapse")}, you need to add it to homeserver.yaml and ${cyan("restart Synapse")}.`) console.log(" https://element-hq.github.io/synapse/latest/application_services.html") console.log(` In ${cyan("Conduit")}, you need to send the file contents to the #admins room.`) console.log(" https://docs.conduit.rs/appservices.html") console.log() - const {as} = require("../matrix/appservice") + // Done with user prompts, reg is now guaranteed to be valid + const api = require("../src/matrix/api") + const file = require("../src/matrix/file") + const utils = require("../src/m2d/converters/utils") + const DiscordClient = require("../src/d2m/discord-client") + const discord = new DiscordClient(reg.ooye.discord_token, "no") + passthrough.discord = discord + + const {as} = require("../src/matrix/appservice") console.log("⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)") let itWorks = false @@ -150,7 +209,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } else { process.stderr.write(".") } - await wait(5000) + await scheduler.wait(5000) } } while (!itWorks) console.log("") @@ -228,8 +287,8 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } // Upload those emojis to the chosen location db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id) - await uploadAutoEmoji(guild, "L1", "docs/img/L1.png") - await uploadAutoEmoji(guild, "L2", "docs/img/L2.png") + await uploadAutoEmoji(discord.snow, guild, "L1", "docs/img/L1.png") + await uploadAutoEmoji(discord.snow, guild, "L2", "docs/img/L2.png") } console.log("✅ Emojis are ready...") diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index 7484d76..a0b6e51 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -100,6 +100,10 @@ export type Models = { emoji_id: string guild_id: string } + + media_proxy: { + permitted_hash: number + } } export type Prepared = { diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 7172273..9efffdf 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -28,9 +28,9 @@ function writeRegistration(reg) { /** @returns {import("../types").InitialAppServiceRegistrationConfig} reg */ function getTemplateRegistration() { return { - id: crypto.randomBytes(16).toString("hex"), - as_token: crypto.randomBytes(16).toString("hex"), - hs_token: crypto.randomBytes(16).toString("hex"), + id: "ooye", + as_token: crypto.randomBytes(32).toString("hex"), + hs_token: crypto.randomBytes(32).toString("hex"), namespaces: { users: [{ exclusive: true, @@ -46,6 +46,7 @@ function getTemplateRegistration() { ], sender_localpart: "_ooye_bot", rate_limited: false, + socket: 6693, ooye: { namespace_prefix: "_ooye_", max_file_size: 5000000, diff --git a/src/types.d.ts b/src/types.d.ts index 93bfc75..53f9f30 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -16,12 +16,14 @@ export type AppServiceRegistrationConfig = { } protocols: [string] rate_limited: boolean + socket?: string | number, ooye: { namespace_prefix: string max_file_size: number server_name: string server_origin: string bridge_origin: string + discord_token: string content_length_workaround: boolean include_user_id_in_mxid: boolean invite: string[] @@ -49,6 +51,7 @@ export type InitialAppServiceRegistrationConfig = { } protocols: [string] rate_limited: boolean + socket?: string | number, ooye: { namespace_prefix: string max_file_size: number, diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js new file mode 100644 index 0000000..79f6a0e --- /dev/null +++ b/src/web/routes/download-matrix.js @@ -0,0 +1,48 @@ +// @ts-check + +const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError} = require("h3") +const {z} = require("zod") +const fetch = require("node-fetch") + +/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore +let hasher = null +// @ts-ignore +require("xxhash-wasm")().then(h => hasher = h) + +const {reg} = require("../../matrix/read-registration") +const {as, select} = require("../../passthrough") + +const schema = { + params: z.object({ + server_name: z.string(), + media_id: z.string() + }) +} + +as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => { + const params = await getValidatedRouterParams(event, schema.params.parse) + + const serverAndMediaID = `${params.server_name}/${params.media_id}` + const unsignedHash = hasher.h64(serverAndMediaID) + const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range + + const row = select("media_proxy", "permitted_hash", {permitted_hash: signedHash}).get() + if (row == null) { + throw createError({ + status: 403, + data: `The file you requested isn't permitted by this media proxy.` + }) + } + + const res = await fetch(`${reg.ooye.server_origin}/_matrix/client/v1/media/download/${params.server_name}/${params.media_id}`, { + headers: { + Authorization: `Bearer ${reg.as_token}` + } + }) + + setResponseStatus(event, res.status) + setResponseHeader(event, "Content-Type", res.headers.get("content-type")) + setResponseHeader(event, "Transfer-Encoding", "chunked") + + return sendStream(event, res.body) +})) diff --git a/src/web/server.js b/src/web/server.js new file mode 100644 index 0000000..7d77e62 --- /dev/null +++ b/src/web/server.js @@ -0,0 +1,5 @@ +// @ts-check + +const {sync, as} = require("../passthrough") + +sync.require("./routes/download-matrix") diff --git a/start.js b/start.js index d6bd4c9..0e9de14 100644 --- a/start.js +++ b/start.js @@ -15,7 +15,7 @@ Object.assign(passthrough, {config, sync, db}) const DiscordClient = require("./src/d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "full") +const discord = new DiscordClient(config.discordToken, "no") passthrough.discord = discord const {as} = require("./src/matrix/appservice") @@ -26,12 +26,13 @@ passthrough.from = orm.from passthrough.select = orm.select const power = require("./src/matrix/power.js") -sync.require("./src/m2d/event-dispatcher") +// sync.require("./src/m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) await discord.cloud.connect() console.log("Discord gateway started") + sync.require("./src/web/server") await power.applyPower() require("./src/stdin") From c0d92ea66d09823d38d902c90fe3a8382de2e04f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Sep 2024 17:19:42 +1200 Subject: [PATCH 087/346] Move calls from getPublicUrlForMxc to getMedia --- src/m2d/actions/emoji-sheet.js | 8 ++-- src/m2d/actions/send-event.js | 8 ++-- src/m2d/converters/event-to-message.js | 22 ++++----- src/m2d/converters/event-to-message.test.js | 52 +++++++++++---------- src/m2d/converters/utils.js | 6 +-- src/matrix/api.js | 17 +++++++ src/matrix/matrix-command-handler.js | 3 +- 7 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/m2d/actions/emoji-sheet.js b/src/m2d/actions/emoji-sheet.js index 5f96297..c81960d 100644 --- a/src/m2d/actions/emoji-sheet.js +++ b/src/m2d/actions/emoji-sheet.js @@ -8,6 +8,8 @@ const {sync} = require("../../passthrough") /** @type {import("../converters/emoji-sheet")} */ const emojiSheetConverter = sync.require("../converters/emoji-sheet") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") /** * Downloads the emoji from the web and converts to uncompressed PNG data. @@ -16,16 +18,12 @@ const emojiSheetConverter = sync.require("../converters/emoji-sheet") */ async function getAndConvertEmoji(mxc) { const abortController = new AbortController() - - const url = utils.getPublicUrlForMxc(mxc) - assert(url) - /** @type {import("node-fetch").Response} */ // If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing. // If we were using connection pooling, we would be forced to download the entire GIF. // So we set no agent to ensure we are not connection pooling. // @ts-ignore the signal is slightly different from the type it wants (still works fine) - const res = await fetch(url, {agent: false, signal: abortController.signal}) + const res = await api.getMedia(mxc, {agent: false, signal: abortController.signal}) return emojiSheetConverter.convertImageStream(res.body, () => { abortController.abort() res.body.pause() diff --git a/src/m2d/actions/send-event.js b/src/m2d/actions/send-event.js index 4422ad3..0a270a0 100644 --- a/src/m2d/actions/send-event.js +++ b/src/m2d/actions/send-event.js @@ -23,7 +23,7 @@ const editMessage = sync.require("../../d2m/actions/edit-message") const emojiSheet = sync.require("../actions/emoji-sheet") /** - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message * @returns {Promise} */ async function resolvePendingFiles(message) { @@ -39,7 +39,7 @@ async function resolvePendingFiles(message) { // Encrypted file const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url")) // @ts-ignore - fetch(p.url).then(res => res.body.pipe(d)) + await api.getMedia(p.mxc).then(res => res.body.pipe(d)) return { name: p.name, file: d @@ -47,7 +47,7 @@ async function resolvePendingFiles(message) { } else { // Unencrypted file /** @type {Readable} */ // @ts-ignore - const body = await fetch(p.url).then(res => res.body) + const body = await api.getMedia(p.mxc).then(res => res.body) return { name: p.name, file: body @@ -79,7 +79,7 @@ async function sendEvent(event) { // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it - let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch, mxcDownloader: emojiSheet.getAndConvertEmoji}) + let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, mxcDownloader: emojiSheet.getAndConvertEmoji}) messagesToEdit = await Promise.all(messagesToEdit.map(async e => { e.message = await resolvePendingFiles(e.message) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 76a32bc..346d8c9 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -305,7 +305,7 @@ function getUserOrProxyOwnerID(mxid) { * This function will strip them from the content and generate the correct pending file of the sprite sheet. * @param {string} content * @param {{id: string, name: string}[]} attachments - * @param {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles + * @param {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles * @param {(mxc: string) => Promise} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock. */ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) { @@ -389,7 +389,7 @@ async function handleRoomOrMessageLinks(input, di) { * @param {string} senderMxid * @param {string} roomID * @param {DiscordTypes.APIGuild} guild - * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di + * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer}} di */ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ @@ -440,7 +440,7 @@ const attachmentEmojis = new Map([ /** * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event * @param {import("discord-api-types/v10").APIGuild} guild - * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"], mxcDownloader: (mxc: string) => Promise}} di simple-as-nails dependency injection for the matrix API + * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, mxcDownloader: (mxc: string) => Promise}} di simple-as-nails dependency injection for the matrix API */ async function eventToMessage(event, guild, di) { let displayName = event.sender @@ -466,7 +466,7 @@ async function eventToMessage(event, guild, di) { let content = event.content.body // ultimate fallback const attachments = [] - /** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */ + /** @type {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */ const pendingFiles = [] /** @type {DiscordTypes.APIUser[]} */ const ensureJoined = [] @@ -767,29 +767,23 @@ async function eventToMessage(event, guild, di) { const description = (event.content.body !== event.content.filename && event.content.filename && event.content.body) || undefined if ("url" in event.content) { // Unencrypted - const url = mxUtils.getPublicUrlForMxc(event.content.url) - assert(url) attachments.push({id: "0", description, filename}) - pendingFiles.push({name: filename, url}) + pendingFiles.push({name: filename, mxc: event.content.url}) } else { // Encrypted - const url = mxUtils.getPublicUrlForMxc(event.content.file.url) - assert(url) assert.equal(event.content.file.key.alg, "A256CTR") attachments.push({id: "0", description, filename}) - pendingFiles.push({name: filename, url, key: event.content.file.key.k, iv: event.content.file.iv}) + pendingFiles.push({name: filename, mxc: event.content.file.url, key: event.content.file.key.k, iv: event.content.file.iv}) } } else if (event.type === "m.sticker") { content = "" - const url = mxUtils.getPublicUrlForMxc(event.content.url) - assert(url) let filename = event.content.body if (event.type === "m.sticker") { let mimetype if (event.content.info?.mimetype?.includes("/")) { mimetype = event.content.info.mimetype } else { - const res = await di.fetch(url, {method: "HEAD"}) + const res = await di.api.getMedia(event.content.url, {method: "HEAD"}) if (res.status === 200) { mimetype = res.headers.get("content-type") } @@ -798,7 +792,7 @@ async function eventToMessage(event, guild, di) { filename += "." + mimetype.split("/")[1] } attachments.push({id: "0", filename}) - pendingFiles.push({name: filename, url}) + pendingFiles.push({name: filename, mxc: event.content.url}) } content = displayNameRunoff + replyLine + content diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b674845..fdaad33 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -3550,7 +3550,7 @@ test("event2message: text attachments work", async t => { content: "", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: undefined, filename: "chiki-powerups.txt"}], - pendingFiles: [{name: "chiki-powerups.txt", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}] + pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}] }] } ) @@ -3586,7 +3586,7 @@ test("event2message: image attachments work", async t => { content: "", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: undefined, filename: "cool cat.png"}], - pendingFiles: [{name: "cool cat.png", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] + pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] } ) @@ -3622,7 +3622,7 @@ test("event2message: image attachments can have a custom description", async t = content: "", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: "Cat emoji surrounded by pink hearts", filename: "cool cat.png"}], - pendingFiles: [{name: "cool cat.png", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] + pendingFiles: [{name: "cool cat.png", url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] } ) @@ -3674,7 +3674,7 @@ test("event2message: encrypted image attachments work", async t => { attachments: [{id: "0", description: undefined, filename: "image.png"}], pendingFiles: [{ name: "image.png", - url: "https://matrix.cadence.moe/_matrix/media/r0/download/heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX", + mxc: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX", key: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo", iv: "Va9SHZpIn5kAAAAAAAAAAA" }] @@ -3717,7 +3717,7 @@ test("event2message: stickers work", async t => { content: "", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "get_real2.gif"}], - pendingFiles: [{name: "get_real2.gif", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/NyMXQFAAdniImbHzsygScbmN"}] + pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}] }] } ) @@ -3736,15 +3736,17 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, { - async fetch(url, options) { - called++ - t.equal(url, "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf") - t.equal(options.method, "HEAD") - return { - status: 200, - headers: new Map([ - ["content-type", "image/gif"] - ]) + api: { + async getMedia(mxc, options) { + called++ + t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf") + t.equal(options.method, "HEAD") + return { + status: 200, + headers: new Map([ + ["content-type", "image/gif"] + ]) + } } } }), @@ -3757,7 +3759,7 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi content: "", avatar_url: undefined, attachments: [{id: "0", filename: "YESYESYES.gif"}], - pendingFiles: [{name: "YESYESYES.gif", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}] + pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}] }] } ) @@ -3777,15 +3779,17 @@ test("event2message: stickers with unknown mimetype are not allowed", async t => event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, { - async fetch(url, options) { - called++ - t.equal(url, "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe") - t.equal(options.method, "HEAD") - return { - status: 404, - headers: new Map([ - ["content-type", "application/json"] - ]) + api: { + async getMedia(mxc, options) { + called++ + t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe") + t.equal(options.method, "HEAD") + return { + status: 404, + headers: new Map([ + ["content-type", "application/json"] + ]) + } } } }) diff --git a/src/m2d/converters/utils.js b/src/m2d/converters/utils.js index c1f5f01..c538627 100644 --- a/src/m2d/converters/utils.js +++ b/src/m2d/converters/utils.js @@ -223,10 +223,10 @@ async function getViaServersQuery(roomID, api) { */ function getPublicUrlForMxc(mxc) { assert(hasher, "xxhash is not ready yet") - const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) - if (!avatarURLParts) return null + const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + if (!mediaParts) return null - const serverAndMediaID = `${avatarURLParts[1]}/${avatarURLParts[2]}` + const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}` const unsignedHash = hasher.h64(serverAndMediaID) const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) diff --git a/src/matrix/api.js b/src/matrix/api.js index d33961d..2c5fd4e 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -297,6 +297,7 @@ async function setUserPowerCascade(roomID, mxid, power) { } async function ping() { + // not using mreq so that we can read the status code const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, { method: "POST", headers: { @@ -312,6 +313,21 @@ async function ping() { } } +/** + * @param {string} mxc + * @param {RequestInit} [init] + */ +function getMedia(mxc, init = {}) { + const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + assert(mediaParts) + return fetch(`${mreq.baseUrl}/client/v1/media/download/${mediaParts[1]}/${mediaParts[2]}`, { + headers: { + Authorization: `Bearer ${reg.as_token}` + }, + ...init + }) +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -336,3 +352,4 @@ module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.setUserPower = setUserPower module.exports.setUserPowerCascade = setUserPowerCascade module.exports.ping = ping +module.exports.getMedia = getMedia diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 7ad84f5..7a35e12 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -217,9 +217,8 @@ const commands = [{ } else { // Upload it to Discord and have the bridge sync it back to Matrix again for (const e of toUpload) { - const publicUrl = mxUtils.getPublicUrlForMxc(e.url) // @ts-ignore - const resizeInput = await fetch(publicUrl, {agent: false}).then(res => res.arrayBuffer()) + const resizeInput = await api.getMedia(e.url, {agent: false}).then(res => res.arrayBuffer()) const resizeOutput = await sharp(resizeInput) .resize(EMOJI_SIZE, EMOJI_SIZE, {fit: "inside", withoutEnlargement: true, background: {r: 0, g: 0, b: 0, alpha: 0}}) .png() From cc7831a313ec9103494c6d94c87dc5dd61a7f8f8 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Sep 2024 23:47:11 +1200 Subject: [PATCH 088/346] Remove token from config file --- addbot.js | 4 ++-- addbot.sh | 2 +- config.example.js | 3 --- readme.md | 7 +------ src/db/migrations/0011-move-bridge-bot-to-real-id.up.js | 3 +-- src/passthrough.js | 1 - src/stdin.js | 2 +- start.js | 8 ++++---- test/test.js | 3 +-- 9 files changed, 11 insertions(+), 22 deletions(-) delete mode 100644 config.example.js diff --git a/addbot.js b/addbot.js index 106ebc1..4ac55fc 100644 --- a/addbot.js +++ b/addbot.js @@ -1,7 +1,7 @@ // @ts-check -const config = require("./config") -const token = config.discordToken +const {reg} = require("./src/matrix/read-registration") +const token = reg.ooye.discord_token const id = Buffer.from(token.split(".")[0], "base64").toString() function addbot() { diff --git a/addbot.sh b/addbot.sh index 6c3ff4b..d40d0da 100755 --- a/addbot.sh +++ b/addbot.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh echo "Open this link to add the bot to a Discord server:" -echo "https://discord.com/oauth2/authorize?client_id=$(grep discordToken config.js | sed -E 's!.*: ["'\'']([A-Za-z0-9+=/_-]*).*!\1!g' | base64 -d)&scope=bot&permissions=1610883072" +echo "https://discord.com/oauth2/authorize?client_id=$(grep discord_token registration.yaml | sed -E 's!.*: ["'\'']([A-Za-z0-9+=/_-]*).*!\1!g' | base64 -d)&scope=bot&permissions=1610883072" diff --git a/config.example.js b/config.example.js deleted file mode 100644 index 0d1a29e..0000000 --- a/config.example.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - discordToken: "yes" -} diff --git a/readme.md b/readme.md index bc19046..7e51cf0 100644 --- a/readme.md +++ b/readme.md @@ -81,11 +81,7 @@ Follow these steps: 1. Install dependencies: `npm install` -1. Copy `config.example.js` to `config.js` and fill in Discord token. - -1. Copy `registration.example.yaml` to `registration.yaml` and fill in bracketed values. You could generate each hex string with `dd if=/dev/urandom bs=32 count=1 2> /dev/null | basenc --base16 | dd conv=lcase 2> /dev/null`. Register the registration in Synapse's `homeserver.yaml` through the usual appservice installation process, then restart Synapse. - -1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. You only need to run this once ever. +1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. 1. Start the bridge: `node start.js` @@ -113,7 +109,6 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * Run this to start the bridge: ├── start.js * Runtime configuration, like tokens and user info: - ├── config.js ├── registration.yaml * The bridge's SQLite database is stored here: ├── db diff --git a/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js b/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js index 6bcf2c7..3fab076 100644 --- a/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js +++ b/src/db/migrations/0011-move-bridge-bot-to-real-id.up.js @@ -10,7 +10,6 @@ */ module.exports = async function(db) { - const config = require("../../../config") - const id = Buffer.from(config.discordToken.split(".")[0], "base64").toString() + const id = require("../../../addbot").id db.prepare("UPDATE OR REPLACE sim SET user_id = ? WHERE user_id = '0'").run(id) } diff --git a/src/passthrough.js b/src/passthrough.js index 18cca6b..378f516 100644 --- a/src/passthrough.js +++ b/src/passthrough.js @@ -3,7 +3,6 @@ /** * @typedef {Object} Passthrough * @property {import("repl").REPLServer} repl - * @property {typeof import("./config")} config * @property {import("./d2m/discord-client")} discord * @property {import("heatsync").default} sync * @property {import("better-sqlite3/lib/database")} db diff --git a/src/stdin.js b/src/stdin.js index cabd8ce..9051395 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -5,7 +5,7 @@ const util = require("util") const {addbot} = require("../addbot") const passthrough = require("./passthrough") -const {discord, config, sync, db} = passthrough +const {discord, sync, db} = passthrough const data = sync.require("../test/data") const createSpace = sync.require("./d2m/actions/create-space") diff --git a/start.js b/start.js index 0e9de14..a9f639a 100644 --- a/start.js +++ b/start.js @@ -4,18 +4,18 @@ const sqlite = require("better-sqlite3") const migrate = require("./src/db/migrate") const HeatSync = require("heatsync") -const config = require("./config") +const {reg} = require("./src/matrix/read-registration") const passthrough = require("./src/passthrough") const db = new sqlite("src/db/ooye.db") /** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() -Object.assign(passthrough, {config, sync, db}) +Object.assign(passthrough, {sync, db}) const DiscordClient = require("./src/d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "no") +const discord = new DiscordClient(reg.ooye.discord_token) passthrough.discord = discord const {as} = require("./src/matrix/appservice") @@ -26,7 +26,7 @@ passthrough.from = orm.from passthrough.select = orm.select const power = require("./src/matrix/power.js") -// sync.require("./src/m2d/event-dispatcher") +sync.require("./src/m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) diff --git a/test/test.js b/test/test.js index c898ad1..ebe49c3 100644 --- a/test/test.js +++ b/test/test.js @@ -13,7 +13,6 @@ const data = require("./data") const fetch = require("node-fetch") const {green} = require("colorette") -const config = require("../config") const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") @@ -46,7 +45,7 @@ const discord = { ]) } -Object.assign(passthrough, { discord, config, sync, db }) +Object.assign(passthrough, { discord, sync, db }) const orm = sync.require("../src/db/orm") passthrough.from = orm.from From 555d7cb12671ffa8e3085d2b0bd28442ab5761af Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Sep 2024 23:48:07 +1200 Subject: [PATCH 089/346] Authmedia bug fixes --- src/discord/register-interactions.js | 1 - src/m2d/converters/event-to-message.test.js | 62 ++++++++++----------- src/matrix/api.js | 4 +- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index c66bef3..eeeab62 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -55,7 +55,6 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ async function dispatchInteraction(interaction) { const interactionId = interaction.data.custom_id || interaction.data.name try { - console.log(interaction) if (interactionId === "Matrix info") { await matrixInfo.interact(interaction) } else if (interactionId === "invite") { diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index fdaad33..a97fd26 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -1073,7 +1073,7 @@ test("event2message: rich reply to a sim user", async t => { content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " Slow news day." + "\nTesting this reply, ignore", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1225,7 +1225,7 @@ test("event2message: rich reply to an already-edited message will quote the new content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" + " this is the new content. heya!" + "\nhiiiii....", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1278,7 +1278,7 @@ test("event2message: rich reply to a missing event will quote from formatted_bod username: "cadence [they]", content: "-# > But who sees the seashells she sells sitting..." + "\nWhat a tongue-bender...", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1327,7 +1327,7 @@ test("event2message: rich reply to a missing event without formatted_body will u messagesToSend: [{ username: "cadence [they]", content: "Testing this reply, ignore", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1378,7 +1378,7 @@ test("event2message: rich reply to a missing event and no reply fallback will no messagesToSend: [{ username: "cadence [they]", content: "Testing this reply, ignore.", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1430,7 +1430,7 @@ test("event2message: should avoid using blockquote contents as reply preview in content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " that can't be true! there's no way :o" + "\nI agree!", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1568,7 +1568,7 @@ test("event2message: should include a reply preview when message ends with a blo content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜ_ooye_cookie**:" + " tanget: @..." + "\naichmophobia", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1652,7 +1652,7 @@ test("event2message: should include a reply preview when replying to a descripti content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" + " It looks like this queue has ended." + `\nso you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?`, - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1705,7 +1705,7 @@ test("event2message: entities are not escaped in main message or reply preview", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + " Testing? \"':.`[]&things" + "\n_Testing?_ \"':.\\`\\[\\]&things", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2037,7 +2037,7 @@ test("event2message: editing a rich reply to a sim user", async t => { content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " Slow news day." + "\nEditing this reply, which is also a test", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2093,7 +2093,7 @@ test("event2message: editing a plaintext body message", async t => { 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", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2148,7 +2148,7 @@ test("event2message: editing a plaintext message to be longer", async t => { message: { content: "aaaaaaaaa ".repeat(198) + "well, I guess it's", username: "cadence [they]", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2157,7 +2157,7 @@ test("event2message: editing a plaintext message to be longer", async t => { messagesToSend: [{ content: "no longer brand new... it's existed for mere seconds..." + ("aaaaaaaaa ".repeat(20)).slice(0, -1), username: "cadence [they]", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2210,7 +2210,7 @@ test("event2message: editing a plaintext message to be shorter", async t => { 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", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2271,7 +2271,7 @@ test("event2message: editing a formatted body message", async t => { 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", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2328,7 +2328,7 @@ test("event2message: rich reply to a matrix user's long message with formatting" content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 **Ⓜcadence [they]**:" + " i should have a little happy test list bold em..." + "\n**no you can't!!!**", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2390,7 +2390,7 @@ test("event2message: rich reply to an image", async t => { username: "cadence [they]", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + "\nCaught in 8K UHD VR QLED Epic Edition", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2446,7 +2446,7 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " [spoiler] cw crossword spoilers you'll never..." + "\nomg NO WAY!!", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2501,7 +2501,7 @@ test("event2message: with layered rich replies, the preview should only be the r content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 **Ⓜcadence [they]**:" + " two" + "\nthree", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2558,7 +2558,7 @@ test("event2message: if event is a reply and starts with a quote, they should be + " i have a feeling that clients are meant to strip..." + "\n" + "\n> To strip the fallback on the `body`, the client should iterate over each line of the string, removing any lines that start with the fallback prefix (\"> “, including the space, without quotes) and stopping when a line is encountered without the prefix. This prefix is known as the “fallback prefix sequence”.", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", + avatar_url: "https://bridge.example.org/download/matrix/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", allowed_mentions: { parse: ["users", "roles"] } @@ -2616,7 +2616,7 @@ test("event2message: rich reply to a deleted event", async t => { username: "Ampflower 🌺", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**ⓂAmpflower 🌺** (in reply to a deleted message)" + "\nHuh it did the same thing here too", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", allowed_mentions: { parse: ["users", "roles"] } @@ -3384,7 +3384,7 @@ test("event2message: caches the member if the member is not known", async t => { messagesToSend: [{ username: "should_be_newly_cached", content: "testing the member state cache", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/this_is_the_avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", allowed_mentions: { parse: ["users", "roles"] } @@ -3548,7 +3548,7 @@ test("event2message: text attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: undefined, filename: "chiki-powerups.txt"}], pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}] }] @@ -3584,7 +3584,7 @@ test("event2message: image attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: undefined, filename: "cool cat.png"}], pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] @@ -3620,9 +3620,9 @@ test("event2message: image attachments can have a custom description", async t = messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: "Cat emoji surrounded by pink hearts", filename: "cool cat.png"}], - pendingFiles: [{name: "cool cat.png", url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] + pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] } ) @@ -3670,7 +3670,7 @@ test("event2message: encrypted image attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", description: undefined, filename: "image.png"}], pendingFiles: [{ name: "image.png", @@ -3715,7 +3715,7 @@ test("event2message: stickers work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "get_real2.gif"}], pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}] }] @@ -3851,7 +3851,7 @@ test("event2message: emojis in other servers are reused if they have the same ti messagesToSend: [{ username: "cadence [they]", content: "<:hippo:230201364309868544>", - avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -3910,7 +3910,7 @@ test("event2message: unknown emojis in the middle are linked", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "a [:ms_robot_grin:](https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy) b", + content: "a [:ms_robot_grin:](https://bridge.example.org/download/matrix/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy) b", avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] @@ -4333,7 +4333,7 @@ slow()("event2message: known and unknown emojis in the end are reuploaded as a s fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64") } t.deepEqual(testResult, { - content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:", + content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:", fileName: "emojis.png", fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAeXRFWHRSYXcACklQVEMgcHJvZmlsZQogICAgICA0Ngoz" }) diff --git a/src/matrix/api.js b/src/matrix/api.js index 2c5fd4e..4b1c676 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -3,12 +3,12 @@ const Ty = require("../types") const assert = require("assert").strict +const fetch = require("node-fetch") + const passthrough = require("../passthrough") const { discord, sync, db } = passthrough /** @type {import("./mreq")} */ const mreq = sync.require("./mreq") -/** @type {import("./file")} */ -const file = sync.require("./file") /** @type {import("./txnid")} */ const makeTxnId = sync.require("./txnid") const {reg} = require("./read-registration.js") From d6dc5cb88f7380c0ae8fbcb70ed5a1001700279d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Sep 2024 00:58:21 +1200 Subject: [PATCH 090/346] Type fixes --- scripts/seed.js | 2 +- src/d2m/converters/remove-reaction.test.js | 4 +++- src/matrix/api.js | 4 ++-- src/web/routes/download-matrix.js | 13 ++++++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/scripts/seed.js b/scripts/seed.js index 01fc3e3..649f3fc 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -9,7 +9,7 @@ const {createServer} = require("http") const {prompt} = require("enquirer") const Input = require("enquirer/lib/prompts/input") -const fetch = require("node-fetch") +const fetch = require("node-fetch").default const {magenta, bold, cyan} = require("ansi-colors") const HeatSync = require("heatsync") const {SnowTransfer} = require("snowtransfer") diff --git a/src/d2m/converters/remove-reaction.test.js b/src/d2m/converters/remove-reaction.test.js index dc6eda5..b42c11a 100644 --- a/src/d2m/converters/remove-reaction.test.js +++ b/src/d2m/converters/remove-reaction.test.js @@ -10,7 +10,9 @@ function fakeSpecificReactionRemoval(userID, emoji, emojiID) { channel_id: "THE_CHANNEL", message_id: "THE_MESSAGE", user_id: userID, - emoji: {id: emojiID, name: emoji} + emoji: {id: emojiID, name: emoji}, + burst: false, + type: 0 } } diff --git a/src/matrix/api.js b/src/matrix/api.js index 4b1c676..4866495 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -3,7 +3,7 @@ const Ty = require("../types") const assert = require("assert").strict -const fetch = require("node-fetch") +const fetch = require("node-fetch").default const passthrough = require("../passthrough") const { discord, sync, db } = passthrough @@ -315,7 +315,7 @@ async function ping() { /** * @param {string} mxc - * @param {RequestInit} [init] + * @param {fetch.RequestInit} [init] */ function getMedia(mxc, init = {}) { const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index 79f6a0e..137af7c 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -1,8 +1,9 @@ // @ts-check +const assert = require("assert/strict") const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError} = require("h3") const {z} = require("zod") -const fetch = require("node-fetch") +const fetch = require("node-fetch").default /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null @@ -40,9 +41,11 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn } }) - setResponseStatus(event, res.status) - setResponseHeader(event, "Content-Type", res.headers.get("content-type")) - setResponseHeader(event, "Transfer-Encoding", "chunked") + const contentType = res.headers.get("content-type") + assert(contentType) - return sendStream(event, res.body) + setResponseStatus(event, res.status) + setResponseHeader(event, "Content-Type", contentType) + setResponseHeader(event, "Transfer-Encoding", "chunked") + return res.body })) From c6175e09f82290bc3624490e44e313c1a7ca4a7b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Sep 2024 01:39:41 +1200 Subject: [PATCH 091/346] Generate public url for linked discord attachments --- src/d2m/converters/edit-to-changes.test.js | 4 +- src/d2m/converters/message-to-event.js | 9 +-- src/d2m/converters/message-to-event.test.js | 10 ++-- src/discord/utils.js | 10 ++++ src/web/routes/download-discord.js | 64 +++++++++++++++++++++ src/web/server.js | 1 + 6 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 src/web/routes/download-discord.js diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index b561bb1..9721a85 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -99,9 +99,9 @@ test("edit2changes: change file type", async t => { t.deepEqual(eventsToRedact, ["$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ"]) t.deepEqual(eventsToSend, [{ $type: "m.room.message", - body: "📝 Uploaded file: https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/gaze_into_my_dark_mind.txt (20 MB)", + body: "📝 Uploaded file: https://bridge.example.org/download/discordcdn/112760669178241024/1141501302497615912/gaze_into_my_dark_mind.txt (20 MB)", format: "org.matrix.custom.html", - formatted_body: "📝 Uploaded file: gaze_into_my_dark_mind.txt (20 MB)", + formatted_body: "📝 Uploaded file: gaze_into_my_dark_mind.txt (20 MB)", "m.mentions": {}, msgtype: "m.text" }]) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 4614082..bd44537 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -103,6 +103,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ * @param {DiscordTypes.APIAttachment} attachment */ async function attachmentToEvent(mentions, attachment) { + const publicURL = dUtils.getPublicUrlForCdn(attachment.url) const emoji = attachment.content_type?.startsWith("image/jp") ? "📸" : attachment.content_type?.startsWith("image/") ? "🖼️" @@ -116,9 +117,9 @@ async function attachmentToEvent(mentions, attachment) { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", - body: `${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})`, + body: `${emoji} Uploaded SPOILER file: ${publicURL} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `
    ${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})
    ` + formatted_body: `
    ${emoji} Uploaded SPOILER file: ${publicURL} (${pb(attachment.size)})
    ` } } // for large files, always link them instead of uploading so I don't use up all the space in the content repo @@ -127,9 +128,9 @@ async function attachmentToEvent(mentions, attachment) { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", - body: `${emoji} Uploaded file: ${attachment.url} (${pb(attachment.size)})`, + body: `${emoji} Uploaded file: ${publicURL} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` + formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` } } else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { return { diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 3df1130..a76741f 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -305,9 +305,9 @@ test("message2event: spoiler attachment", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "📄 Uploaded SPOILER file: https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", + body: "📄 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", format: "org.matrix.custom.html", - formatted_body: "
    📄 Uploaded SPOILER file: https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)
    " + formatted_body: "
    📄 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)
    " }]) }) @@ -788,7 +788,7 @@ test("message2event: very large attachment is linked instead of being uploaded", content: "hey", attachments: [{ filename: "hey.jpg", - url: "https://discord.com/404/hey.jpg", + url: "https://cdn.discordapp.com/attachments/123/456/789.mega", content_type: "application/i-made-it-up", size: 100e6 }] @@ -802,9 +802,9 @@ test("message2event: very large attachment is linked instead of being uploaded", $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "📄 Uploaded file: https://discord.com/404/hey.jpg (100 MB)", + body: "📄 Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", - formatted_body: '📄 Uploaded file: hey.jpg (100 MB)' + formatted_body: '📄 Uploaded file: hey.jpg (100 MB)' }]) }) diff --git a/src/discord/utils.js b/src/discord/utils.js index 865b2e3..85d86cc 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -3,6 +3,8 @@ const DiscordTypes = require("discord-api-types/v10") const assert = require("assert").strict +const {reg} = require("../matrix/read-registration") + const EPOCH = 1420070400000 /** @@ -117,6 +119,13 @@ function timestampToSnowflakeInexact(timestamp) { return String((timestamp - EPOCH) * 2**22) } +/** @param {string} url */ +function getPublicUrlForCdn(url) { + const match = url.match(`https://cdn.discordapp.com/attachments/([0-9]+)/([0-9]+)/([-A-Za-z0-9_.,]+)`) + if (!match) return url + return `${reg.ooye.bridge_origin}/download/discordcdn/${match[1]}/${match[2]}/${match[3]}` +} + module.exports.getPermissions = getPermissions module.exports.hasPermission = hasPermission module.exports.hasSomePermissions = hasSomePermissions @@ -125,3 +134,4 @@ module.exports.isWebhookMessage = isWebhookMessage module.exports.isEphemeralMessage = isEphemeralMessage module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact +module.exports.getPublicUrlForCdn = getPublicUrlForCdn diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js new file mode 100644 index 0000000..f5871d3 --- /dev/null +++ b/src/web/routes/download-discord.js @@ -0,0 +1,64 @@ +// @ts-check + +const assert = require("assert/strict") +const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError} = require("h3") +const {z} = require("zod") + +const {discord, as, select} = require("../../passthrough") + +const schema = { + params: z.object({ + channel_id: z.string().regex(/^[0-9]+$/), + attachment_id: z.string().regex(/^[0-9]+$/), + file_name: z.string().regex(/^[-A-Za-z0-9_.,]+$/) + }) +} + +/** @type {Map>} */ +const cache = new Map() + +function hasExpired(url) { + const params = new URL(url).searchParams + const ex = params.get("ex") + assert(ex) // refreshed urls from the discord api always include this parameter + return parseInt(ex, 16) < Date.now() / 1000 +} + +// purge expired urls from cache every hour +setInterval(() => { + for (const entry of cache.entries()) { + if (hasExpired(entry[1])) cache.delete(entry[0]) + } + console.log(`purged discord media cache, it now has ${cache.size} urls`) +}, 60 * 60 * 1000).unref() + +as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineEventHandler(async event => { + const params = await getValidatedRouterParams(event, schema.params.parse) + + const row = select("channel_room", "channel_id", {channel_id: params.channel_id}).get() + if (row == null) { + throw createError({ + status: 403, + data: `The file you requested isn't permitted by this media proxy.` + }) + } + + const url = `https://cdn.discordapp.com/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` + let promise = cache.get(url) + let refreshed + if (promise) { + console.log("using existing cache entry") + refreshed = await promise + if (hasExpired(refreshed)) promise = undefined + console.log(promise) + } + if (!promise) { + console.log("refreshing and storing") + promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) + cache.set(url, promise) + refreshed = await promise + } + assert(refreshed) // will have been assigned by one of the above branches + + return sendRedirect(event, refreshed) +})) diff --git a/src/web/server.js b/src/web/server.js index 7d77e62..967885c 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -3,3 +3,4 @@ const {sync, as} = require("../passthrough") sync.require("./routes/download-matrix") +sync.require("./routes/download-discord") From adf07ad736f61578066abf17a51067427d11151c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Sep 2024 01:58:17 +1200 Subject: [PATCH 092/346] Generate public urls for attachments in message --- src/d2m/converters/message-to-event.js | 8 ++++++++ src/discord/utils.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index bd44537..5191e6e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -362,6 +362,13 @@ async function messageToEvent(message, guild, options = {}, di) { return content } + /** + * Translate Discord attachment links into links that go via the bridge, so they last forever. + */ + function transformAttachmentLinks(content) { + return content.replace(/https:\/\/cdn\.discord(?:app)?\.com\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/g, url => dUtils.getPublicUrlForCdn(url)) + } + /** * Translate links and emojis and mentions and stuff. Give back the text and HTML so they can be combined into bigger events. * @param {string} content Partial or complete Discord message content @@ -370,6 +377,7 @@ async function messageToEvent(message, guild, options = {}, di) { * @param {any} customHtmlOutput */ async function transformContent(content, customOptions = {}, customParser = null, customHtmlOutput = null) { + content = transformAttachmentLinks(content) content = await transformContentMessageLinks(content) // Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter. diff --git a/src/discord/utils.js b/src/discord/utils.js index 85d86cc..24a3c85 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -121,7 +121,7 @@ function timestampToSnowflakeInexact(timestamp) { /** @param {string} url */ function getPublicUrlForCdn(url) { - const match = url.match(`https://cdn.discordapp.com/attachments/([0-9]+)/([0-9]+)/([-A-Za-z0-9_.,]+)`) + const match = url.match(/https:\/\/cdn.discordapp.com\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) if (!match) return url return `${reg.ooye.bridge_origin}/download/discordcdn/${match[1]}/${match[2]}/${match[3]}` } From a6a1b7fb284d7152d5cdcb6fb9db9847d5a0437b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Sep 2024 02:17:13 +1200 Subject: [PATCH 093/346] Update dev overview in readme --- readme.md | 87 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/readme.md b/readme.md index 7e51cf0..695c26a 100644 --- a/readme.md +++ b/readme.md @@ -110,47 +110,52 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in ├── start.js * Runtime configuration, like tokens and user info: ├── registration.yaml - * The bridge's SQLite database is stored here: - ├── db - │   ├── *.sql, *.db - │   * Migrations change the database schema when you update to a newer version of OOYE: - │   └── migrations - │       └── *.sql, *.js - * Discord-to-Matrix bridging: - ├── d2m - │   * Execute actions through the whole flow, like sending a Discord message to Matrix: - │   ├── actions - │   │   └── *.js - │   * Convert data from one form to another without depending on bridge state. Called by actions: - │   ├── converters - │   │   └── *.js - │   * Making Discord work: - │   ├── discord-*.js - │   * Listening to events from Discord and dispatching them to the correct `action`: - │   └── event-dispatcher.js - ├── discord - │   └── discord-command-handler.js - * Matrix-to-Discord bridging: - ├── m2d - │   * Execute actions through the whole flow, like sending a Matrix message to Discord: - │   ├── actions - │   │   └── *.js - │   * Convert data from one form to another without depending on bridge state. Called by actions: - │   ├── converters - │   │   └── *.js - │   * Listening to events from Matrix and dispatching them to the correct `action`: - │   └── event-dispatcher.js - * We aren't using the matrix-js-sdk, so here are all the functions for the Matrix C-S and Appservice APIs: - ├── matrix - │   └── *.js - * Various files you can run once if you need them. - ├── scripts - │   * First time running a new bridge? Run this file to plant a seed, which will flourish into state for the bridge: - │   ├── seed.js - │   * Hopefully you won't need the rest of these. Code quality varies wildly. - │   └── *.js * You are here! :) └── readme.md + * Source code + └── src + * The bridge's SQLite database is stored here: + ├── db + │   ├── *.sql, *.db + │   * Migrations change the database schema when you update to a newer version of OOYE: + │   └── migrations + │       └── *.sql, *.js + * Discord-to-Matrix bridging: + ├── d2m + │   * Execute actions through the whole flow, like sending a Discord message to Matrix: + │   ├── actions + │   │   └── *.js + │   * Convert data from one form to another without depending on bridge state. Called by actions: + │   ├── converters + │   │   └── *.js + │   * Making Discord work: + │   ├── discord-*.js + │   * Listening to events from Discord and dispatching them to the correct `action`: + │   └── event-dispatcher.js + * Discord bot commands and menus: + ├── discord + │   ├── interactions + │   │   └── *.js + │   └── discord-command-handler.js + * Matrix-to-Discord bridging: + ├── m2d + │   * Execute actions through the whole flow, like sending a Matrix message to Discord: + │   ├── actions + │   │   └── *.js + │   * Convert data from one form to another without depending on bridge state. Called by actions: + │   ├── converters + │   │   └── *.js + │   * Listening to events from Matrix and dispatching them to the correct `action`: + │   └── event-dispatcher.js + * We aren't using the matrix-js-sdk, so here are all the functions for the Matrix C-S and Appservice APIs: + ├── matrix + │   └── *.js + * Various files you can run once if you need them. + └── scripts + * First time running a new bridge? Run this file to plant a seed, which will flourish into state for the bridge: + ├── seed.js + * Hopefully you won't need the rest of these. Code quality varies wildly. + └── *.js ## Dependency justification @@ -160,7 +165,7 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (1) @cloudrac3r/discord-markdown: This is my fork! * (0) @cloudrac3r/giframe: This is my fork! * (1) @cloudrac3r/html-template-tag: This is my fork! -* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It has several dependencies because HTTP servers have to do more than you'd think. +* (0) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It depends on h3 and zod, which are already pulled in by OOYE. * (0) @cloudrac3r/mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. @@ -172,6 +177,7 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. * (0) get-stream: Only needed if content_length_workaround is true. +* (14) h3: HTTP server. OOYE needs this for the appservice listener, authmedia proxy, and more. 14 transitive dependencies is on the low end for an HTTP server. * (1) heatsync: Module hot-reloader that I trust. * (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON. * (0) minimist: It's already pulled in by better-sqlite3->prebuild-install. @@ -182,5 +188,6 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in * (10) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. +* (0) zod: Input validation for the web server. It's popular and easy to use. Total transitive production dependencies: 116 From b3daa6b84c47e35d4f8a58593d19aa6c716e4e13 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Sep 2024 19:11:23 +1200 Subject: [PATCH 094/346] Fix discord URL cache purge --- src/web/routes/download-discord.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index f5871d3..9bac9f3 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -17,21 +17,16 @@ const schema = { /** @type {Map>} */ const cache = new Map() -function hasExpired(url) { +/** @param {string} url */ +function timeUntilExpiry(url) { const params = new URL(url).searchParams const ex = params.get("ex") assert(ex) // refreshed urls from the discord api always include this parameter - return parseInt(ex, 16) < Date.now() / 1000 + const time = parseInt(ex, 16)*1000 - Date.now() + if (time > 0) return time + return false } -// purge expired urls from cache every hour -setInterval(() => { - for (const entry of cache.entries()) { - if (hasExpired(entry[1])) cache.delete(entry[0]) - } - console.log(`purged discord media cache, it now has ${cache.size} urls`) -}, 60 * 60 * 1000).unref() - as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineEventHandler(async event => { const params = await getValidatedRouterParams(event, schema.params.parse) @@ -45,18 +40,21 @@ as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defi const url = `https://cdn.discordapp.com/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` let promise = cache.get(url) + /** @type {string | undefined} */ let refreshed if (promise) { - console.log("using existing cache entry") refreshed = await promise - if (hasExpired(refreshed)) promise = undefined - console.log(promise) + if (!timeUntilExpiry(refreshed)) promise = undefined } if (!promise) { - console.log("refreshing and storing") promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) cache.set(url, promise) refreshed = await promise + const time = timeUntilExpiry(refreshed) + assert(time) // the just-refreshed URL will always be in the future + setTimeout(() => { + cache.delete(url) + }, time).unref() } assert(refreshed) // will have been assigned by one of the above branches From 6bc3eaf8667cf440df473344531b4dd844e2e035 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 00:33:37 +1200 Subject: [PATCH 095/346] Migrate all legacy commands to interactions --- src/d2m/event-dispatcher.js | 4 - src/discord/discord-command-handler.js | 274 ------------------------ src/discord/interactions/permissions.js | 1 - src/discord/interactions/privacy.js | 59 +++++ src/discord/register-interactions.js | 35 ++- 5 files changed, 92 insertions(+), 281 deletions(-) delete mode 100644 src/discord/discord-command-handler.js create mode 100644 src/discord/interactions/privacy.js diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 235248c..1806ee6 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -27,8 +27,6 @@ const updatePins = sync.require("./actions/update-pins") const api = sync.require("../matrix/api") /** @type {import("../discord/utils")} */ const dUtils = sync.require("../discord/utils") -/** @type {import("../discord/discord-command-handler")}) */ -const discordCommandHandler = sync.require("../discord/discord-command-handler") /** @type {import("../m2d/converters/utils")} */ const mxUtils = require("../m2d/converters/utils") /** @type {import("./actions/speedbump")} */ @@ -266,7 +264,6 @@ module.exports = { // @ts-ignore await sendMessage.sendMessage(message, channel, guild, row), - await discordCommandHandler.execute(message, channel, guild) retrigger.messageFinishedBridging(message.id) }, @@ -313,7 +310,6 @@ module.exports = { */ async onReactionAdd(client, data) { if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. - discordCommandHandler.onReactionAdd(data) await addReaction.addReaction(data) }, diff --git a/src/discord/discord-command-handler.js b/src/discord/discord-command-handler.js deleted file mode 100644 index f0a4b49..0000000 --- a/src/discord/discord-command-handler.js +++ /dev/null @@ -1,274 +0,0 @@ -// @ts-check - -const assert = require("assert").strict -const util = require("util") -const DiscordTypes = require("discord-api-types/v10") -const {reg} = require("../matrix/read-registration") -const {addbot} = require("../../addbot") - -const {discord, sync, db, select} = require("../passthrough") -/** @type {import("../matrix/api")}) */ -const api = sync.require("../matrix/api") -/** @type {import("../matrix/file")} */ -const file = sync.require("../matrix/file") -/** @type {import("../m2d/converters/utils")} */ -const mxUtils = sync.require("../m2d/converters/utils") -/** @type {import("../d2m/actions/create-space")} */ -const createSpace = sync.require("../d2m/actions/create-space") -/** @type {import("./utils")} */ -const utils = sync.require("./utils") - -const PREFIX = "//" - -let buttons = [] - -/** - * @param {string} channelID where to add the button - * @param {string} messageID where to add the button - * @param {string} emoji emoji to add as a button - * @param {string} userID only listen for responses from this user - * @returns {Promise} - */ -async function addButton(channelID, messageID, emoji, userID) { - await discord.snow.channel.createReaction(channelID, messageID, emoji) - return new Promise(resolve => { - buttons.push({channelID, messageID, userID, resolve, created: Date.now()}) - }) -} - -// Clear out old buttons every so often to free memory -setInterval(() => { - const now = Date.now() - buttons = buttons.filter(b => now - b.created < 2*60*60*1000) -}, 10*60*1000) - -/** @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ -function onReactionAdd(data) { - const button = buttons.find(b => b.channelID === data.channel_id && b.messageID === data.message_id && b.userID === data.user_id) - if (button) { - buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again - button.resolve(data) - } -} - -/** - * @callback CommandExecute - * @param {DiscordTypes.GatewayMessageCreateDispatchData} message - * @param {DiscordTypes.APIGuildTextChannel} channel - * @param {DiscordTypes.APIGuild} guild - * @param {Partial} [ctx] - */ - -/** - * @typedef Command - * @property {string[]} aliases - * @property {(message: DiscordTypes.GatewayMessageCreateDispatchData, channel: DiscordTypes.APIGuildTextChannel, guild: DiscordTypes.APIGuild) => Promise} execute - */ - -/** @param {CommandExecute} execute */ -function replyctx(execute) { - /** @type {CommandExecute} */ - return function(message, channel, guild, ctx = {}) { - ctx.message_reference = { - message_id: message.id, - channel_id: channel.id, - guild_id: guild.id, - fail_if_not_exists: false - } - return execute(message, channel, guild, ctx) - } -} - -/** @type {Command[]} */ -const commands = [{ - aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"], - execute: replyctx( - async (message, channel, guild, ctx) => { - // Guard - const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() - if (!roomID) return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "This channel isn't bridged to the other side." - }) - - // Current avatar - const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") - let currentAvatarMessage = - ( avatarEvent.url ? `Current room-specific avatar: ${mxUtils.getPublicUrlForMxc(avatarEvent.url)}` - : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") - - // Next potential avatar - const nextAvatarURL = message.attachments.find(a => a.content_type?.startsWith("image/"))?.url || message.content.match(/https?:\/\/[^ ]+\.[^ ]+\.(?:png|jpg|jpeg|webp)\b/)?.[0] - let nextAvatarMessage = - ( nextAvatarURL ? `\nYou want to set it to: ${nextAvatarURL}\nHit ✅ to make it happen.` - : "") - - const sent = await discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: currentAvatarMessage + nextAvatarMessage - }) - - if (nextAvatarURL) { - addButton(channel.id, sent.id, "✅", message.author.id).then(async data => { - const mxcUrl = await file.uploadDiscordFileToMxc(nextAvatarURL) - await api.sendState(roomID, "m.room.avatar", "", { - url: mxcUrl - }) - db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE channel_id = ?").run(mxcUrl, channel.id) - await discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "Your creation is unleashed. Any complaints will be redirected to Grelbo." - }) - }) - } - } - ) -}, { - aliases: ["invite"], - execute: replyctx( - async (message, channel, guild, ctx) => { - // Check guild is bridged - const spaceID = select("guild_space", "space_id", {guild_id: guild.id}).pluck().get() - const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() - if (!spaceID || !roomID) return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "This server isn't bridged to Matrix, so you can't invite Matrix users." - }) - - // Check CREATE_INSTANT_INVITE permission - assert(message.member) - const guildPermissions = utils.getPermissions(message.member.roles, guild.roles) - if (!(guildPermissions & DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) { - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "You don't have permission to invite people to this Discord server." - }) - } - - // Guard against accidental mentions instead of the MXID - if (message.content.match(/<[@#:].*>/)) return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "You have to say the Matrix ID of the person you want to invite, but you mentioned a Discord user in your message.\nOne way to fix this is by writing `` ` `` backticks `` ` `` around the Matrix ID." - }) - - // Get named MXID - const mxid = message.content.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] - if (!mxid) return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`" - }) - - // Check for existing invite to the space - let spaceMember - try { - spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) - } catch (e) {} - if (spaceMember && spaceMember.membership === "invite") { - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `\`${mxid}\` already has an invite, which they haven't accepted yet.` - }) - } - - // Invite Matrix user if not in space - if (!spaceMember || spaceMember.membership !== "join") { - await api.inviteToRoom(spaceID, mxid) - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `You invited \`${mxid}\` to the server.` - }) - } - - // The Matrix user *is* in the space, maybe we want to invite them to this channel? - let roomMember - try { - roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) - } catch (e) {} - if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { - const sent = await discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?\nHit ✅ to make it happen.` - }) - return addButton(channel.id, sent.id, "✅", message.author.id).then(async data => { - await api.inviteToRoom(roomID, mxid) - await discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `You invited \`${mxid}\` to the channel.` - }) - }) - } - - // The Matrix user *is* in the space and in the channel. - await discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `\`${mxid}\` is already in this server and this channel.` - }) - } - ) -}, { - aliases: ["addbot"], - execute: replyctx( - async (message, channel, guild, ctx) => { - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: addbot() - }) - } - ) -}, { - aliases: ["privacy", "discoverable", "publish", "published"], - execute: replyctx( - async (message, channel, guild, ctx) => { - const current = select("guild_space", "privacy_level", {guild_id: guild.id}).pluck().get() - if (current == null) { - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "This server isn't bridged to the other side." - }) - } - - const levels = ["invite", "link", "directory"] - const level = levels.findIndex(x => message.content.includes(x)) - if (level === -1) { - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "**Usage: `//privacy `**. This will set who can join the space on Matrix-side. There are three levels:" - + "\n`invite`: Can only join with a direct in-app invite from another Matrix user, or the //invite command." - + "\n`link`: Matrix links can be created and shared like Discord's invite links. `invite` features also work." - + "\n`directory`: Publishes to the Matrix in-app directory, like Server Discovery. Preview enabled. `invite` and `link` also work." - + `\n**Current privacy level: \`${levels[current]}\`**` - }) - } - - assert(message.member) - const guildPermissions = utils.getPermissions(message.member.roles, guild.roles) - if (guild.owner_id !== message.author.id && !(guildPermissions & BigInt(0x28))) { // MANAGE_GUILD | ADMINISTRATOR - return discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: "You don't have permission to change the privacy level. You need Manage Server or Administrator." - }) - } - - db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(level, guild.id) - discord.snow.channel.createMessage(channel.id, { - ...ctx, - content: `Privacy level updated to \`${levels[level]}\`. Changes will apply shortly.` - }) - await createSpace.syncSpaceFully(guild.id) - } - ) -}] - -/** @type {CommandExecute} */ -async function execute(message, channel, guild) { - if (!message.content.startsWith(PREFIX)) return - const words = message.content.slice(PREFIX.length).split(" ") - const commandName = words[0] - const command = commands.find(c => c.aliases.includes(commandName)) - if (!command) return - - await command.execute(message, channel, guild) -} - -module.exports.execute = execute -module.exports.onReactionAdd = onReactionAdd diff --git a/src/discord/interactions/permissions.js b/src/discord/interactions/permissions.js index 82c3d3c..e010b1b 100644 --- a/src/discord/interactions/permissions.js +++ b/src/discord/interactions/permissions.js @@ -5,7 +5,6 @@ const Ty = require("../../types") const {discord, sync, db, select, from} = require("../../passthrough") const assert = require("assert/strict") - /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") diff --git a/src/discord/interactions/privacy.js b/src/discord/interactions/privacy.js new file mode 100644 index 0000000..bb8c6c9 --- /dev/null +++ b/src/discord/interactions/privacy.js @@ -0,0 +1,59 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db, select} = require("../../passthrough") +const {id: botID} = require("../../../addbot") + +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") + +/** + * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction + */ +async function interact({id, token, data, guild_id}) { + // Check guild is bridged + const current = select("guild_space", "privacy_level", {guild_id}).pluck().get() + if (current == null) return { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This server isn't bridged to Matrix, so you can't set the Matrix privacy level.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + } + + // Get input level + /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore + const options = data.options + const input = options?.[0].value || "" + const levels = ["invite", "link", "directory"] + const level = levels.findIndex(x => input === x) + if (level === -1) { + return discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "**Usage: `/privacy `**. This will set who can join the space on Matrix-side. There are three levels:" + + "\n`invite`: Can only join with a direct in-app invite from another user. No shareable invite links." + + "\n`link`: Matrix links can be created and shared like Discord's invite links. In-app invites still work." + + "\n`directory`: Publicly visible in the Matrix space directory, like Server Discovery. Invites and links still work." + + `\n**Current privacy level: \`${levels[current]}\`**`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + } + + await discord.snow.interaction.createInteractionResponse(id, token, { + type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: DiscordTypes.MessageFlags.Ephemeral + } + }) + + db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(level, guild_id) + await createSpace.syncSpaceFully(guild_id) // this is inefficient but OK to call infrequently on user request + + await discord.snow.interaction.editOriginalInteractionResponse(botID, token, { + content: `Privacy level updated to \`${levels[level]}\`.` + }) +} + +module.exports.interact = interact diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index eeeab62..cd9203f 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -9,6 +9,9 @@ const invite = sync.require("./interactions/invite.js") const permissions = sync.require("./interactions/permissions.js") const bridge = sync.require("./interactions/bridge.js") const reactions = sync.require("./interactions/reactions.js") +const privacy = sync.require("./interactions/privacy.js") + +// User must have EVERY permission in default_member_permissions to be able to use the command discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Matrix info", @@ -40,16 +43,42 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "bridge", contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.ChatInput, - description: "Start bridging this channel to a Matrix room.", + description: "Start bridging this channel to a Matrix room", default_member_permissions: String(DiscordTypes.PermissionFlagsBits.ManageChannels), options: [ { type: DiscordTypes.ApplicationCommandOptionType.String, - description: "Destination room to bridge to.", + description: "Destination room to bridge to", name: "room", autocomplete: true } ] +}, { + name: "privacy", + contexts: [DiscordTypes.InteractionContextType.Guild], + type: DiscordTypes.ApplicationCommandType.ChatInput, + description: "Change whether Matrix users can join through direct invites, links, or the public directory.", + default_member_permissions: String(DiscordTypes.PermissionFlagsBits.ManageGuild), + options: [ + { + type: DiscordTypes.ApplicationCommandOptionType.String, + description: "Check or set the new privacy level", + name: "level", + choices: [{ + name: "❓️ Check the current privacy level and get more information.", + value: "info" + }, { + name: "🤝 Only allow joining with a direct in-app invite from another user. No shareable invite links.", + value: "invite" + }, { + name: "🔗 Matrix links can be created and shared like Discord's invite links. In-app invites still work.", + value: "link", + }, { + name: "🌏️ Publicly visible in the Matrix directory, like Server Discovery. Invites and links still work.", + value: "directory" + }] + } + ] }]) async function dispatchInteraction(interaction) { @@ -69,6 +98,8 @@ async function dispatchInteraction(interaction) { await bridge.interact(interaction) } else if (interactionId === "Reactions") { await reactions.interact(interaction) + } else if (interactionId === "privacy") { + await privacy.interact(interaction) } else { throw new Error(`Unknown interaction ${interactionId}`) } From ae9acbcc52a97ca3c3394a16b53f1ef45dcd3318 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 00:34:53 +1200 Subject: [PATCH 096/346] Generate public urls for media.discordapp.net too --- src/d2m/converters/message-to-event.js | 2 +- src/discord/utils.js | 4 +- src/web/routes/download-discord.js | 65 ++++++++++++++------------ 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 5191e6e..154c461 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -366,7 +366,7 @@ async function messageToEvent(message, guild, options = {}, di) { * Translate Discord attachment links into links that go via the bridge, so they last forever. */ function transformAttachmentLinks(content) { - return content.replace(/https:\/\/cdn\.discord(?:app)?\.com\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/g, url => dUtils.getPublicUrlForCdn(url)) + return content.replace(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/g, url => dUtils.getPublicUrlForCdn(url)) } /** diff --git a/src/discord/utils.js b/src/discord/utils.js index 24a3c85..8cb241b 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -121,9 +121,9 @@ function timestampToSnowflakeInexact(timestamp) { /** @param {string} url */ function getPublicUrlForCdn(url) { - const match = url.match(/https:\/\/cdn.discordapp.com\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) + const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) if (!match) return url - return `${reg.ooye.bridge_origin}/download/discordcdn/${match[1]}/${match[2]}/${match[3]}` + return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}` } module.exports.getPermissions = getPermissions diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index 9bac9f3..047dd94 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -27,36 +27,41 @@ function timeUntilExpiry(url) { return false } -as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineEventHandler(async event => { - const params = await getValidatedRouterParams(event, schema.params.parse) +function defineMediaProxyHandler(domain) { + return defineEventHandler(async event => { + const params = await getValidatedRouterParams(event, schema.params.parse) - const row = select("channel_room", "channel_id", {channel_id: params.channel_id}).get() - if (row == null) { - throw createError({ - status: 403, - data: `The file you requested isn't permitted by this media proxy.` - }) - } + const row = select("channel_room", "channel_id", {channel_id: params.channel_id}).get() + if (row == null) { + throw createError({ + status: 403, + data: `The file you requested isn't permitted by this media proxy.` + }) + } - const url = `https://cdn.discordapp.com/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` - let promise = cache.get(url) - /** @type {string | undefined} */ - let refreshed - if (promise) { - refreshed = await promise - if (!timeUntilExpiry(refreshed)) promise = undefined - } - if (!promise) { - promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) - cache.set(url, promise) - refreshed = await promise - const time = timeUntilExpiry(refreshed) - assert(time) // the just-refreshed URL will always be in the future - setTimeout(() => { - cache.delete(url) - }, time).unref() - } - assert(refreshed) // will have been assigned by one of the above branches + const url = `https://${domain}/attachments/${params.channel_id}/${params.attachment_id}/${params.file_name}` + let promise = cache.get(url) + /** @type {string | undefined} */ + let refreshed + if (promise) { + refreshed = await promise + if (!timeUntilExpiry(refreshed)) promise = undefined + } + if (!promise) { + promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) + cache.set(url, promise) + refreshed = await promise + const time = timeUntilExpiry(refreshed) + assert(time) // the just-refreshed URL will always be in the future + setTimeout(() => { + cache.delete(url) + }, time).unref() + } + assert(refreshed) // will have been assigned by one of the above branches - return sendRedirect(event, refreshed) -})) + return sendRedirect(event, refreshed) + }) +} + +as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com")) +as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net")) From b8a992453b12e7522e686d69005afe0cedf7f787 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 00:49:38 +1200 Subject: [PATCH 097/346] addbot.js as an executable script --- addbot.js | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 addbot.js diff --git a/addbot.js b/addbot.js old mode 100644 new mode 100755 index 4ac55fc..e13c829 --- a/addbot.js +++ b/addbot.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check const {reg} = require("./src/matrix/read-registration") From 256196cc545da475e69f141a73326e17f468a9a0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 00:50:00 +1200 Subject: [PATCH 098/346] start.js as an executable script --- start.js | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 start.js diff --git a/start.js b/start.js old mode 100644 new mode 100755 index a9f639a..caa01c2 --- a/start.js +++ b/start.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check const sqlite = require("better-sqlite3") From b8962780ae6e40944280a47edfade01435af3064 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 00:55:54 +1200 Subject: [PATCH 099/346] Fix type errors in scripts and make executable --- scripts/capture-message-update-events.js | 11 ++++++----- scripts/check-migrate.js | 8 ++++---- scripts/migrate-from-old-bridge.js | 20 ++++++++++---------- scripts/save-channel-names-to-db.js | 11 ++++++----- scripts/save-event-types-to-db.js | 5 +++-- scripts/seed.js | 6 +++--- scripts/wal.js | 1 + 7 files changed, 33 insertions(+), 29 deletions(-) mode change 100644 => 100755 scripts/capture-message-update-events.js mode change 100644 => 100755 scripts/check-migrate.js mode change 100644 => 100755 scripts/migrate-from-old-bridge.js mode change 100644 => 100755 scripts/save-channel-names-to-db.js mode change 100644 => 100755 scripts/save-event-types-to-db.js mode change 100644 => 100755 scripts/seed.js mode change 100644 => 100755 scripts/wal.js diff --git a/scripts/capture-message-update-events.js b/scripts/capture-message-update-events.js old mode 100644 new mode 100755 index 671b062..fb29a5d --- a/scripts/capture-message-update-events.js +++ b/scripts/capture-message-update-events.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check // **** @@ -16,16 +17,16 @@ function fieldToPresenceValue(field) { const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") -const config = require("../config") -const passthrough = require("../passthrough") +const {reg} = require("../src/matrix/read-registration") +const passthrough = require("../src/passthrough") const sync = new HeatSync({watchFS: false}) -Object.assign(passthrough, {config, sync}) +Object.assign(passthrough, {sync}) -const DiscordClient = require("../d2m/discord-client") +const DiscordClient = require("../src/d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "no") +const discord = new DiscordClient(reg.ooye.discord_token, "no") passthrough.discord = discord ;(async () => { diff --git a/scripts/check-migrate.js b/scripts/check-migrate.js old mode 100644 new mode 100755 index 308ea87..82f2832 --- a/scripts/check-migrate.js +++ b/scripts/check-migrate.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check // Trigger the database migration flow and exit after committing. @@ -5,11 +6,10 @@ const sqlite = require("better-sqlite3") -const config = require("../config") -const passthrough = require("../passthrough") +const passthrough = require("../src/passthrough") const db = new sqlite("db/ooye.db") -const migrate = require("../db/migrate") +const migrate = require("../src/db/migrate") -Object.assign(passthrough, {config, db }) +Object.assign(passthrough, {db}) migrate.migrate(db) diff --git a/scripts/migrate-from-old-bridge.js b/scripts/migrate-from-old-bridge.js old mode 100644 new mode 100755 index e03938d..c47bb05 --- a/scripts/migrate-from-old-bridge.js +++ b/scripts/migrate-from-old-bridge.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check const assert = require("assert").strict @@ -6,12 +7,11 @@ const Semaphore = require("@chriscdn/promise-semaphore") const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") -const config = require("../config") -const passthrough = require("../passthrough") +const passthrough = require("../src/passthrough") const sync = new HeatSync({watchFS: false}) -const {reg} = require("../matrix/read-registration") +const {reg} = require("../src/matrix/read-registration") assert(reg.old_bridge) const oldAT = reg.old_bridge.as_token const newAT = reg.as_token @@ -25,19 +25,19 @@ db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration ( PRIMARY KEY("discord_channel") ) WITHOUT ROWID;`) -Object.assign(passthrough, {config, sync, db}) +Object.assign(passthrough, {sync, db}) -const DiscordClient = require("../d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "half") +const DiscordClient = require("../src/d2m/discord-client") +const discord = new DiscordClient(reg.ooye.discord_token, "half") passthrough.discord = discord -/** @type {import("../d2m/actions/create-space")} */ +/** @type {import("../src/d2m/actions/create-space")} */ const createSpace = sync.require("../d2m/actions/create-space") -/** @type {import("../d2m/actions/create-room")} */ +/** @type {import("../src/d2m/actions/create-room")} */ const createRoom = sync.require("../d2m/actions/create-room") -/** @type {import("../matrix/mreq")} */ +/** @type {import("../src/matrix/mreq")} */ const mreq = sync.require("../matrix/mreq") -/** @type {import("../matrix/api")} */ +/** @type {import("../src/matrix/api")} */ const api = sync.require("../matrix/api") const sema = new Semaphore() diff --git a/scripts/save-channel-names-to-db.js b/scripts/save-channel-names-to-db.js old mode 100644 new mode 100755 index 1e5c541..7a72949 --- a/scripts/save-channel-names-to-db.js +++ b/scripts/save-channel-names-to-db.js @@ -1,19 +1,20 @@ +#!/usr/bin/env node // @ts-check const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") -const config = require("../config") -const passthrough = require("../passthrough") +const {reg} = require("../src/matrix/read-registration") +const passthrough = require("../src/passthrough") const db = new sqlite("db/ooye.db") const sync = new HeatSync({watchFS: false}) -Object.assign(passthrough, {config, sync, db}) +Object.assign(passthrough, {sync, db}) -const DiscordClient = require("../d2m/discord-client") +const DiscordClient = require("../src/d2m/discord-client") -const discord = new DiscordClient(config.discordToken, "no") +const discord = new DiscordClient(reg.ooye.discord_token, "no") passthrough.discord = discord ;(async () => { diff --git a/scripts/save-event-types-to-db.js b/scripts/save-event-types-to-db.js old mode 100644 new mode 100755 index 290aa4a..db14e07 --- a/scripts/save-event-types-to-db.js +++ b/scripts/save-event-types-to-db.js @@ -1,16 +1,17 @@ +#!/usr/bin/env node // @ts-check const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") -const passthrough = require("../passthrough") +const passthrough = require("../src/passthrough") const db = new sqlite("db/ooye.db") const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, {sync, db}) -const api = require("../matrix/api") +const api = require("../src/matrix/api") /** @type {{event_id: string, room_id: string, event_type: string}[]} */ // @ts-ignore const rows = db.prepare("SELECT event_id, room_id, event_type FROM event_message INNER JOIN message_channel USING (message_id) INNER JOIN channel_room USING (channel_id)").all() diff --git a/scripts/seed.js b/scripts/seed.js old mode 100644 new mode 100755 index 649f3fc..991eadc --- a/scripts/seed.js +++ b/scripts/seed.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check const assert = require("assert").strict @@ -33,14 +34,13 @@ if (fs.existsSync("db")) { fs.rmSync("db", {recursive: true}) } -const config = require("../config") const passthrough = require("../src/passthrough") const db = new sqlite("src/db/ooye.db") const migrate = require("../src/db/migrate") const sync = new HeatSync({watchFS: false}) -Object.assign(passthrough, { sync, config, db }) +Object.assign(passthrough, {sync, db}) const orm = sync.require("../src/db/orm") passthrough.from = orm.from @@ -225,7 +225,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { assert(utils.eventSenderIsFromDiscord(mxid), "appservice's mxid must be in the namespace it controls") assert(reg.ooye.server_origin.match(/^https?:\/\//), "server origin must start with http or https") assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash") - const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString() + const botID = Buffer.from(reg.ooye.discord_token.split(".")[0], "base64").toString() assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") assert.match(reg.url, /^https?:/, "url must start with http:// or https://") diff --git a/scripts/wal.js b/scripts/wal.js old mode 100644 new mode 100755 index 1ad15d0..352ac9d --- a/scripts/wal.js +++ b/scripts/wal.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node // @ts-check const sqlite = require("better-sqlite3") From 01af302796a378faba0b24fab0a75920ccf1da48 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 01:09:47 +1200 Subject: [PATCH 100/346] Move ooye.db to current working dir --- .gitignore | 2 +- package.json | 1 + readme.md | 17 +++++++++-------- scripts/check-migrate.js | 2 +- scripts/migrate-from-old-bridge.js | 2 +- scripts/save-channel-names-to-db.js | 2 +- scripts/save-event-types-to-db.js | 2 +- scripts/seed.js | 6 +++--- scripts/wal.js | 2 +- start.js | 2 +- 10 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 3781b9b..a462935 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ node_modules config.js registration.yaml coverage -src/db/ooye.db* +ooye.db* test/res/* !test/res/lottie* diff --git a/package.json b/package.json index 69101c8..5933ba9 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "supertape": "^10.4.0" }, "scripts": { + "start": "node start.js", "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", diff --git a/readme.md b/readme.md index 695c26a..d9ade3c 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ For more information about features, [see the user guide.](https://gitdab.com/ca Using WeatherStack as a thin layer between the bridge application and the Discord API lets us control exactly what data is cached in memory. Only necessary information is cached. For example, member data, user data, message content, and past edits are never stored in memory. This keeps the memory usage low and also prevents it ballooning in size over the bridge's runtime. -The bridge uses a small SQLite database to store relationships like which Discord messages correspond to which Matrix messages. This is so the bridge knows what to edit when some message is edited on Discord. Using `without rowid` on the database tables stores the index and the data in the same B-tree. Since Matrix and Discord's internal IDs are quite long, this vastly reduces storage space because those IDs do not have to be stored twice separately. Some event IDs are actually stored as xxhash integers to reduce storage requirements even more. On my personal instance of OOYE, every 100,000 messages require 16.1 MB of storage space in the SQLite database. +The bridge uses a small SQLite database to store relationships like which Discord messages correspond to which Matrix messages. This is so the bridge knows what to edit when some message is edited on Discord. Using `without rowid` on the database tables stores the index and the data in the same B-tree. Since Matrix and Discord's internal IDs are quite long, this vastly reduces storage space because those IDs do not have to be stored twice separately. Some event IDs and URLs are actually stored as xxhash integers to reduce storage requirements even more. On my personal instance of OOYE, every 300,000 messages (representing a year of conversations) requires 47.3 MB of storage space in the SQLite database. Only necessary data and columns are queried from the database. We only contact the homeserver API if the database doesn't contain what we need. @@ -83,7 +83,7 @@ Follow these steps: 1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. -1. Start the bridge: `node start.js` +1. Start the bridge: `npm run start` 1. Add the bot to a server - use any *one* of the following commands for an invite link: * (in the REPL) `addbot` @@ -106,18 +106,19 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in ## Repository structure . - * Run this to start the bridge: - ├── start.js * Runtime configuration, like tokens and user info: ├── registration.yaml * You are here! :) - └── readme.md + ├── readme.md + * The bridge's SQLite database is stored here: + ├── ooye.db* * Source code └── src - * The bridge's SQLite database is stored here: + * Database schema: ├── db - │   ├── *.sql, *.db - │   * Migrations change the database schema when you update to a newer version of OOYE: + │   ├── orm.js, orm-defs.d.ts + │   * Migrations change the database schema when you update to a newer version of OOYE: + │   ├── migrate.js │   └── migrations │       └── *.sql, *.js * Discord-to-Matrix bridging: diff --git a/scripts/check-migrate.js b/scripts/check-migrate.js index 82f2832..04a4402 100755 --- a/scripts/check-migrate.js +++ b/scripts/check-migrate.js @@ -7,7 +7,7 @@ const sqlite = require("better-sqlite3") const passthrough = require("../src/passthrough") -const db = new sqlite("db/ooye.db") +const db = new sqlite("ooye.db") const migrate = require("../src/db/migrate") Object.assign(passthrough, {db}) diff --git a/scripts/migrate-from-old-bridge.js b/scripts/migrate-from-old-bridge.js index c47bb05..c5e50a1 100755 --- a/scripts/migrate-from-old-bridge.js +++ b/scripts/migrate-from-old-bridge.js @@ -17,7 +17,7 @@ const oldAT = reg.old_bridge.as_token const newAT = reg.as_token const oldDB = new sqlite(reg.old_bridge.database) -const db = new sqlite("db/ooye.db") +const db = new sqlite("ooye.db") db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration ( discord_channel TEXT NOT NULL, diff --git a/scripts/save-channel-names-to-db.js b/scripts/save-channel-names-to-db.js index 7a72949..1f36a73 100755 --- a/scripts/save-channel-names-to-db.js +++ b/scripts/save-channel-names-to-db.js @@ -6,7 +6,7 @@ const HeatSync = require("heatsync") const {reg} = require("../src/matrix/read-registration") const passthrough = require("../src/passthrough") -const db = new sqlite("db/ooye.db") +const db = new sqlite("ooye.db") const sync = new HeatSync({watchFS: false}) diff --git a/scripts/save-event-types-to-db.js b/scripts/save-event-types-to-db.js index db14e07..edfbb9c 100755 --- a/scripts/save-event-types-to-db.js +++ b/scripts/save-event-types-to-db.js @@ -5,7 +5,7 @@ const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") const passthrough = require("../src/passthrough") -const db = new sqlite("db/ooye.db") +const db = new sqlite("ooye.db") const sync = new HeatSync({watchFS: false}) diff --git a/scripts/seed.js b/scripts/seed.js index 991eadc..16be900 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -21,11 +21,11 @@ const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"] // Move database file if it's still in the old location if (fs.existsSync("db")) { if (fs.existsSync("db/ooye.db")) { - fs.renameSync("db/ooye.db", "src/db/ooye.db") + fs.renameSync("db/ooye.db", "ooye.db") } const files = fs.readdirSync("db") if (files.length) { - console.error("You must manually move or delete the files in the db folder:") + console.error("The db folder is deprecated and must be removed. Your ooye.db database file has already been moved to the root of the repo. You must manually move or delete the remaining files:") for (const file of files) { console.error(file) } @@ -35,7 +35,7 @@ if (fs.existsSync("db")) { } const passthrough = require("../src/passthrough") -const db = new sqlite("src/db/ooye.db") +const db = new sqlite("ooye.db") const migrate = require("../src/db/migrate") const sync = new HeatSync({watchFS: false}) diff --git a/scripts/wal.js b/scripts/wal.js index 352ac9d..625f2ba 100755 --- a/scripts/wal.js +++ b/scripts/wal.js @@ -2,6 +2,6 @@ // @ts-check const sqlite = require("better-sqlite3") -const db = new sqlite("db/ooye.db", {fileMustExist: true}) +const db = new sqlite("ooye.db", {fileMustExist: true}) db.pragma("journal_mode = wal") db.close() diff --git a/start.js b/start.js index caa01c2..be434f0 100755 --- a/start.js +++ b/start.js @@ -7,7 +7,7 @@ const HeatSync = require("heatsync") const {reg} = require("./src/matrix/read-registration") const passthrough = require("./src/passthrough") -const db = new sqlite("src/db/ooye.db") +const db = new sqlite("ooye.db") /** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() From d5ec013648dd6cf31d61771289e6e95336e8fdb1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 01:13:31 +1200 Subject: [PATCH 101/346] Mention that links don't expire --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d9ade3c..2043640 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ Most features you'd expect in both directions, plus a little extra spice: * Custom emoji list syncing * Custom emojis in messages * Custom room names/avatars can be applied on Matrix-side -* Larger files from Discord are linked instead of reuploaded to Matrix +* Larger files from Discord are linked instead of reuploaded to Matrix (links don't expire) * Simulated user accounts are named @the_persons_username rather than @112233445566778899 For more information about features, [see the user guide.](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/user-guide.md) From f35c5266b6f7b6e3307c4674e95b5b9b456c0ebd Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 02:11:43 +1200 Subject: [PATCH 102/346] Tidy up repo --- .gitignore | 13 ++++++++-- scripts/remove-old-bridged-users.js | 39 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 scripts/remove-old-bridged-users.js diff --git a/.gitignore b/.gitignore index a462935..e533dce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ -node_modules +# Secrets config.js registration.yaml -coverage ooye.db* +events.db* + +# Automatically generated +node_modules +coverage test/res/* !test/res/lottie* +icon.svg +*~ +.#* +\#*# +launch.json diff --git a/scripts/remove-old-bridged-users.js b/scripts/remove-old-bridged-users.js new file mode 100644 index 0000000..d8910bd --- /dev/null +++ b/scripts/remove-old-bridged-users.js @@ -0,0 +1,39 @@ +// @ts-check + +const HeatSync = require("heatsync") +const sync = new HeatSync({watchFS: false}) + +const sqlite = require("better-sqlite3") +const db = new sqlite("db/ooye.db") + +const passthrough = require("../src/passthrough") +Object.assign(passthrough, {db, sync}) + +const api = require("../src/matrix/api") +const mreq = require("../src/matrix/mreq") + +const rooms = db.prepare("select room_id from channel_room").pluck().all() + +;(async () => { + // Step 5: Kick users starting with @_discord_ + await mreq.withAccessToken("baby", async () => { + for (const roomID of rooms) { + try { + const members = await api.getJoinedMembers(roomID) + for (const mxid of Object.keys(members.joined)) { + if (mxid.startsWith("@_discord_") && !mxid.startsWith("@_discord_bot")) { + await api.leaveRoom(roomID, mxid) + } + } + await api.setUserPower(roomID, "@_discord_bot:cadence.moe", 0) + await api.leaveRoom(roomID) + } catch (e) { + if (e.message.includes("Appservice not in room")) { + // ok + } else { + throw e + } + } + } + }) +})() From 6a15f4fc0f7d9fdc97a85c19cc21026df438a7fe Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 22:17:00 +1200 Subject: [PATCH 103/346] Check for Matrix v1.11 in setup --- scripts/seed.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/seed.js b/scripts/seed.js index 16be900..55ada69 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -87,6 +87,9 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})` try { var json = await res.json() + if (!Array.isArray(json?.versions) || !json.versions.includes("v1.11")) { + return `OOYE needs Matrix version v1.11, but ${url} doesn't support this` + } } catch (e) { return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)` } From 76caaa146861105ab42fd1942afbd911adf14820 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 22:19:35 +1200 Subject: [PATCH 104/346] Force registration to be re-created Removing YAML support makes current registration files invalid, and seed.js will prompt to set up a new one. This is good, because old ones are incompatible and new ones had to be set up anyway. --- package-lock.json | 17 ----------------- package.json | 1 - src/matrix/read-registration.js | 15 +++------------ 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf6f81c..c9d0574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.3", - "js-yaml": "^4.1.0", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", @@ -1017,11 +1016,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/as-table": { "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", @@ -1931,17 +1925,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/just-kebab-case": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", diff --git a/package.json b/package.json index 5933ba9..3fd7651 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.3", - "js-yaml": "^4.1.0", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 9efffdf..8438387 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -4,7 +4,6 @@ const fs = require("fs") const crypto = require("crypto") const assert = require("assert").strict const path = require("path") -const yaml = require("js-yaml") const registrationFilePath = path.join(process.cwd(), "registration.yaml") @@ -60,18 +59,10 @@ function getTemplateRegistration() { function readRegistration() { /** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore let result = null - if (fs.existsSync(registrationFilePath)) { + try { const content = fs.readFileSync(registrationFilePath, "utf8") - if (content.startsWith("{")) { // Use JSON parser - result = JSON.parse(content) - checkRegistration(result) - } else { // Use YAML parser - result = yaml.load(content) - checkRegistration(result) - // Convert to JSON - writeRegistration(result) - } - } + result = JSON.parse(content) + } catch (e) {} return result } From a61ad309089a80e21001744f04127f0b562e666a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 22:21:21 +1200 Subject: [PATCH 105/346] Annotate the specific step that is slow --- scripts/seed.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/seed.js b/scripts/seed.js index 55ada69..300ee29 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -219,8 +219,6 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { as.close().catch(() => {}) - console.log("⏩ Processing. This could take up to 30 seconds. Please be patient...") - const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}` // ensure registration is correctly set... @@ -302,6 +300,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { console.log("✅ Discord profile updated...") // 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.)") await api.profileSetDisplayname(mxid, "Out Of Your Element") await api.profileSetAvatarUrl(mxid, avatarUrl) console.log("✅ Matrix profile updated...") From 1ebf9e193c5e34cd0b618ae65df96d80eeea50ec Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 22:54:04 +1200 Subject: [PATCH 106/346] Use getMedia API for media proxy --- src/web/routes/download-matrix.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index 137af7c..f996716 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -3,15 +3,16 @@ const assert = require("assert/strict") const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError} = require("h3") const {z} = require("zod") -const fetch = require("node-fetch").default /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null // @ts-ignore require("xxhash-wasm")().then(h => hasher = h) -const {reg} = require("../../matrix/read-registration") -const {as, select} = require("../../passthrough") +const {sync, as, select} = require("../../passthrough") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") const schema = { params: z.object({ @@ -35,11 +36,7 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn }) } - const res = await fetch(`${reg.ooye.server_origin}/_matrix/client/v1/media/download/${params.server_name}/${params.media_id}`, { - headers: { - Authorization: `Bearer ${reg.as_token}` - } - }) + const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`) const contentType = res.headers.get("content-type") assert(contentType) From 15472b4a469b377c7935190902b103d3ad87ee95 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Sep 2024 22:56:55 +1200 Subject: [PATCH 107/346] Move test files out of matrix CDN matrix.org LOVES her! learn this one simple trick --- test/test.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/test.js b/test/test.js index ebe49c3..41824a3 100644 --- a/test/test.js +++ b/test/test.js @@ -79,18 +79,18 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not await stp.pipeline(res.body, fs.createWriteStream(to, {encoding: "binary"})) } await allReporter([ - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/bZFuuUSEebJYXUMSxuuSuLTa", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/qWmbXeRspZRLPcjseyLmeyXC", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/WbYqNlACRuicynBfdnPYtmvc", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HYcztccFIPgevDvoaWNsEtGJ", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HXfFuougamkURPPMflTJRxGc", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"}, - {url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"}, + {url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"}, + {url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"}, + {url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"}, + {url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"}, + {url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"}, + {url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"}, + {url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"}, + {url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"}, + {url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"}, + {url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"}, + {url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"}, + {url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"}, {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"}, {url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"}, ]) From 50bd9fd9d793d1809edb3279fb8d3f45351aaf81 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 16 Sep 2024 00:54:31 +1200 Subject: [PATCH 108/346] Remove colorette dependency transitively --- package-lock.json | 36 +++++++++++++++++++++++------------- package.json | 3 +-- test/test.js | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9d0574..01c6fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,11 +38,10 @@ "zod": "^3.23.8" }, "devDependencies": { - "@cloudrac3r/tap-dot": "^2.0.2", + "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^10.1.2", - "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", "supertape": "^10.4.0" @@ -74,6 +73,19 @@ "node": ">=18" } }, + "../tap-dot": { + "name": "@cloudrac3r/tap-dot", + "version": "2.0.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@cloudrac3r/tap-out": "^3.2.3", + "ansi-colors": "^4.1.3" + }, + "bin": { + "tap-dot": "bin/dot" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -236,13 +248,14 @@ } }, "node_modules/@cloudrac3r/tap-dot": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.2.tgz", - "integrity": "sha512-q2IzqZvFIIjy8dBnuC5+baZ6KHroxOjWzaaBd+FGPk2NvPpsRdf8dFv0rTTntco8Z62grmCuMPc4/Nblx+Ot3A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.3.tgz", + "integrity": "sha512-GmdDvc9LCdD4IvKf5DhTTSywGzIT8KbxjhNFSewnaUEfiGDer65CK62KzdciZgWBZfow8xaHISGfXgThn5/rqg==", "dev": true, + "license": "MIT", "dependencies": { "@cloudrac3r/tap-out": "^3.2.3", - "colorette": "^1.0.5" + "ansi-colors": "^4.1.3" }, "bin": { "tap-dot": "bin/dot" @@ -253,6 +266,7 @@ "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-out/-/tap-out-3.2.3.tgz", "integrity": "sha512-WS89ziuTMLZlOVTXArkok32HcpWWxejQmZAUOl6nbB2Y114M8LfSOQ7t/rmgVsYB4oc6Ehe/nzevUMt4GGbCsA==", "dev": true, + "license": "MIT", "dependencies": { "re-emitter": "1.1.4", "split2": "^4.2.0" @@ -1313,12 +1327,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2293,7 +2301,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", "integrity": "sha512-C0SIXdXDSus2yqqvV7qifnb4NoWP7mEBXJq3axci301mXHCZb8Djwm4hrEZo4UeXRaEnfjH98uQ8EBppk2oNWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/react-is": { "version": "18.2.0", @@ -2551,6 +2560,7 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 10.x" } diff --git a/package.json b/package.json index 3fd7651..97f5921 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,10 @@ "zod": "^3.23.8" }, "devDependencies": { - "@cloudrac3r/tap-dot": "^2.0.2", + "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", "c8": "^10.1.2", - "colorette": "^1.4.0", "cross-env": "^7.0.3", "discord-api-types": "^0.37.60", "supertape": "^10.4.0" diff --git a/test/test.js b/test/test.js index 41824a3..281df29 100644 --- a/test/test.js +++ b/test/test.js @@ -11,7 +11,7 @@ const data = require("./data") /** @type {import("node-fetch").default} */ // @ts-ignore const fetch = require("node-fetch") -const {green} = require("colorette") +const {green} = require("ansi-colors") const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") From 8277cc4a174ed7661209bbece5b88f26e75d014f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Sep 2024 11:25:24 +1200 Subject: [PATCH 109/346] npm start works too --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 2043640..5b75067 100644 --- a/readme.md +++ b/readme.md @@ -83,7 +83,7 @@ Follow these steps: 1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. -1. Start the bridge: `npm run start` +1. Start the bridge: `npm start` 1. Add the bot to a server - use any *one* of the following commands for an invite link: * (in the REPL) `addbot` From f011cac3c56725467c5099a5c2db3c2c22b19855 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Sep 2024 10:01:33 +1200 Subject: [PATCH 110/346] Fix timestamp resolution bug --- src/d2m/converters/message-to-event.js | 9 +++- src/d2m/converters/message-to-event.test.js | 49 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 154c461..582b26c 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -349,8 +349,13 @@ async function messageToEvent(message, guild, options = {}, di) { result = `https://matrix.to/#/${roomID}/${eventID}?${via}` } else { const ts = dUtils.snowflakeToTimestampExact(messageID) - const {event_id} = await di.api.getEventForTimestamp(roomID, ts) - result = `https://matrix.to/#/${roomID}/${event_id}?${via}` + try { + const {event_id} = await di.api.getEventForTimestamp(roomID, ts) + result = `https://matrix.to/#/${roomID}/${event_id}?${via}` + } catch (e) { + // M_NOT_FOUND: Unable to find event from in direction Direction.FORWARDS + result = `[unknown event, timestamp resolution failed, in room: https://matrix.to/#/${roomID}?${via}]` + } } } else { result = `${match[0]} [event is from another server]` diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index a76741f..415f48d 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1,5 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") +const {MatrixServerError} = require("../../matrix/mreq") const data = require("../../../test/data") const Ty = require("../../types") @@ -267,6 +268,54 @@ test("message2event: message link that OOYE doesn't know about", async t => { t.equal(called, 3, "getEventForTimestamp, getStateEvent, and getJoinedMembers should be called once each") }) +test("message2event: message timestamp failed to fetch", async t => { + let called = 0 + const events = await messageToEvent(data.message.message_link_to_before_ooye, data.guild.general, {}, { + api: { + async getEventForTimestamp(roomID, ts) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + throw new MatrixServerError({ + errcode: "M_NOT_FOUND", + error: "Unable to find event from 1726762095974 in direction Direction.FORWARDS" + }, {}) + }, + async getStateEvent(roomID, type, key) { // for ?via calculation + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, + async getJoinedMembers(roomID) { // for ?via calculation + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + return { + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} + } + } + } + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "Me: I'll scroll up to find a certain message I'll send\n_scrolls up and clicks message links for god knows how long_\n_completely forgets what they were looking for and simply begins scrolling up to find some fun moments_\n_stumbles upon:_ " + + "[unknown event, timestamp resolution failed, in room: https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org]", + format: "org.matrix.custom.html", + formatted_body: "Me: I'll scroll up to find a certain message I'll send
    scrolls up and clicks message links for god knows how long
    completely forgets what they were looking for and simply begins scrolling up to find some fun moments
    stumbles upon: " + + '[unknown event, timestamp resolution failed, in room: https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org]' + }]) + t.equal(called, 3, "getEventForTimestamp, getStateEvent, and getJoinedMembers should be called once each") +}) + test("message2event: message link from another server", async t => { const events = await messageToEvent(data.message.message_link_from_another_server, data.guild.general) t.deepEqual(events, [{ From 1d2daf25047a8d234dc1f9995617c1308fc529fd Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Sep 2024 16:50:14 +1200 Subject: [PATCH 111/346] Add web dependencies --- package-lock.json | 276 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + readme.md | 38 ++++--- 3 files changed, 300 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01c6fdd..2843e50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,10 @@ "@cloudrac3r/in-your-element": "^1.0.0", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", + "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/turndown": "^7.1.4", + "@stackoverflow/stacks": "^2.5.7", + "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", @@ -86,6 +89,49 @@ "tap-dot": "bin/dot" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -247,6 +293,45 @@ "node": ">=14.19.0" } }, + "node_modules/@cloudrac3r/pug": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@cloudrac3r/pug/-/pug-4.0.4.tgz", + "integrity": "sha512-RZhxM/WfSHT0n39URlwDdugBfGfwEWmr+w+mCyiT9jaiqCjeZPpXkps/cWLA1XRLo7fzq0+9THtGzVKXS487/A==", + "dependencies": { + "@cloudrac3r/pug-code-gen": "3.0.5", + "@cloudrac3r/pug-lexer": "5.0.3", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/@cloudrac3r/pug-code-gen": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-code-gen/-/pug-code-gen-3.0.5.tgz", + "integrity": "sha512-dKKpy3i9YlVa3lBgu5Jds513c7AtzmmsR2/lGhY2NOODSpIiTcbWLw1obA9YEmmH1tAJny+J6ePYN1N1RgjjQA==", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/@cloudrac3r/pug-lexer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-lexer/-/pug-lexer-5.0.3.tgz", + "integrity": "sha512-ym4g4q+l9IC2H1wXCDnF79AQZ48xtxO675JOT316e17W2wHWtgRccXpT6DkBAaRDZycmkGzSxID1S15T2lZj+g==", + "dependencies": { + "character-parser": "^4.0.0", + "is-expression": "^4.0.0", + "pug-error": "^2.1.0" + } + }, "node_modules/@cloudrac3r/tap-dot": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.3.tgz", @@ -296,6 +381,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@hotwired/stimulus": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", + "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==" + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -788,6 +878,15 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@putout/cli-keypress": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-2.0.0.tgz", @@ -820,6 +919,20 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@stackoverflow/stacks": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.5.7.tgz", + "integrity": "sha512-1ipTt7jqUszyd78Gn9TADT22PL0yXe14iEfgZyvJlDvrNrmyJLoGsFMRMwcduPol6/C/zkFt2dmfph/5vFDcYA==", + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/@stackoverflow/stacks-icons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@stackoverflow/stacks-icons/-/stacks-icons-6.0.2.tgz", + "integrity": "sha512-NDXV/0w6on9fJBfaLrBtPSXTbGyitD+mBTmIpLmDWbVgZo3EJgZBdPdElO0nO7K0WR2Yee1nKhC8euRhd9nicg==" + }, "node_modules/@supertape/engine-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-2.0.0.tgz", @@ -995,6 +1108,17 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -1039,12 +1163,28 @@ "printable-characters": "^1.0.42" } }, + "node_modules/assert-never": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", + "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/backtracker": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-4.0.0.tgz", @@ -1198,6 +1338,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-4.0.0.tgz", + "integrity": "sha512-jWburCrDpd+aPopB7esjh/gLyZoHZS4C2xwwJlkTPyhhJdXG+FCG0P4qCOInvOd9yhiuAEJYlZsUMQ0JSK4ykw==" + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -1348,6 +1493,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -1483,6 +1637,11 @@ "integrity": "sha512-xsH4UwmnCQl4KjAf01/p9ck9s+/vDqzHbUxPOBzo8fcVUa/hQG6qInD7Cr44KAuCM+xCxGJFSAUx450pBrX0+g==", "license": "MIT" }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + }, "node_modules/domino": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", @@ -1842,6 +2001,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1933,6 +2101,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + }, "node_modules/just-kebab-case": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", @@ -2097,6 +2270,14 @@ "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ohash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", @@ -2268,6 +2449,66 @@ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2887,6 +3128,19 @@ "integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==", "dev": true }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" + }, "node_modules/token-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", @@ -3002,6 +3256,14 @@ "node": ">=10.12.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3031,6 +3293,20 @@ "node": ">= 8" } }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 97f5921..4864bef 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "@cloudrac3r/in-your-element": "^1.0.0", "@cloudrac3r/mixin-deep": "^3.0.0", "@cloudrac3r/pngjs": "^7.0.3", + "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/turndown": "^7.1.4", + "@stackoverflow/stacks": "^2.5.7", + "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", "better-sqlite3": "^11.1.2", "chunk-text": "^2.0.1", diff --git a/readme.md b/readme.md index 5b75067..cfb4939 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,7 @@ You'll need: Follow these steps: -1. [Get Node.js version 18 or later](https://nodejs.org/en/download/prebuilt-installer) +1. [Get Node.js version 20 or later](https://nodejs.org/en/download/prebuilt-installer) 1. Clone this repo and checkout a specific tag. (Development happens on main. Stable versions are tagged.) * The latest release tag is ![](https://img.shields.io/gitea/v/release/cadence/out-of-your-element?gitea_url=https%3A%2F%2Fgitdab.com&style=flat-square&label=%20&color=black). @@ -87,19 +87,18 @@ Follow these steps: 1. Add the bot to a server - use any *one* of the following commands for an invite link: * (in the REPL) `addbot` - * (in a chat) `//addbot` * $ `node addbot.js` * $ `npm run addbot` * $ `./addbot.sh` Now any message on Discord will create the corresponding rooms on Matrix-side. After the rooms have been created, Matrix and Discord users can chat back and forth. -To get into the rooms on your Matrix account, either add yourself to `invite` in `registration.yaml`, or use the `//invite [your mxid here]` command on Discord. +To get into the rooms on your Matrix account, use the `/invite [your mxid here]` command on Discord. # Development setup * Install development dependencies with `npm install --save-dev` so you can run the tests. -* Any files you change will automatically be reloaded, except for `stdin.js` and `d2m/discord-*.js`. +* Most files you change, such as actions, converters, and web, will automatically be reloaded. * If developing on a different computer to the one running the homeserver, use SSH port forwarding so that Synapse can connect on its `localhost:6693` to reach the running bridge on your computer. Example: `ssh -T -v -R 6693:localhost:6693 me@matrix.cadence.moe` * I recommend developing in Visual Studio Code so that the JSDoc x TypeScript annotation comments work. I don't know which other editors or language servers support annotations and type inference. @@ -160,35 +159,40 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in ## Dependency justification -(deduped transitive dependency count) dependency name: explanation +Total transitive production dependencies: 147 -* (0) @chriscdn/promise-semaphore: It does what I want! I like it! -* (1) @cloudrac3r/discord-markdown: This is my fork! -* (0) @cloudrac3r/giframe: This is my fork! -* (1) @cloudrac3r/html-template-tag: This is my fork! +### 🦕 + +* (31) better-sqlite3: SQLite3 is the best database, and this is the best library for it. +* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.) +* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. +* (14) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, and more. 14 transitive dependencies is on the low end for a web server. +* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. + +### 🪱 + +* (0) @chriscdn/promise-semaphore: It does what I want. +* (1) @cloudrac3r/discord-markdown: This is my fork. +* (0) @cloudrac3r/giframe: This is my fork. +* (1) @cloudrac3r/html-template-tag: This is my fork. * (0) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It depends on h3 and zod, which are already pulled in by OOYE. -* (0) @cloudrac3r/mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.) +* (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. +* (3) @stackoverflow/stacks: Stack Overflow design language and icons. * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. -* (42) better-sqlite3: SQLite3 is the best database, and this is the best library for it. Really! I love it. * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. * (0) domino: DOM implementation that's already pulled in by turndown. * (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. * (0) get-stream: Only needed if content_length_workaround is true. -* (14) h3: HTTP server. OOYE needs this for the appservice listener, authmedia proxy, and more. 14 transitive dependencies is on the low end for an HTTP server. * (1) heatsync: Module hot-reloader that I trust. * (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON. * (0) minimist: It's already pulled in by better-sqlite3->prebuild-install. * (3) node-fetch@2: I like it and it does what I want. Version 2 is used because version 3 is ESM-only. * (0) prettier-bytes: It does what I want and has no dependencies. -* (51) sharp: Image compositing and processing. Jimp has fewer dependencies, but sharp is faster. -* (8) snowtransfer: Discord API library with bring-your-own-caching that I trust. -* (10) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. +* (2) snowtransfer: Discord API library with bring-your-own-caching that I trust. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. * (0) zod: Input validation for the web server. It's popular and easy to use. - -Total transitive production dependencies: 116 From b6c23c30fb2d3179863871ef9c3a7d5c4ff3c275 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 22 Sep 2024 15:42:15 +1200 Subject: [PATCH 112/346] Upload web code --- package-lock.json | 2 +- package.json | 1 + readme.md | 1 + scripts/seed.js | 23 ++- scripts/start-server.js | 42 +++++ src/d2m/discord-client.js | 2 +- .../migrations/0014-add-guild-autocreate.sql | 5 + src/db/orm-defs.d.ts | 1 + src/db/orm.js | 9 +- src/types.d.ts | 1 + src/web/pug-sync.js | 78 +++++++++ src/web/pug/guild.pug | 159 ++++++++++++++++++ src/web/pug/home.pug | 24 +++ src/web/pug/includes/hash.svg | 46 +++++ src/web/pug/includes/template.pug | 66 ++++++++ src/web/pug/invite.pug | 32 ++++ src/web/pug/ok.pug | 6 + src/web/routes/guild-settings.js | 23 +++ src/web/routes/invite.js | 99 +++++++++++ src/web/routes/oauth.js | 92 ++++++++++ src/web/server.js | 58 ++++++- src/web/static/htmx.min.js | 1 + 22 files changed, 765 insertions(+), 6 deletions(-) create mode 100755 scripts/start-server.js create mode 100644 src/db/migrations/0014-add-guild-autocreate.sql create mode 100644 src/web/pug-sync.js create mode 100644 src/web/pug/guild.pug create mode 100644 src/web/pug/home.pug create mode 100644 src/web/pug/includes/hash.svg create mode 100644 src/web/pug/includes/template.pug create mode 100644 src/web/pug/invite.pug create mode 100644 src/web/pug/ok.pug create mode 100644 src/web/routes/guild-settings.js create mode 100644 src/web/routes/invite.js create mode 100644 src/web/routes/oauth.js create mode 100644 src/web/static/htmx.min.js diff --git a/package-lock.json b/package-lock.json index 2843e50..178690a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.3", + "lru-cache": "^10.4.3", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", @@ -2131,7 +2132,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/make-dir": { diff --git a/package.json b/package.json index 4864bef..ba02a83 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.3", + "lru-cache": "^10.4.3", "minimist": "^1.2.8", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", diff --git a/readme.md b/readme.md index cfb4939..992d0de 100644 --- a/readme.md +++ b/readme.md @@ -189,6 +189,7 @@ Total transitive production dependencies: 147 * (0) get-stream: Only needed if content_length_workaround is true. * (1) heatsync: Module hot-reloader that I trust. * (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON. +* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used. * (0) minimist: It's already pulled in by better-sqlite3->prebuild-install. * (3) node-fetch@2: I like it and it does what I want. Version 2 is used because version 3 is ESM-only. * (0) prettier-bytes: It does what I want and has no dependencies. diff --git a/scripts/seed.js b/scripts/seed.js index 300ee29..f20afb7 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -38,6 +38,7 @@ const passthrough = require("../src/passthrough") const db = new sqlite("ooye.db") const migrate = require("../src/db/migrate") +/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, {sync, db}) @@ -166,8 +167,28 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } } }) + + console.log("What is your Discord client secret?") + /** @type {{discord_client_secret: string}} */ + const clientSecretResponse = await prompt({ + type: "input", + name: "discord_client_secret", + message: "Client secret" + }) + const template = getTemplateRegistration() - reg = {...template, url: bridgeOriginResponse.bridge_origin, ooye: {...template.ooye, ...serverNameResponse, ...bridgeOriginResponse, server_origin: serverOrigin, ...discordTokenResponse}} + reg = { + ...template, + url: bridgeOriginResponse.bridge_origin, + ooye: { + ...template.ooye, + ...serverNameResponse, + ...bridgeOriginResponse, + server_origin: serverOrigin, + ...discordTokenResponse, + ...clientSecretResponse + } + } registration.reg = reg checkRegistration(reg) writeRegistration(reg) diff --git a/scripts/start-server.js b/scripts/start-server.js new file mode 100755 index 0000000..430b3ba --- /dev/null +++ b/scripts/start-server.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// @ts-check + +const {createServer} = require("http") +const EventEmitter = require("events") +const {createApp, createRouter, toNodeListener} = require("h3") +const sqlite = require("better-sqlite3") +const migrate = require("../src/db/migrate") +const HeatSync = require("heatsync") + +const {reg} = require("../src/matrix/read-registration") +const passthrough = require("../src/passthrough") +const db = new sqlite("ooye.db") + +/** @type {import("heatsync").default} */ // @ts-ignore +const sync = new HeatSync() + +Object.assign(passthrough, {sync, db}) + +const DiscordClient = require("../src/d2m/discord-client") + +const discord = new DiscordClient(reg.ooye.discord_token, "half") +passthrough.discord = discord + +const app = createApp() +const router = createRouter() +app.use(router) +const server = createServer(toNodeListener(app)) +server.listen(reg.socket || new URL(reg.url).port) +const as = Object.assign(new EventEmitter(), {app, router, server}) // @ts-ignore +passthrough.as = as + +const orm = sync.require("../src/db/orm") +passthrough.from = orm.from +passthrough.select = orm.select + +;(async () => { + await migrate.migrate(db) + await discord.cloud.connect() + console.log("Discord gateway started") + sync.require("../src/web/server") +})() diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index ace8481..bffb904 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -6,7 +6,7 @@ const { Client: CloudStorm } = require("cloudstorm") const passthrough = require("../passthrough") const { sync } = passthrough -/** @type {typeof import("./discord-packets")} */ +/** @type {import("./discord-packets")} */ const discordPackets = sync.require("./discord-packets") class DiscordClient { diff --git a/src/db/migrations/0014-add-guild-autocreate.sql b/src/db/migrations/0014-add-guild-autocreate.sql new file mode 100644 index 0000000..a6ad212 --- /dev/null +++ b/src/db/migrations/0014-add-guild-autocreate.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE guild_space ADD COLUMN autocreate INTEGER NOT NULL DEFAULT 1; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index a0b6e51..a9c2c85 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -31,6 +31,7 @@ export type Models = { guild_id: string space_id: string privacy_level: number + autocreate: number } lottie: { diff --git a/src/db/orm.js b/src/db/orm.js index 601a7a0..646012b 100644 --- a/src/db/orm.js +++ b/src/db/orm.js @@ -15,8 +15,13 @@ function select(table, cols, where = {}, e = "") { if (!Array.isArray(cols)) cols = [cols] const parameters = [] const wheres = Object.entries(where).map(([col, value]) => { - parameters.push(value) - return `"${col}" = ?` + if (Array.isArray(value)) { + parameters.push(...value) + return `"${col}" IN (` + Array(value.length).fill("?").join(", ") + ")" + } else { + parameters.push(value) + return `"${col}" = ?` + } }) const whereString = wheres.length ? " WHERE " + wheres.join(" AND ") : "" /** @type {U.Prepared>} */ diff --git a/src/types.d.ts b/src/types.d.ts index 53f9f30..62d9b30 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -24,6 +24,7 @@ export type AppServiceRegistrationConfig = { server_origin: string bridge_origin: string discord_token: string + discord_client_secret: string content_length_workaround: boolean include_user_id_in_mxid: boolean invite: string[] diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js new file mode 100644 index 0000000..32e7acc --- /dev/null +++ b/src/web/pug-sync.js @@ -0,0 +1,78 @@ +// @ts-check + +const assert = require("assert/strict") +const fs = require("fs") +const {join} = require("path") +const h3 = require("h3") +const {defineEventHandler, defaultContentType, setResponseStatus, useSession, getQuery} = h3 +const {compileFile} = require("@cloudrac3r/pug") + +const {as} = require("../passthrough") +const {reg} = require("../matrix/read-registration") + +// Pug + +let globals = {} + +/** @type {Map) => Promise>} */ +const pugCache = new Map() + +function addGlobals(obj) { + Object.assign(globals, obj) +} + +/** + * @param {import("h3").H3Event} event + * @param {string} filename + * @param {Record} locals + */ +function render(event, filename, locals) { + const path = join(__dirname, "pug", filename) + + function compile() { + try { + const template = compileFile(path, {}) + pugCache.set(path, async (event, locals) => { + defaultContentType(event, "text/html; charset=utf-8") + const session = await useSession(event, {password: reg.as_token}) + return template(Object.assign({}, + getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything + globals, // Globals + locals, // Explicit locals overwrite globals in case we need to DI something + {session} // Session is always session because it has to be trusted + )) + }) + } catch (e) { + pugCache.set(path, async (event) => { + setResponseStatus(event, 500, "Internal Template Error") + defaultContentType(event, "text/plain") + return e.toString() + }) + } + } + + if (!pugCache.has(path)) { + compile() + fs.watch(path, {persistent: false}, compile) + fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile) + } + + const cb = pugCache.get(path) + assert(cb) + return cb(event, locals) +} + +/** + * @param {import("h3").Router} router + * @param {string} url + * @param {string} filename + */ +function createRoute(router, url, filename) { + router.get(url, defineEventHandler(async event => { + return render(event, filename, {}) + })) +} + +module.exports.addGlobals = addGlobals +module.exports.render = render +module.exports.createRoute = createRoute diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug new file mode 100644 index 0000000..9cc0772 --- /dev/null +++ b/src/web/pug/guild.pug @@ -0,0 +1,159 @@ +extends includes/template.pug + +mixin badge-readonly + .s-badge.s-badge__xs.s-badge__icon.s-badge__muted + != icons.Icons.IconEyeSm + | Read-only + +mixin badge-private + .s-badge.s-badge__xs.s-badge__icon.s-badge__warning + != icons.Icons.IconLockSm + | Private + +mixin discord(channel, radio=false) + - let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, "", channel.permission_overwrites) + .s-user-card.s-user-card__small + if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) + != icons.Icons.IconLock + else if channel.type === 5 + != icons.Icons.IconBullhorn + else if channel.type === 2 + != icons.Icons.IconPhone + else if channel.type === 11 || channel.type === 12 + != icons.Icons.IconCollection + else + include includes/hash.svg + .s-user-card--info.ws-nowrap + if radio + = channel.name + else + .s-user-card--link.fs-body1 + a(href=`https://discord.com/channels/${channel.guild_id}/${channel.id}`)= channel.name + if channel.parent_id + .s-user-card--location= discord.channels.get(channel.parent_id).name + if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) + +badge-private + else if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages) + +badge-readonly + +mixin matrix(row, radio=false, badge="") + .s-user-card.s-user-card__small + != icons.Icons.IconMessage + .s-user-card--info.ws-nowrap + if radio + = row.nick || row.name + else + .s-user-card--link.fs-body1 + a(href=`https://matrix.to/#/${row.room_id}`)= row.nick || row.name + +block body + if !guild_id && session.data.managedGuilds + .s-empty-state.wmx4.p48 + != icons.Spots.SpotEmptyXL + p Select a server from the top right corner to continue. + p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.] + + else if !session.data.managedGuilds + .s-empty-state.wmx4.p48 + != icons.Spots.SpotEmptyXL + p You need to log in to manage your servers. + a.s-btn.s-btn__icon.s-btn__filled(href="/oauth") + != icons.Icons.IconDiscord + = ` Log in with Discord` + + else if !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id) + .s-empty-state.wmx4.p48 + != icons.Spots.SpotAlertXL + p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord. + p If you've checked your permissions, try #[a(href="/oauth") logging in again.] + + else + - let guild = discord.guilds.get(guild_id) + + .s-page-title.mb24 + h1.s-page-title--header= guild.name + + .d-flex.g16 + .fl-grow1 + h2.fs-headline1 Invite a Matrix user + + form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") + label.s-label(for="mxid") Matrix ID + input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") + label.s-label(for="permissions") Permissions + .s-select + select#permissions(name="permissions") + option(value="default") Default + option(value="moderator") Moderator + input(type="hidden" name="guild_id" value=guild_id) + .grid--row-start2 + button.s-btn.s-btn__filled.htmx-indicator Invite + div + - + let size = 105 + let src = new URL(`https://api.qrserver.com/v1/create-qr-code/?qzone=1&format=svg&size=${size}x${size}`) + src.searchParams.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) + img(width=size height=size src=src.toString()) + + h2.mt48.fs-headline1 Linked channels + + - + function getPosition(channel) { + let position = 0 + let looking = channel + while (looking.parent_id) { + looking = discord.channels.get(looking.parent_id) + position = looking.position * 1000 + } + if (channel.position) position += channel.position + return position + } + let channelIDs = discord.guildChannelMap.get(guild_id) + + let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all() + let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c})).filter(c => c.channel) + let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id) + linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel)) + + let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) + let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => [0, 5].includes(c.type)) + unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) + .s-card.bs-sm.p0 + .s-table-container + table.s-table.s-table__bx-simple + each row in linkedChannelsWithDetails + tr + td.w40: +discord(row.channel) + td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm + td: +matrix(row) + else + tr + td(colspan="3") + .s-empty-state No channels linked between Discord and Matrix yet... + + h3.mt32.fs-category Auto-create + .s-card + form.d-flex.ai-center.g8 + label.s-label.fl-grow1(for="autocreate") + | Create new Matrix rooms automatically + p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when a new Discord channel is spoken in. + - let value = select("guild_space", "autocreate", {guild_id}).pluck().get() + input(type="hidden" name="guild_id" value=guild_id) + input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) + .is-loading#autocreate-loading + + h3.mt32.fs-category Manually link channels + form.d-flex.g16.ai-start(method="post" action="/api/link") + .fl-grow2.s-btn-group.fd-column.w40 + each channel in unlinkedChannels + input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) + label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id) + +discord(channel, true, "Announcement") + else + .s-empty-state.p8 All Discord channels are linked. + .fl-grow1.s-btn-group.fd-column.w30 + .s-empty-state.p8 I don't know how to get the Matrix room list yet... + div + button.s-btn.s-btn__icon.s-btn__filled + != icons.Icons.IconLink + = ` Connect` diff --git a/src/web/pug/home.pug b/src/web/pug/home.pug new file mode 100644 index 0000000..1ad787d --- /dev/null +++ b/src/web/pug/home.pug @@ -0,0 +1,24 @@ +extends includes/template.pug + +block body + .s-page-title.mb24 + h1.s-page-title--header Bridge a Discord server + + .d-grid.grid__2.g24 + .s-card.bs-md.d-flex.fd-column + h2 Easy mode + p Add the bot to your Discord server. + p It will automatically create new Matrix rooms for you. + .fl-grow1 + a.s-btn.s-btn__filled.s-btn__icon(href="/oauth?action=add") + != icons.Icons.IconPlus + = ` Add to server` + .s-card.bs-md.d-flex.fd-column + h2 Self-service + p OOYE will link an existing Discord server and Matrix space together. + p Choose this option if you already have a community set up on Matrix. + p Or, choose this if you're migrating from a different bridge. + .fl-grow1 + a.s-btn.s-btn__outlined.s-btn__icon(href="/oauth?action=add-self-service") + != icons.Icons.IconUnorderedList + = ` Set up self-service` diff --git a/src/web/pug/includes/hash.svg b/src/web/pug/includes/hash.svg new file mode 100644 index 0000000..0f6fdd7 --- /dev/null +++ b/src/web/pug/includes/hash.svg @@ -0,0 +1,46 @@ + + diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug new file mode 100644 index 0000000..c117056 --- /dev/null +++ b/src/web/pug/includes/template.pug @@ -0,0 +1,66 @@ +mixin guild(guild) + span.s-avatar.s-avatar__32.s-user-card--avatar + if guild.icon + img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`) + else + .s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0] + .s-user-card--info.ai-start + strong= guild.name + ul.s-user-card--awards + li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels + +doctype html +html(lang="en") + head + title Out Of Your Element + link(rel="stylesheet" type="text/css" href="/static/stacks.min.css") + + meta(name="htmx-config" content='{"indicatorClass":"is-loading"}') + style. + .themed { + --theme-base-primary-color-h: 266; + --theme-base-primary-color-s: 53%; + --theme-base-primary-color-l: 63%; + --theme-dark-primary-color-h: 266; + --theme-dark-primary-color-s: 53%; + --theme-dark-primary-color-l: 63%; + } + body.themed.theme-system + header.s-topbar + .s-topbar--skip-link(href="#content") Skip to main content + .s-topbar--container.wmx9 + a.s-topbar--logo(href="/") + img.s-avatar.s-avatar__32(src="/icon.png") + nav.s-topbar--navigation + ul.s-topbar--content + li.ps-relative + if !session.data.managedGuilds || session.data.managedGuilds.length === 0 + a.s-btn.s-btn__icon.as-center(href="/oauth") + != icons.Icons.IconDiscord + = ` Log in` + else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id) + button.s-topbar--item.s-btn.s-btn__muted.s-user-card(popovertarget="guilds") + +guild(discord.guilds.get(guild_id)) + else if session.data.managedGuilds + button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.s-label(popovertarget="guilds") + | Your servers + #guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible + .s-popover--arrow.s-popover--arrow__tc + .s-popover--content.overflow-y-auto.overflow-x-hidden + ul.s-menu(role="menu") + each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g) + li(role="menuitem") + a.s-topbar--item.s-user-card.d-flex.p4(href=`/guild?guild_id=${guild.id}`) + +guild(guild) + .mx-auto.w100.wmx9.py24#content + block body + script. + document.querySelectorAll("[popovertarget]").forEach(e => { + e.addEventListener("click", () => { + const rect = e.getBoundingClientRect() + const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }` + // console.log(t) + document.styleSheets[0].insertRule(t) + }) + }) + script(src="/static/htmx.min.js") diff --git a/src/web/pug/invite.pug b/src/web/pug/invite.pug new file mode 100644 index 0000000..e346880 --- /dev/null +++ b/src/web/pug/invite.pug @@ -0,0 +1,32 @@ +extends includes/template.pug + +block body + if !isValid + .s-empty-state.wmx4.p48 + != icons.Spots.SpotAlertXL + p This QR code has expired. + p Refresh the guild management page to generate a new one. + + else + - let guild = discord.guilds.get(guild_id) + + .s-page-title.mb24 + h1.s-page-title--header= guild.name + + .d-flex.g16 + .fl-grow1 + h2.fs-headline1 Invite a Matrix user + + form.d-flex.gy16.fd-column(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") + .d-flex.gy4.fd-column + label.s-label(for="mxid") Matrix ID + input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") + .d-flex.gy4.fd-column + label.s-label(for="permissions") Permissions + .s-select + select#permissions(name="permissions") + option(value="default") Default + option(value="moderator") Moderator + input(type="hidden" name="nonce" value=nonce) + div + button.s-btn.s-btn__filled.htmx-indicator Invite diff --git a/src/web/pug/ok.pug b/src/web/pug/ok.pug new file mode 100644 index 0000000..9aed737 --- /dev/null +++ b/src/web/pug/ok.pug @@ -0,0 +1,6 @@ +extends includes/template.pug + +block body + .ta-center.wmx5.p48.mx-auto + != icons.Spots.SpotApproveXL + p.mt24.fs-body2= msg diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js new file mode 100644 index 0000000..d67a5f2 --- /dev/null +++ b/src/web/routes/guild-settings.js @@ -0,0 +1,23 @@ +// @ts-check + +const {z} = require("zod") +const {defineEventHandler, sendRedirect, useSession, createError, readValidatedBody} = require("h3") + +const {as, db} = require("../../passthrough") +const {reg} = require("../../matrix/read-registration") + +const schema = { + autocreate: z.object({ + guild_id: z.string(), + autocreate: z.string().optional() + }) +} + +as.router.post("/api/autocreate", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.autocreate.parse) + const session = await useSession(event, {password: reg.as_token}) + if (!(session.data.managedGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) + + db.prepare("UPDATE guild_space SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id) + return sendRedirect(event, `/guild?guild_id=${parsedBody.guild_id}`, 302) +})) diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js new file mode 100644 index 0000000..eec7a3c --- /dev/null +++ b/src/web/routes/invite.js @@ -0,0 +1,99 @@ +// @ts-check + +const assert = require("assert/strict") +const {z} = require("zod") +const {defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3") +const {randomUUID} = require("crypto") +const {LRUCache} = require("lru-cache") + +const {discord, as, sync, select} = require("../../passthrough") +/** @type {import("../pug-sync")} */ +const pugSync = sync.require("../pug-sync") +const {reg} = require("../../matrix/read-registration") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +const schema = { + guild: z.object({ + guild_id: z.string().optional() + }), + invite: z.object({ + mxid: z.string().regex(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/), + permissions: z.enum(["default", "moderator"]), + guild_id: z.string().optional(), + nonce: z.string().optional() + }), + inviteNonce: z.object({ + nonce: z.string() + }) +} + +/** @type {LRUCache} nonce to guild id */ +const validNonce = new LRUCache({max: 200}) + +as.router.get("/guild", defineEventHandler(async event => { + const {guild_id} = await getValidatedQuery(event, schema.guild.parse) + const nonce = randomUUID() + if (guild_id) { + // Security note: the nonce alone is valid for updating the guild + // We have not verified the user has sufficient permissions in the guild at generation time + // These permissions are checked later during page rendering and the generated nonce is only revealed if the permissions are sufficient + validNonce.set(nonce, guild_id) + } + return pugSync.render(event, "guild.pug", {nonce}) +})) + +as.router.get("/invite", defineEventHandler(async event => { + const {nonce} = await getValidatedQuery(event, schema.inviteNonce.parse) + const isValid = validNonce.has(nonce) + const guild_id = validNonce.get(nonce) + const guild = discord.guilds.get(guild_id || "") + return pugSync.render(event, "invite.pug", {isValid, nonce, guild_id, guild}) +})) + +as.router.post("/api/invite", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.invite.parse) + const session = await useSession(event, {password: reg.as_token}) + + // Check guild ID or nonce + if (parsedBody.guild_id) { + var guild_id = parsedBody.guild_id + if (!(session.data.managedGuilds || []).includes(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't invite users to a guild you don't have Manage Server permissions in"}) + } else if (parsedBody.nonce) { + if (!validNonce.has(parsedBody.nonce)) throw createError({status: 403, message: "Nonce expired", data: "Nonce means number-used-once, and, well, you tried to use it twice..."}) + let ok = validNonce.get(parsedBody.nonce) + assert(ok) + var guild_id = ok + validNonce.delete(parsedBody.nonce) + } else { + throw createError({status: 400, message: "Missing guild ID", data: "Passing a guild ID or a nonce is required."}) + } + + // Check guild is bridged + const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get() + if (!spaceID) throw createError({status: 428, message: "Server not bridged", data: "You can only invite Matrix users to servers that are bridged to Matrix."}) + + // Check for existing invite to the space + let spaceMember + try { + spaceMember = await api.getStateEvent(spaceID, "m.room.member", parsedBody.mxid) + } catch (e) {} + if (spaceMember && (spaceMember.membership === "invite" || spaceMember.membership === "join")) { + return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302) + } + + // Invite + await api.inviteToRoom(spaceID, parsedBody.mxid) + + // Permissions + if (parsedBody.permissions === "moderator") { + await api.setUserPowerCascade(spaceID, parsedBody.mxid, 50) + } + + if (parsedBody.guild_id) { + return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302) + } else { + return sendRedirect(event, "/ok?msg=User has been invited.", 302) + } +})) diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js new file mode 100644 index 0000000..f3078ad --- /dev/null +++ b/src/web/routes/oauth.js @@ -0,0 +1,92 @@ +// @ts-check + +const {z} = require("zod") +const {randomUUID} = require("crypto") +const {defineEventHandler, getValidatedQuery, sendRedirect, getQuery, useSession, createError} = require("h3") +const {SnowTransfer} = require("snowtransfer") +const DiscordTypes = require("discord-api-types/v10") +const fetch = require("node-fetch") + +const {as} = require("../../passthrough") +const {id} = require("../../../addbot") +const {reg} = require("../../matrix/read-registration") + +const redirect_uri = `${reg.ooye.bridge_origin}/oauth` + +const schema = { + first: z.object({ + action: z.string().optional() + }), + code: z.object({ + state: z.string(), + code: z.string(), + guild_id: z.string().optional() + }), + token: z.object({ + token_type: z.string(), + access_token: z.string(), + expires_in: z.number({coerce: true}), + refresh_token: z.string(), + scope: z.string() + }) +} + +as.router.get("/oauth", defineEventHandler(async event => { + const session = await useSession(event, {password: reg.as_token}) + let scope = "guilds" + + const parsedFirstQuery = await getValidatedQuery(event, schema.first.safeParse) + if (parsedFirstQuery.data?.action === "add") { + scope = "bot+guilds" + await session.update({selfService: false}) + } else if (parsedFirstQuery.data?.action === "add-self-service") { + scope = "bot+guilds" + await session.update({selfService: true}) + } + + async function tryAgain() { + const newState = randomUUID() + await session.update({state: newState}) + return sendRedirect(event, `https://discord.com/oauth2/authorize?client_id=${id}&scope=${scope}&permissions=1610883072&response_type=code&redirect_uri=${redirect_uri}&state=${newState}`) + } + + const parsedQuery = await getValidatedQuery(event, schema.code.safeParse) + if (!parsedQuery.success) return tryAgain() + + const savedState = session.data.state + if (!savedState) throw createError({status: 400, message: "Missing state", data: "Missing saved state parameter. Please try again, and make sure you have cookies enabled."}) + if (savedState != parsedQuery.data.state) return tryAgain() + + const res = await fetch("https://discord.com/api/oauth2/token", { + method: "post", + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: id, + client_secret: reg.ooye.discord_client_secret, + redirect_uri, + code: parsedQuery.data.code + }) + }) + const root = await res.json() + + const parsedToken = schema.token.safeParse(root) + if (!res.ok || !parsedToken.success) { + throw createError({status: 502, message: "Invalid token response", data: `Discord completed OAuth, but returned this instead of an OAuth access token: ${JSON.stringify(root)}`}) + } + + const client = new SnowTransfer(`Bearer ${parsedToken.data.access_token}`) + try { + const guilds = await client.user.getGuilds() + const managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id) + await session.update({managedGuilds}) + } catch (e) { + throw createError({status: 502, message: "API call failed", data: e.message}) + } + + if (parsedQuery.data.guild_id) { + // TODO: we probably need to create a matrix space and database entry immediately here so that self-service settings apply and so matrix users can be invited + return sendRedirect(event, `/guild?guild_id=${parsedQuery.data.guild_id}`, 302) + } + + return sendRedirect(event, "/", 302) +})) diff --git a/src/web/server.js b/src/web/server.js index 967885c..387439f 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -1,6 +1,62 @@ // @ts-check -const {sync, as} = require("../passthrough") +const fs = require("fs") +const {join} = require("path") +const h3 = require("h3") +const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, setResponseStatus, useSession, getQuery, handleCacheHeaders} = h3 +const icons = require("@stackoverflow/stacks-icons") +const DiscordTypes = require("discord-api-types/v10") +const dUtils = require("../discord/utils") + +const {sync, discord, as, select} = require("../passthrough") +/** @type {import("./pug-sync")} */ +const pugSync = sync.require("./pug-sync") +const {id} = require("../../addbot") + +// Pug + +pugSync.addGlobals({id, h3, discord, select, DiscordTypes, dUtils, icons}) +pugSync.createRoute(as.router, "/", "home.pug") +pugSync.createRoute(as.router, "/ok", "ok.pug") + +// Routes sync.require("./routes/download-matrix") sync.require("./routes/download-discord") +sync.require("./routes/invite") +sync.require("./routes/guild-settings") +sync.require("./routes/oauth") + +// Files + +function compressResponse(event, response) { + if (!getRequestHeader(event, "accept-encoding")?.includes("gzip")) return + if (typeof response.body !== "string") return + /** @type {ReadableStream} */ // @ts-ignore + const stream = new Response(response.body).body + setResponseHeader(event, "content-encoding", "gzip") + response.body = stream.pipeThrough(new CompressionStream("gzip")) +} + +as.router.get("/static/stacks.min.css", defineEventHandler({ + onBeforeResponse: compressResponse, + handler: async event => { + handleCacheHeaders(event, {maxAge: 86400}) + defaultContentType(event, "text/css") + return fs.promises.readFile(require.resolve("@stackoverflow/stacks/dist/css/stacks.css"), "utf-8") + } +})) + +as.router.get("/static/htmx.min.js", defineEventHandler({ + onBeforeResponse: compressResponse, + handler: async event => { + handleCacheHeaders(event, {maxAge: 86400}) + defaultContentType(event, "text/javascript") + return fs.promises.readFile(join(__dirname, "static", "htmx.min.js"), "utf-8") + } +})) + +as.router.get("/icon.png", defineEventHandler(event => { + handleCacheHeaders(event, {maxAge: 86400}) + return fs.promises.readFile(join(__dirname, "../../docs/img/icon.png")) +})) diff --git a/src/web/static/htmx.min.js b/src/web/static/htmx.min.js new file mode 100644 index 0000000..c11fbbd --- /dev/null +++ b/src/web/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.2"};Q.onLoad=$;Q.process=Dt;Q.on=be;Q.off=we;Q.trigger=de;Q.ajax=Hn;Q.find=r;Q.findAll=p;Q.closest=g;Q.remove=K;Q.addClass=Y;Q.removeClass=o;Q.toggleClass=W;Q.takeClass=ge;Q.swap=ze;Q.defineExtension=Bn;Q.removeExtension=Un;Q.logAll=z;Q.logNone=J;Q.parseInterval=h;Q._=_;const n={addTriggerHandler:Et,bodyContains:le,canAccessLocalStorage:j,findThisElement:Ee,filterValues:hn,swap:ze,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:T,getExpressionVars:Cn,getHeaders:dn,getInputValues:cn,getInternalData:ie,getSwapSpecification:pn,getTriggerSpecs:lt,getTarget:Ce,makeFragment:D,mergeObjects:ue,makeSettleInfo:xn,oobSwap:Te,querySelectorExt:ae,settleImmediately:Gt,shouldCancel:ht,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Bt};const v=["get","post","put","delete","patch"];const O=v.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");const R=e("head");function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function H(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function T(e,t){while(e&&!t(e)){e=u(e)}return e||null}function q(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;T(t,function(e){return!!(r=q(t,ce(e),n))});if(r!=="unset"){return r}}function f(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function L(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function N(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function A(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function I(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function P(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function k(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(P(e)){const t=I(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){w(e)}finally{e.remove()}}})}function D(e){const t=e.replace(R,"");const n=L(t);let r;if(n==="html"){r=new DocumentFragment;const i=N(e);A(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=N(t);A(r,i.body);r.title=i.title}else{const i=N('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){k(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function M(e){return typeof e==="function"}function X(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function U(e){return e.trim().split(/\s+/)}function ue(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){w(e);return null}}function j(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function V(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function _(e){return vn(ne().body,function(){return eval(e)})}function $(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function z(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function J(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function p(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return p(ne(),e)}}function E(){return window}function K(e,t){e=y(e);if(t){E().setTimeout(function(){K(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function G(e){return e instanceof HTMLElement?e:null}function Z(e){return typeof e==="string"?e:null}function d(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function Y(e,t,n){e=ce(y(e));if(!e){return}if(n){E().setTimeout(function(){Y(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function o(e,t,n){let r=ce(y(e));if(!r){return}if(n){E().setTimeout(function(){o(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function ge(e,t){e=y(e);se(e.parentElement.children,function(e){o(e,t)});Y(ce(e),t)}function g(e,t){e=ce(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||f(e,t)){return e}}while(e=e&&ce(u(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function pe(e,t){return e.substring(e.length-t.length)===t}function i(e){const t=e.trim();if(l(t,"<")&&pe(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ce(e),i(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(d(e),i(t.substr(5)))]}else if(t==="next"){return[ce(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[me(e,i(t.substr(5)),!!n)]}else if(t==="previous"){return[ce(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[ye(e,i(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[H(e,!!n)]}else if(t.indexOf("global ")===0){return m(e,t.slice(7),true)}else{return F(d(H(e,!!n)).querySelectorAll(i(t)))}}var me=function(t,e,n){const r=d(H(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(d(t)||document,e)}else{return e}}function xe(e,t,n){if(M(t)){return{target:ne().body,event:Z(e),listener:t}}else{return{target:y(e),event:Z(t),listener:n}}}function be(t,n,r){_n(function(){const e=xe(t,n,r);e.target.addEventListener(e.event,e.listener)});const e=M(n);return e?n:r}function we(t,n,r){_n(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return M(n)?n:r}const ve=ne().createElement("output");function Se(e,t){const n=re(e,t);if(n){if(n==="this"){return[Ee(e,t)]}else{const r=m(e,n);if(r.length===0){w('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Ee(e,t){return ce(T(e,function(e){return te(ce(e),t)!=null}))}function Ce(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Ee(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Oe(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{s=e}const n=ne().querySelectorAll(t);if(n){se(n,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!He(s,e)){t=d(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){_e(s,e,e,t,i)}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function qe(e){se(p(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){e.parentNode.replaceChild(n,e)}})}function Le(l,e,u){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=d(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Re(t,i);u.tasks.push(function(){Re(t,s)})}}})}function Ne(e){return function(){o(e,Q.config.addedClass);Dt(ce(e));Ae(d(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=G(f(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;Y(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function ze(e,t,r,o){if(!o){o={}}e=y(e);const n=document.activeElement;let i={};try{i={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const s=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=D(t);s.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(l,r.settleDelay)}else{l()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(X(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(nt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function b(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function ot(e){let t;if(e.length>0&&Qe.test(e[0])){e.shift();t=b(e,et).trim();e.shift()}else{t=b(e,x)}return t}const it="input, textarea, select";function st(e,t,n){const r=[];const o=tt(t);do{b(o,We);const l=o.length;const u=b(o,/[,\[\s]/);if(u!==""){if(u==="every"){const c={trigger:"every"};b(o,We);c.pollInterval=h(b(o,/[,\[\s]/));b(o,We);var i=rt(e,o,"event");if(i){c.eventFilter=i}r.push(c)}else{const a={trigger:u};var i=rt(e,o,"event");if(i){a.eventFilter=i}while(o.length>0&&o[0]!==","){b(o,We);const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(b(o,x))}else if(f==="from"&&o[0]===":"){o.shift();if(Qe.test(o[0])){var s=ot(o)}else{var s=b(o,x);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=ot(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=ot(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(b(o,x))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=b(o,x)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=ot(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=b(o,x)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}b(o,We)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function lt(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||st(e,t,r)}if(n.length>0){return n}else if(f(e,"form")){return[{trigger:"submit"}]}else if(f(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(f(e,it)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function ut(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function at(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function dt(t,n,e){if(t instanceof HTMLAnchorElement&&at(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";if(r==="get"){}o=ee(t,"action")}e.forEach(function(e){mt(t,function(e,t){const n=ce(e);if(ft(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ce(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(f(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function gt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function mt(s,l,e,u,c){const a=ie(s);let t;if(u.from){t=m(s,u.from)}else{t=[s]}if(u.changed){t.forEach(function(e){const t=ie(e);t.lastValue=e.value})}se(t,function(o){const i=function(e){if(!le(s)){o.removeEventListener(u.trigger,i);return}if(gt(s,e)){return}if(c||ht(e,s)){e.preventDefault()}if(pt(u,s,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(s)<0){t.handledFor.push(s);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!f(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=ie(o);const r=o.value;if(n.lastValue===r){return}n.lastValue=r}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){de(s,"htmx:trigger");l(s,e);a.throttle=E().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=E().setTimeout(function(){de(s,"htmx:trigger");l(s,e)},u.delay)}else{de(s,"htmx:trigger");l(s,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:i,on:o});o.addEventListener(u.trigger,i)})}let yt=false;let xt=null;function bt(){if(!xt){xt=function(){yt=true};window.addEventListener("scroll",xt);setInterval(function(){if(yt){yt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){wt(e)})}},200)}}function wt(e){if(!s(e,"data-hx-revealed")&&B(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function St(t,n,e){let i=false;se(v,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){Et(t,e,n,function(e,t){const n=ce(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function Et(r,e,t,n){if(e.trigger==="revealed"){bt();mt(r,n,t,e);wt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ce(r),n,e)}else{mt(r,n,t,e)}}function Ct(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function qt(e){const t=g(ce(e.target),"button, input[type='submit']");const n=Nt(e);if(n){n.lastButtonClicked=t}}function Lt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function Nt(e){const t=g(ce(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",qt);e.addEventListener("focusin",qt);e.addEventListener("focusout",Lt)}function It(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function _t(t){if(!j()){return null}t=V(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=D(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=jt();const r=xn(n);Dn(e.title);Ve(n,t,r);Gt(r.tasks);Ut=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Yt(e){zt();e=e||location.pathname+location.search;const t=_t(e);if(t){const n=D(t.content);const r=jt();const o=xn(r);Dn(n.title);Ve(r,n,o);Gt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Ut=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Zt(e)}}}function Wt(e){let t=Se(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Qt(e){let t=Se(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function en(e,t){se(e,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function tn(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function sn(t,n,r,o,i){if(o==null||tn(t,o)){return}else{t.push(o)}if(nn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=F(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=F(o.files)}rn(s,e,n);if(i){ln(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){on(e.name,e.value,n)}else{t.push(e)}if(i){ln(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}rn(t,e,n)})}}function ln(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function un(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){sn(n,o,i,g(e,"form"),l)}sn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const c=s.lastButtonClicked||e;const a=ee(c,"name");rn(a,c.value,o)}const u=Se(e,"hx-include");se(u,function(e){sn(n,r,i,ce(e),l);if(!f(e,"form")){se(d(e).querySelectorAll(it),function(e){sn(n,r,i,e,l)})}});un(r,o);return{errors:i,formData:r,values:An(r)}}function an(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function fn(e){e=Ln(e);let n="";e.forEach(function(e,t){n=an(n,t,e)});return n}function dn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};wn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function gn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function pn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!gn(e)){r.show="top"}if(n){const s=U(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=c;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{w("Unknown modifier in hx-swap: "+l)}}}}return r}function mn(e){return re(e,"hx-encoding")==="multipart/form-data"||f(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function yn(t,n,r){let o=null;Bt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(mn(n)){return un(new FormData,Ln(r))}else{return fn(r)}}}function xn(e){return{tasks:[],elts:[e]}}function bn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function wn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return wn(ce(u(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Sn(e,t){return wn(e,"hx-vars",true,t)}function En(e,t){return wn(e,"hx-vals",false,t)}function Cn(e){return ue(Sn(e),En(e))}function On(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function Rn(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function C(e,t){return t.test(e.getAllResponseHeaders())}function Hn(e,t,n){e=e.toLowerCase();if(n){if(n instanceof Element||typeof n==="string"){return he(e,t,null,null,{targetOverride:y(n),returnPromise:true})}else{return he(e,t,y(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:y(n.target),swapOverride:n.swap,select:n.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Tn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function qn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ue({url:o,sameHost:r},n))}function Ln(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Nn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Nn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Mn;const X=i.select||null;if(!le(r)){oe(s);return e}const u=i.targetOverride||ce(Ce(r));if(u==null||u==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let c=ie(r);const a=c.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:u,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Ee(r,"hx-sync")}else{d=ce(ae(r,I))}h=(A[1]||"drop").trim();c=ie(d);if(h==="drop"&&c.xhr&&c.abortable!==true){oe(s);return e}else if(h==="abort"){if(c.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const Z=h.split(" ");g=(Z[1]||"last").trim()}}if(c.xhr){if(c.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(c.queuedRequests==null){c.queuedRequests=[]}if(g==="first"&&c.queuedRequests.length===0){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){c.queuedRequests=[];c.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;c.xhr=p;c.abortable=F;const m=function(){c.xhr=null;c.abortable=false;if(c.queuedRequests!=null&&c.queuedRequests.length>0){const e=c.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!de(r,"htmx:prompt",{prompt:y,target:u})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let x=dn(r,u,y);if(t!=="get"&&!mn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=ue(x,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){un(j,Ln(i.values))}const V=Ln(Cn(r));const w=un(j,V);let v=hn(w,r);if(Q.config.getCacheBusterParam&&t==="get"){v.set("org.htmx.cache-buster",ee(u,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=wn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:v,parameters:An(v),unfilteredFormData:w,unfilteredParameters:An(w),headers:x,target:u,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;x=C.headers;v=Ln(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const $=n.split("#");const z=$[0];const O=$[1];let R=n;if(E){R=z;const Y=!v.keys().next().done;if(Y){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=fn(v);if(O){R+="#"+O}}}if(!qn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in x){if(x.hasOwnProperty(k)){const W=x[k];On(p,k,W)}}}const H={xhr:p,target:u,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Tn(r);H.pathInfo.responsePath=Rn(p);M(r,H);if(H.keepIndicators!==true){en(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ue({error:e},H));throw e}};p.onerror=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Wt(r);var q=Qt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:yn(p,r,v);p.send(J);return e}function In(e,t){const n=t.xhr;let r=null;let o=null;if(C(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(C(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(C(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const u=re(e,"hx-replace-url");const c=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(u){a="replace";f=u}else if(c){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function Pn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function kn(e){for(var t=0;t0){E().setTimeout(e,y.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ue({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Xn={};function Fn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Bn(e,t){if(t.init){t.init(n)}Xn[e]=ue(Fn(),t)}function Un(e){delete Xn[e]}function jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Xn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return jn(ce(u(e)),n,r)}var Vn=false;ne().addEventListener("DOMContentLoaded",function(){Vn=true});function _n(e){if(Vn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function $n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function Jn(){const e=zn();if(e){Q.config=ue(Q.config,e)}}_n(function(){Jn();$n();let e=ne().body;Dt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Yt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file From f3eb1fbeb31228267b194f54afa6722af856697e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 22 Sep 2024 15:43:02 +1200 Subject: [PATCH 113/346] Fix not matching all errors in catch block --- src/d2m/actions/expression.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/actions/expression.js b/src/d2m/actions/expression.js index b81f4ed..fd75aa5 100644 --- a/src/d2m/actions/expression.js +++ b/src/d2m/actions/expression.js @@ -30,7 +30,7 @@ async function emojisToState(emojis) { } db.prepare("INSERT OR IGNORE INTO emoji (emoji_id, name, animated, mxc_url) VALUES (?, ?, ?, ?)").run(emoji.id, emoji.name, +!!emoji.animated, url) }).catch(e => { - if (e.data.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit. + if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit. return } console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`) From 06f502dd89fe019c920eeddd5f29f94c3c53bc9b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 22 Sep 2024 15:53:58 +1200 Subject: [PATCH 114/346] Store Discord media proxy hashes in the database --- src/discord/utils.js | 10 ++++++++++ src/web/routes/download-discord.js | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/discord/utils.js b/src/discord/utils.js index 8cb241b..dc96ff8 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -5,6 +5,13 @@ const assert = require("assert").strict const {reg} = require("../matrix/read-registration") +const {db} = require("../passthrough") + +/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore +let hasher = null +// @ts-ignore +require("xxhash-wasm")().then(h => hasher = h) + const EPOCH = 1420070400000 /** @@ -123,6 +130,9 @@ function timestampToSnowflakeInexact(timestamp) { function getPublicUrlForCdn(url) { const match = url.match(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/) if (!match) return url + const unsignedHash = hasher.h64(match[3]) // attachment ID + const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range + db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) return `${reg.ooye.bridge_origin}/download/discord${match[1]}/${match[2]}/${match[3]}/${match[4]}` } diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index 047dd94..ee64074 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -4,6 +4,11 @@ const assert = require("assert/strict") const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError} = require("h3") const {z} = require("zod") +/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore +let hasher = null +// @ts-ignore +require("xxhash-wasm")().then(h => hasher = h) + const {discord, as, select} = require("../../passthrough") const schema = { @@ -31,7 +36,10 @@ function defineMediaProxyHandler(domain) { return defineEventHandler(async event => { const params = await getValidatedRouterParams(event, schema.params.parse) - const row = select("channel_room", "channel_id", {channel_id: params.channel_id}).get() + const unsignedHash = hasher.h64(params.attachment_id) + const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range + + const row = select("media_proxy", "permitted_hash", {permitted_hash: signedHash}).get() if (row == null) { throw createError({ status: 403, From dbbb8281e6304c73ebd1b6be0e7a35bd86794f6b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Sep 2024 16:31:47 +1200 Subject: [PATCH 115/346] Create guild_active table instead of column --- docs/self-service-room-creation-rules.md | 70 +++++++++++++++++++ src/db/migrations/0014-add-guild-active.sql | 11 +++ .../migrations/0014-add-guild-autocreate.sql | 5 -- src/db/orm-defs.d.ts | 4 ++ src/web/pug/guild.pug | 2 +- src/web/routes/guild-settings.js | 2 +- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 docs/self-service-room-creation-rules.md create mode 100644 src/db/migrations/0014-add-guild-active.sql delete mode 100644 src/db/migrations/0014-add-guild-autocreate.sql diff --git a/docs/self-service-room-creation-rules.md b/docs/self-service-room-creation-rules.md new file mode 100644 index 0000000..6292fe7 --- /dev/null +++ b/docs/self-service-room-creation-rules.md @@ -0,0 +1,70 @@ +# Self-service room creation rules + +Before version 3 of Out Of Your Element, new Matrix rooms would be created on-demand when a Discord channel is spoken in for the first time. This has worked pretty well. + +This is done through functions like ensureRoom and ensureSpace in actions: + +```js +async function sendMessage(message, channel, guild, row) { + const roomID = await createRoom.ensureRoom(message.channel_id) + ... +} + +/** + * Ensures the room exists. If it doesn't, creates the room with an accurate initial state. + * @param {string} channelID + * @returns {Promise} Matrix room ID + */ +function ensureRoom(channelID) { + return _syncRoom(channelID, /* shouldActuallySync */ false) /* calls ensureSpace */ +} + +/** + * Ensures the space exists. If it doesn't, creates the space with an accurate initial state. + * @param {DiscordTypes.APIGuild} guild + * @returns {Promise} Matrix space ID + */ +function ensureSpace(guild) { + return _syncSpace(guild, /* shouldActuallySync */ false) +} +``` + +With the introduction of self-service mode, we still want to retain this as a possible mode of operation, since some people prefer to have OOYE handle this administrative work. However, other people prefer to manage the links between channels and rooms themselves, and have control over what new rooms get linked up to. + +Visibly, this is managed through the web interface. The web interface lets moderators enable/disable auto-creation of new rooms, as well as set which channels and rooms are linked together. + +There is a small complication. Not only are Matrix rooms created automatically, their Matrix spaces are also created automatically during room sync: ensureRoom calls ensureSpace. If a user opts in to self-service mode by clicking the specific button in the web portal, we must ensure the _space is not created automatically either,_ because the Matrix user will provide a space to link to. + +To solve this, we need a way to suppress specific guilds from having auto-created spaces. The natural way to represent this is a column on guild_space, but that doesn't work, because each guild_space row requires a guild and space to be linked, and we _don't want_ them to be linked. + +So, internally, OOYE keeps track of this through a new table: + +```sql +CREATE TABLE "guild_active" ( + "guild_id" TEXT NOT NULL, -- only guilds that are bridged are present in this table + "autocreate" INTEGER NOT NULL, -- 0 or 1 + PRIMARY KEY("guild_id") +) WITHOUT ROWID; +``` + +There is one more complication. When adding a Discord bot through web oauth with a redirect_uri, Discord adds the bot to the server normally, _then_ redirects back to OOYE, and only then does OOYE know which guild the bot was just added to. So, for a short time between the bot being added and the user being redirected, OOYE might receive Discord events in the server before it has the chance to create the guild_active database row. + +So to prevent this, self-service behaviour needs to be an implicit default, and users must firmly choose one system or another to begin using OOYE. It is important for me to design this in a way that doesn't force users to do any extra work or make a choice they don't understand to keep the pre-v3 behaviour. + +So there will be 3 states of whether a guild is self-service or not. At first, it could be absent from the table, in which case events for it will be dropped. Or it could be in the table with autocomplete = 0, in which case only rooms that already exist in channel_room will have messages bridged. Or it could have autocomplete = 1, in which case Matrix rooms will be created as needed, as per the pre-v3 behaviour. + +| Auto-create | Meaning | +| -- | ------------ | +| 😶‍🌫️ | Unbridged - waiting | +| ❌ | Bridged - self-service | +| ✅ | Bridged - auto-create | + +Pressing buttons on web or using the /invite command on a guild will insert a row into guild_active, allowing it to be bridged. + +So here's all the technical changes needed to support self-service in v3: + +- New guild_active table showing whether, and how, a guild is bridged +- When /invite command is used, INSERT OR IGNORE INTO state 1 and ensureRoom + ensureSpace. +- When bot is added through "easy mode" web button, REPLACE INTO state 1 and ensureSpace. +- When bot is added through "self-service" web button, REPLACE INTO state 2. +- Event dispatcher will only ensureRoom if the guild_active state is 1 diff --git a/src/db/migrations/0014-add-guild-active.sql b/src/db/migrations/0014-add-guild-active.sql new file mode 100644 index 0000000..01e2f16 --- /dev/null +++ b/src/db/migrations/0014-add-guild-active.sql @@ -0,0 +1,11 @@ +BEGIN TRANSACTION; + +CREATE TABLE "guild_active" ( + "guild_id" TEXT NOT NULL, + "autocreate" INTEGER NOT NULL, + PRIMARY KEY("guild_id") +) WITHOUT ROWID; + +INSERT INTO guild_active (guild_id, autocreate) SELECT guild_id, 1 FROM guild_space; + +COMMIT; diff --git a/src/db/migrations/0014-add-guild-autocreate.sql b/src/db/migrations/0014-add-guild-autocreate.sql deleted file mode 100644 index a6ad212..0000000 --- a/src/db/migrations/0014-add-guild-autocreate.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN TRANSACTION; - -ALTER TABLE guild_space ADD COLUMN autocreate INTEGER NOT NULL DEFAULT 1; - -COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index a9c2c85..02003af 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -31,6 +31,10 @@ export type Models = { guild_id: string space_id: string privacy_level: number + } + + guild_active: { + guild_id: string autocreate: number } diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 9cc0772..b5449a4 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -137,7 +137,7 @@ block body label.s-label.fl-grow1(for="autocreate") | Create new Matrix rooms automatically p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when a new Discord channel is spoken in. - - let value = select("guild_space", "autocreate", {guild_id}).pluck().get() + - let value = select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) .is-loading#autocreate-loading diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index d67a5f2..7940853 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -18,6 +18,6 @@ as.router.post("/api/autocreate", defineEventHandler(async event => { const session = await useSession(event, {password: reg.as_token}) if (!(session.data.managedGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) - db.prepare("UPDATE guild_space SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id) + db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id) return sendRedirect(event, `/guild?guild_id=${parsedBody.guild_id}`, 302) })) From af68657ec42a040a98ad01e19135f6b9b58a3024 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Sep 2024 17:21:35 +1200 Subject: [PATCH 116/346] Make ensureRoom/syncRoom check if autocreatable --- docs/self-service-room-creation-rules.md | 6 +++--- src/d2m/actions/create-room.js | 26 +++++++++++++++++++++--- src/db/orm-defs.d.ts | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/self-service-room-creation-rules.md b/docs/self-service-room-creation-rules.md index 6292fe7..1638f09 100644 --- a/docs/self-service-room-creation-rules.md +++ b/docs/self-service-room-creation-rules.md @@ -63,8 +63,8 @@ Pressing buttons on web or using the /invite command on a guild will insert a ro So here's all the technical changes needed to support self-service in v3: -- New guild_active table showing whether, and how, a guild is bridged +- New guild_active table showing whether, and how, a guild is bridged. - When /invite command is used, INSERT OR IGNORE INTO state 1 and ensureRoom + ensureSpace. - When bot is added through "easy mode" web button, REPLACE INTO state 1 and ensureSpace. -- When bot is added through "self-service" web button, REPLACE INTO state 2. -- Event dispatcher will only ensureRoom if the guild_active state is 1 +- When bot is added through "self-service" web button, REPLACE INTO state 0. +- Event dispatcher will only ensureRoom if the guild_active state is 1. diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 509d0ff..9be489b 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -279,6 +279,17 @@ function channelToGuild(channel) { return guild } +/** + * @param {string} channelID + * @param {string} guildID + */ +function existsOrAutocreatable(channelID, guildID) { + const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channelID}).get() + if (existing) return existing + const autocreate = select("guild_active", "autocreate", {guild_id: guildID}).pluck().get() + return autocreate +} + /* Ensure flow: 1. Get IDs @@ -297,6 +308,7 @@ function channelToGuild(channel) { */ /** + * Create room and/or sync room data. Please check that a channel_room entry exists or autocreate = 1 before calling this. * @param {string} channelID * @param {boolean} shouldActuallySync false if just need to ensure room exists (which is a quick database check), true if also want to sync room data when it does exist (slow) * @returns {Promise} room ID @@ -311,9 +323,17 @@ async function _syncRoom(channelID, shouldActuallySync) { await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it } - const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channelID}).get() + const existing = existsOrAutocreatable(channelID, guild.id) + + if (existing === 0) { + throw new Error("refusing to create a new matrix room when autocreate is deactivated") + } if (!existing) { + throw new Error("refusing to craete a new matrix room when there is no guild_active entry") + } + + if (existing === 1) { const creation = (async () => { const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild, {api}) const roomID = await createRoom(channel, guild, spaceID, channelKState, privacyLevel) @@ -353,12 +373,12 @@ async function _syncRoom(channelID, shouldActuallySync) { return roomID } -/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. */ +/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */ function ensureRoom(channelID) { return _syncRoom(channelID, false) } -/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. */ +/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */ function syncRoom(channelID) { return _syncRoom(channelID, true) } diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index 02003af..b1e6b79 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -35,7 +35,7 @@ export type Models = { guild_active: { guild_id: string - autocreate: number + autocreate: 0 | 1 } lottie: { From 8915e8d96c9fb401d1972996cec318f9f68187d0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Sep 2024 17:21:55 +1200 Subject: [PATCH 117/346] Make invite INSERT OR IGNORE autocreate entry --- src/discord/interactions/invite.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index 689ea1a..ce80e99 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -4,6 +4,10 @@ const DiscordTypes = require("discord-api-types/v10") const assert = require("assert/strict") const {discord, sync, db, select, from} = require("../../passthrough") +/** @type {import("../../d2m/actions/create-room")} */ +const createRoom = sync.require("../../d2m/actions/create-room") +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") @@ -12,17 +16,6 @@ const api = sync.require("../../matrix/api") * @returns {Promise} */ async function _interact({data, channel, guild_id}) { - // Check guild is bridged - const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() - const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() - if (!spaceID || !roomID) return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "This server isn't bridged to Matrix, so you can't invite Matrix users.", - flags: DiscordTypes.MessageFlags.Ephemeral - } - } - // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options @@ -36,6 +29,13 @@ async function _interact({data, channel, guild_id}) { } } + // Ensure guild and room are bridged + db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id) + const roomID = await createRoom.ensureRoom(channel.id) + assert(roomID) + const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() + assert(spaceID) + // Check for existing invite to the space let spaceMember try { From 3af31385f06e10ebd2bfbc1e050a53702739b560 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Sep 2024 01:08:29 +1200 Subject: [PATCH 118/346] Use $url resolver in channelToKState --- src/d2m/actions/create-room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 9be489b..2ea9c91 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -180,7 +180,7 @@ async function channelToKState(channel, guild, di) { network: { id: guild.id, displayname: guild.name, - avatar_url: await file.uploadDiscordFileToMxc(file.guildIcon(guild)) + avatar_url: {$url: file.guildIcon(guild)} }, channel: { id: channel.id, From 312ea69d7381bcd16c112ae12432dfb0a8787cf0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Sep 2024 01:54:01 +1200 Subject: [PATCH 119/346] Fix page duplicating when clicking toggle switch --- src/web/pug/guild.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index b5449a4..2130aff 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -139,7 +139,7 @@ block body p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when a new Discord channel is spoken in. - let value = select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) - input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) + input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" hx-swap="none" checked=value) .is-loading#autocreate-loading h3.mt32.fs-category Manually link channels From b0a0e62a8626782a38dbc29cde3b130f9264a7a2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Sep 2024 01:58:26 +1200 Subject: [PATCH 120/346] Adapt createRoom/space/invite to self-service --- docs/self-service-room-creation-rules.md | 4 + package-lock.json | 12 +-- src/d2m/actions/create-room.js | 99 ++++++++++++++++-------- src/d2m/actions/create-space.js | 3 + src/discord/interactions/invite.js | 22 ++++-- test/data.js | 2 +- 6 files changed, 96 insertions(+), 46 deletions(-) diff --git a/docs/self-service-room-creation-rules.md b/docs/self-service-room-creation-rules.md index 1638f09..c47ca14 100644 --- a/docs/self-service-room-creation-rules.md +++ b/docs/self-service-room-creation-rules.md @@ -61,6 +61,8 @@ So there will be 3 states of whether a guild is self-service or not. At first, i Pressing buttons on web or using the /invite command on a guild will insert a row into guild_active, allowing it to be bridged. +One more thing. Before v3, when a Matrix room was autocreated it would autocreate the space as well, if it needed to. But now, since nothing will be created until the user takes an action, the guild will always be created directly in response to a request. So room creation can now trust that the guild exists already. + So here's all the technical changes needed to support self-service in v3: - New guild_active table showing whether, and how, a guild is bridged. @@ -68,3 +70,5 @@ So here's all the technical changes needed to support self-service in v3: - When bot is added through "easy mode" web button, REPLACE INTO state 1 and ensureSpace. - When bot is added through "self-service" web button, REPLACE INTO state 0. - Event dispatcher will only ensureRoom if the guild_active state is 1. +- createRoom can trust that the space exists because we check that in a calling function. +- createRoom will only create other dependencies if the guild is autocreate. diff --git a/package-lock.json b/package-lock.json index 178690a..45de4cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1217,9 +1217,9 @@ ] }, "node_modules/better-sqlite3": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz", - "integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.3.0.tgz", + "integrity": "sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1633,9 +1633,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.98", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.98.tgz", - "integrity": "sha512-xsH4UwmnCQl4KjAf01/p9ck9s+/vDqzHbUxPOBzo8fcVUa/hQG6qInD7Cr44KAuCM+xCxGJFSAUx450pBrX0+g==", + "version": "0.37.101", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.101.tgz", + "integrity": "sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==", "license": "MIT" }, "node_modules/doctypes": { diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 2ea9c91..75696f2 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -15,8 +15,6 @@ const api = sync.require("../../matrix/api") const ks = sync.require("../../matrix/kstate") /** @type {import("../../discord/utils")} */ const utils = sync.require("../../discord/utils") -/** @type {import("./create-space")}) */ -const createSpace = sync.require("./create-space") // watch out for the require loop /** * There are 3 levels of room privacy: @@ -95,27 +93,21 @@ function convertNameAndTopic(channel, guild, customName) { async function channelToKState(channel, guild, di) { // @ts-ignore const parentChannel = discord.channels.get(channel.parent_id) - /** Used for membership/permission checks. */ - let guildSpaceID - /** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */ - let parentSpaceID - let privacyLevel - if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) { // it's a forum channel's thread, so use a different space to group those threads - guildSpaceID = await createSpace.ensureSpace(guild) - parentSpaceID = await ensureRoom(channel.parent_id) - privacyLevel = select("guild_space", "privacy_level", {space_id: guildSpaceID}).pluck().get() - } else { // otherwise use the guild's space like usual - parentSpaceID = await createSpace.ensureSpace(guild) - guildSpaceID = parentSpaceID - privacyLevel = select("guild_space", "privacy_level", {space_id: parentSpaceID}).pluck().get() - } - assert(typeof parentSpaceID === "string") - assert(typeof guildSpaceID === "string") - assert(typeof privacyLevel === "number") + const guildRow = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get() + assert(guildRow) - const row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get() - const customName = row?.nick - const customAvatar = row?.custom_avatar + /** Used for membership/permission checks. */ + let guildSpaceID = guildRow.space_id + /** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */ + let parentSpaceID = guildSpaceID + if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) { + parentSpaceID = await ensureRoom(channel.parent_id) + assert(typeof parentSpaceID === "string") + } + + const channelRow = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get() + const customName = channelRow?.nick + const customAvatar = channelRow?.custom_avatar const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName) const avatarEventContent = {} @@ -125,6 +117,7 @@ async function channelToKState(channel, guild, di) { avatarEventContent.url = {$url: file.guildIcon(guild)} } + const privacyLevel = guildRow.privacy_level let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] if (channel["thread_metadata"]) history_visibility = "world_readable" @@ -280,16 +273,60 @@ function channelToGuild(channel) { } /** - * @param {string} channelID + * This function handles whether it's allowed to bridge messages in this channel, and if so, where to. + * This has to account for whether self-service is enabled for the guild or not. + * This also has to account for different channel types, like forum channels (which need the + * parent forum to already exist, and ignore the self-service setting), or thread channels (which + * need the parent channel to already exist, and ignore the self-service setting). + * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread * @param {string} guildID + * @returns obj if bridged; 1 if autocreatable; null/undefined if guild is not bridged; 0 if self-service and not autocreatable thread */ -function existsOrAutocreatable(channelID, guildID) { - const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channelID}).get() +function existsOrAutocreatable(channel, guildID) { + // 1. If the channel is already linked somewhere, it's always okay to bridge to that destination, no matter what. Yippee! + const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channel.id}).get() if (existing) return existing + + // 2. If the guild is an autocreate guild, it's always okay to bridge to that destination, and + // we'll need to create any dependent resources recursively. const autocreate = select("guild_active", "autocreate", {guild_id: guildID}).pluck().get() + if (autocreate === 1) return autocreate + + // 3. If the guild is not approved for bridging yet, we can't bridge there. + // They need to decide one way or another whether it's self-service before we can continue. + if (autocreate == null) return autocreate + + // 4. If we got here, the guild is in self-service mode. + // New channels won't be able to create new rooms. But forum threads or channel threads could be fine. + if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) { + // In self-service mode, threads rely on the parent resource already existing. + /** @type {DiscordTypes.APIGuildTextChannel} */ // @ts-ignore + const parent = discord.channels.get(channel.parent_id) + assert(parent) + const parentExisting = existsOrAutocreatable(parent, guildID) + if (parentExisting) return 1 // Autocreatable + } + + // 5. If we got here, the guild is in self-service mode and the channel is truly not bridged. return autocreate } +/** + * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread + * @param {string} guildID + * @returns obj if bridged; 1 if autocreatable. (throws if not autocreatable) + */ +function assertExistsOrAutocreatable(channel, guildID) { + const existing = existsOrAutocreatable(channel, guildID) + if (existing === 0) { + throw new Error(`Guild ${guildID} is self-service, so won't create a Matrix room for channel ${channel.id}`) + } + if (!existing) { + throw new Error(`Guild ${guildID} is not bridged, so won't create a Matrix room for channel ${channel.id}`) + } + return existing +} + /* Ensure flow: 1. Get IDs @@ -323,15 +360,7 @@ async function _syncRoom(channelID, shouldActuallySync) { await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it } - const existing = existsOrAutocreatable(channelID, guild.id) - - if (existing === 0) { - throw new Error("refusing to create a new matrix room when autocreate is deactivated") - } - - if (!existing) { - throw new Error("refusing to craete a new matrix room when there is no guild_active entry") - } + const existing = assertExistsOrAutocreatable(channel, guild.id) if (existing === 1) { const creation = (async () => { @@ -475,3 +504,5 @@ module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel +module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable +module.exports.existsOrAutocreatable = existsOrAutocreatable diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index ec1677e..0c3d5e3 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -92,6 +92,9 @@ async function _syncSpace(guild, shouldActuallySync) { const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get() if (!row) { + const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get() + assert.equal(autocreate, 1, `refusing to implicitly create guild ${guild.id}. set the guild_active data first before calling ensureSpace/syncSpace.`) + const creation = (async () => { const guildKState = await guildToKState(guild, createRoom.DEFAULT_PRIVACY_LEVEL) // New spaces will have to use the default privacy level; we obviously can't look up the existing entry const spaceID = await createSpace(guild, guildKState) diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index ce80e99..3b39d9d 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -1,6 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") +const Ty = require("../../types") const assert = require("assert/strict") const {discord, sync, db, select, from} = require("../../passthrough") @@ -12,7 +13,7 @@ const createSpace = sync.require("../../d2m/actions/create-space") const api = sync.require("../../matrix/api") /** - * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction + * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction * @returns {Promise} */ async function _interact({data, channel, guild_id}) { @@ -29,12 +30,23 @@ async function _interact({data, channel, guild_id}) { } } + const guild = discord.guilds.get(guild_id) + assert(guild) + // Ensure guild and room are bridged db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id) + const existing = createRoom.existsOrAutocreatable(channel, guild_id) + if (existing === 0) return { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + } + assert(existing) // can't be null or undefined as we just inserted the guild_active row + + const spaceID = await createSpace.ensureSpace(guild) const roomID = await createRoom.ensureRoom(channel.id) - assert(roomID) - const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() - assert(spaceID) // Check for existing invite to the space let spaceMember @@ -115,7 +127,7 @@ async function _interactButton({channel, message}) { } } -/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */ async function interact(interaction) { await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) } diff --git a/test/data.js b/test/data.js index c8217c2..eb2c42a 100644 --- a/test/data.js +++ b/test/data.js @@ -58,7 +58,7 @@ module.exports = { network: { id: "112760669178241024", displayname: "Psychonauts 3", - avatar_url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + avatar_url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"} }, channel: { id: "112760669178241024", From 734c9a583875caa34b459bb1adbf3a860ae993d3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Sep 2024 13:37:54 +1200 Subject: [PATCH 121/346] Remove hardcoded cadence.moe --- scripts/seed.js | 5 +++-- src/matrix/read-registration.js | 11 +++++++---- src/web/pug/guild.pug | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/seed.js b/scripts/seed.js index f20afb7..3603595 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -105,7 +105,8 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { const serverNameResponse = await prompt({ type: "input", name: "server_name", - message: "Homeserver name" + message: "Homeserver name", + validate: serverName => !!serverName.match(/[a-z][a-z.]+[a-z]/) }) console.log("What is the URL of your homeserver?") @@ -176,7 +177,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { message: "Client secret" }) - const template = getTemplateRegistration() + const template = getTemplateRegistration(serverNameResponse.server_name) reg = { ...template, url: bridgeOriginResponse.bridge_origin, diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 8438387..fec434f 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -24,8 +24,11 @@ function writeRegistration(reg) { fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2)) } -/** @returns {import("../types").InitialAppServiceRegistrationConfig} reg */ -function getTemplateRegistration() { +/** + * @param {string} serverName + * @returns {import("../types").InitialAppServiceRegistrationConfig} reg + */ +function getTemplateRegistration(serverName) { return { id: "ooye", as_token: crypto.randomBytes(32).toString("hex"), @@ -33,11 +36,11 @@ function getTemplateRegistration() { namespaces: { users: [{ exclusive: true, - regex: "@_ooye_.*:cadence.moe" + regex: `@_ooye_.*:${serverName}` }], aliases: [{ exclusive: true, - regex: "#_ooye_.*:cadence.moe" + regex: `#_ooye_.*:${serverName}` }] }, protocols: [ diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index b5449a4..f92bf75 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -136,7 +136,7 @@ block body form.d-flex.ai-center.g8 label.s-label.fl-grow1(for="autocreate") | Create new Matrix rooms automatically - p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when a new Discord channel is spoken in. + p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in. - let value = select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) From d6de57f0c36bd7c01255f642bd099e7e56c2881c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 26 Sep 2024 02:16:50 +1200 Subject: [PATCH 122/346] Factor out namespace_prefix --- src/matrix/read-registration.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index fec434f..9fb0535 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -29,6 +29,7 @@ function writeRegistration(reg) { * @returns {import("../types").InitialAppServiceRegistrationConfig} reg */ function getTemplateRegistration(serverName) { + const namespace_prefix = "_ooye_" return { id: "ooye", as_token: crypto.randomBytes(32).toString("hex"), @@ -36,21 +37,21 @@ function getTemplateRegistration(serverName) { namespaces: { users: [{ exclusive: true, - regex: `@_ooye_.*:${serverName}` + regex: `@${namespace_prefix}.*:${serverName}` }], aliases: [{ exclusive: true, - regex: `#_ooye_.*:${serverName}` + regex: `#${namespace_prefix}.*:${serverName}` }] }, protocols: [ "discord" ], - sender_localpart: "_ooye_bot", + sender_localpart: `${namespace_prefix}bot`, rate_limited: false, socket: 6693, ooye: { - namespace_prefix: "_ooye_", + namespace_prefix, max_file_size: 5000000, content_length_workaround: false, include_user_id_in_mxid: false, From 8743910c35954212fc9cf1ac1640b97ab4894718 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 26 Sep 2024 09:47:21 +1200 Subject: [PATCH 123/346] Rename seed.js to setup; add npm script for it --- package.json | 1 + readme.md | 8 ++++---- scripts/{seed.js => setup.js} | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename scripts/{seed.js => setup.js} (99%) mode change 100755 => 100644 diff --git a/package.json b/package.json index ba02a83..6b4484d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "scripts": { "start": "node start.js", + "setup": "node scripts/setup.js", "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", diff --git a/readme.md b/readme.md index 992d0de..daebb1e 100644 --- a/readme.md +++ b/readme.md @@ -81,9 +81,9 @@ Follow these steps: 1. Install dependencies: `npm install` -1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. +1. Run `npm run setup` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. -1. Start the bridge: `npm start` +1. Start the bridge: `npm run start` 1. Add the bot to a server - use any *one* of the following commands for an invite link: * (in the REPL) `addbot` @@ -152,8 +152,8 @@ To get into the rooms on your Matrix account, use the `/invite [your mxid here]` │   └── *.js * Various files you can run once if you need them. └── scripts - * First time running a new bridge? Run this file to plant a seed, which will flourish into state for the bridge: - ├── seed.js + * First time running a new bridge? Run this file to set up prerequisites on the Matrix server: + ├── setup.js * Hopefully you won't need the rest of these. Code quality varies wildly. └── *.js diff --git a/scripts/seed.js b/scripts/setup.js old mode 100755 new mode 100644 similarity index 99% rename from scripts/seed.js rename to scripts/setup.js index f20afb7..2fb2dd0 --- a/scripts/seed.js +++ b/scripts/setup.js @@ -305,7 +305,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } // Otherwise, it's the user's problem if (!guild) { - return die(`Error: The bot needs to upload some emojis. Please say where to upload them to. Run seed.js again with --emoji-guild=GUILD_ID`) + return die(`Error: The bot needs to upload some emojis. Please say where to upload them to. Run setup again with --emoji-guild=GUILD_ID`) } // Upload those emojis to the chosen location db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id) From 69c93ca9d9ae4272f6716f71d1532bb04300be51 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 26 Sep 2024 09:59:13 +1200 Subject: [PATCH 124/346] Automatically set up content_length_workaround --- src/matrix/mreq.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js index 4707ae6..cdcf405 100644 --- a/src/matrix/mreq.js +++ b/src/matrix/mreq.js @@ -5,7 +5,7 @@ const mixin = require("@cloudrac3r/mixin-deep") const stream = require("stream") const getStream = require("get-stream") -const {reg} = require("./read-registration.js") +const {reg, writeRegistration} = require("./read-registration.js") const baseUrl = `${reg.ooye.server_origin}/_matrix` @@ -45,13 +45,15 @@ async function mreq(method, url, body, extra = {}) { const root = await res.json() if (!res.ok || root.errcode) { - if (root.error?.includes("Content-Length")) { - console.error(`OOYE cannot stream uploads to Synapse. Please choose one of these workarounds:` - + `\n * Run an nginx reverse proxy to Synapse, and point registration.yaml's` - + `\n \`server_origin\` to nginx` - + `\n * Set \`content_length_workaround: true\` in registration.yaml (this will` - + `\n halve the speed of bridging d->m files)`) - throw new Error("Synapse is not accepting stream uploads, see the message above.") + if (root.error?.includes("Content-Length") || !reg.ooye.content_length_workaround) { + reg.ooye.content_length_workaround = true + const root = await mreq(method, url, body, extra) + console.error("OOYE cannot stream uploads to Synapse. The `content_length_workaround` option" + + "\nhas been activated in registration.yaml, which works around the problem, but" + + "\nhalves the speed of bridging d->m files. A better way to resolve this problem" + + "\nis to run an nginx reverse proxy to Synapse and re-run OOYE setup.") + writeRegistration(reg) + return root } delete opts.headers.Authorization throw new MatrixServerError(root, {baseUrl, url, ...opts}) From d629e666db1e01453041cd4668cfc6e71ee54d81 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Sep 2024 00:21:48 +1200 Subject: [PATCH 125/346] Fis messages being double-redacted --- src/m2d/actions/redact.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/actions/redact.js b/src/m2d/actions/redact.js index ffbb261..5a12d5a 100644 --- a/src/m2d/actions/redact.js +++ b/src/m2d/actions/redact.js @@ -13,7 +13,7 @@ const utils = sync.require("../converters/utils") */ async function deleteMessage(event) { const rows = from("event_message").join("message_channel", "message_id").select("channel_id", "message_id").where({event_id: event.redacts}).all() - db.prepare("DELETE FROM event_message WHERE event_id = ?").run(event.event_id) + db.prepare("DELETE FROM event_message WHERE event_id = ?").run(event.redacts) for (const row of rows) { db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(row.message_id) await discord.snow.channel.deleteMessage(row.channel_id, row.message_id, event.content.reason) From bac2deb32f88344c24861fb46ff407867b91d9e1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Sep 2024 03:11:59 +1300 Subject: [PATCH 126/346] Check existsOrAutocreatable before dispatching --- src/d2m/actions/create-room.js | 2 +- src/d2m/event-dispatcher.js | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 75696f2..2d8d1af 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -504,5 +504,5 @@ module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel -module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable module.exports.existsOrAutocreatable = existsOrAutocreatable +module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 1806ee6..af8eedf 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -191,7 +191,7 @@ module.exports = { async onThreadCreate(client, thread) { const channelID = thread.parent_id || undefined const parentRoomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() - if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel + if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel (won't autocreate) const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread) await announceThread.announceThread(parentRoomID, threadRoomID, thread) }, @@ -249,6 +249,7 @@ module.exports = { if (message.author.username === "Deleted User") return // Nothing we can do for deleted users. const channel = client.channels.get(message.channel_id) if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. + const guild = client.guilds.get(channel.guild_id) assert(guild) @@ -259,11 +260,13 @@ module.exports = { if (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only! + if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable + const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id) if (affected) return // @ts-ignore - await sendMessage.sendMessage(message, channel, guild, row), + await sendMessage.sendMessage(message, channel, guild, row) retrigger.messageFinishedBridging(message.id) }, @@ -278,7 +281,7 @@ module.exports = { // Otherwise, if there are embeds, then the system generated URL preview embeds. if (!(typeof data.content === "string" || "embeds" in data)) return - // Deal with Eventual Consistency(TM) + // Check that the sending-to room exists, and deal with Eventual Consistency(TM) if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return if (data.webhook_id) { @@ -295,11 +298,11 @@ module.exports = { /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore const message = data - const channel = client.channels.get(message.channel_id) if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) assert(guild) + // @ts-ignore await editMessage.editMessage(message, guild, row) }, From 034f8d6b554c9273f43fcbfdaedfbff0813d0d34 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Sep 2024 03:27:40 +1300 Subject: [PATCH 127/346] Code coverage reporting --- package.json | 2 +- src/db/migrations/0002-optimise-profile-content.up.js | 1 + src/db/orm-defs.d.ts | 1 + src/db/orm.js | 2 +- src/db/orm.test.js | 5 +++++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6b4484d..857e2d2 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,6 @@ "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", - "cover": "c8 -o test/coverage --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -x d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" + "cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/matrix/mreq.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" } } diff --git a/src/db/migrations/0002-optimise-profile-content.up.js b/src/db/migrations/0002-optimise-profile-content.up.js index a8619cf..5b540cb 100644 --- a/src/db/migrations/0002-optimise-profile-content.up.js +++ b/src/db/migrations/0002-optimise-profile-content.up.js @@ -3,6 +3,7 @@ module.exports = async function(db) { const contents = db.prepare("SELECT distinct hashed_profile_content FROM sim_member WHERE hashed_profile_content IS NOT NULL").pluck().all() const stmt = db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE hashed_profile_content = ?") db.transaction(() => { + /* c8 ignore next 6 */ for (let s of contents) { let b = Buffer.isBuffer(s) ? Uint8Array.from(s) : Uint8Array.from(Buffer.from(s)) const unsignedHash = hasher.h64Raw(b) diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index b1e6b79..c235e99 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -124,3 +124,4 @@ export type PickTypeOf> = T extends { [k in K]?: any } ? export type Merge = {[x in AllKeys]: PickTypeOf} export type Nullable = {[k in keyof T]: T[k] | null} export type Numberish = {[k in keyof T]: T[k] extends number ? (number | bigint) : T[k]} +export type ValueOrArray = {[k in keyof T]: T[k][] | T[k]} diff --git a/src/db/orm.js b/src/db/orm.js index 646012b..4d9b6f1 100644 --- a/src/db/orm.js +++ b/src/db/orm.js @@ -8,7 +8,7 @@ const U = require("./orm-defs") * @template {keyof U.Models[Table]} Col * @param {Table} table * @param {Col[] | Col} cols - * @param {Partial>} where + * @param {Partial>>} where * @param {string} [e] */ function select(table, cols, where = {}, e = "") { diff --git a/src/db/orm.test.js b/src/db/orm.test.js index a53cc66..278723a 100644 --- a/src/db/orm.test.js +++ b/src/db/orm.test.js @@ -30,6 +30,11 @@ test("orm: select: all, where and pluck works on multiple columns", t => { t.deepEqual(names, ["cadence [they]"]) }) +test("orm: select: in array works", t => { + const ids = select("emoji", "emoji_id", {name: ["online", "upstinky"]}).pluck().all() + t.deepEqual(ids, ["288858540888686602", "606664341298872324"]) +}) + test("orm: from: get pluck works", t => { const guildID = from("guild_space").pluck("guild_id").and("WHERE space_id = ?").get("!jjWAGMeQdNrVZSSfvz:cadence.moe") t.equal(guildID, data.guild.general.id) From cf756cb0af195618d6fb3d5f506b595910aa234d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Sep 2024 03:58:51 +1300 Subject: [PATCH 128/346] Create space as needed in oauth flow I have manually tested this with both web flows, the link flow, the /invite command, and the toggle switch, and they all work. --- src/web/pug/guild.pug | 2 +- src/web/pug/includes/template.pug | 3 ++- src/web/routes/invite.js | 16 +++++++++------- src/web/routes/oauth.js | 11 ++++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 960f30b..1e27f2e 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -137,7 +137,7 @@ block body label.s-label.fl-grow1(for="autocreate") | Create new Matrix rooms automatically p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in. - - let value = select("guild_active", "autocreate", {guild_id}).pluck().get() + - let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" hx-swap="none" checked=value) .is-loading#autocreate-loading diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index c117056..94b5e92 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -13,6 +13,7 @@ doctype html html(lang="en") head title Out Of Your Element + link(rel="stylesheet" type="text/css" href="/static/stacks.min.css") meta(name="htmx-config" content='{"indicatorClass":"is-loading"}') @@ -52,7 +53,7 @@ html(lang="en") li(role="menuitem") a.s-topbar--item.s-user-card.d-flex.p4(href=`/guild?guild_id=${guild.id}`) +guild(guild) - .mx-auto.w100.wmx9.py24#content + .mx-auto.w100.wmx9.py24.px8#content block body script. document.querySelectorAll("[popovertarget]").forEach(e => { diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js index eec7a3c..94fa367 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/invite.js @@ -9,6 +9,8 @@ const {LRUCache} = require("lru-cache") const {discord, as, sync, select} = require("../../passthrough") /** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") const {reg} = require("../../matrix/read-registration") /** @type {import("../../matrix/api")} */ @@ -71,20 +73,20 @@ as.router.post("/api/invite", defineEventHandler(async event => { } // Check guild is bridged - const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get() - if (!spaceID) throw createError({status: 428, message: "Server not bridged", data: "You can only invite Matrix users to servers that are bridged to Matrix."}) + const guild = discord.guilds.get(guild_id) + assert(guild) + const spaceID = await createSpace.ensureSpace(guild) // Check for existing invite to the space let spaceMember try { spaceMember = await api.getStateEvent(spaceID, "m.room.member", parsedBody.mxid) } catch (e) {} - if (spaceMember && (spaceMember.membership === "invite" || spaceMember.membership === "join")) { - return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302) - } - // Invite - await api.inviteToRoom(spaceID, parsedBody.mxid) + if (!spaceMember || spaceMember.membership !== "invite" || spaceMember.membership !== "join") { + // Invite + await api.inviteToRoom(spaceID, parsedBody.mxid) + } // Permissions if (parsedBody.permissions === "moderator") { diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js index f3078ad..2f12dd1 100644 --- a/src/web/routes/oauth.js +++ b/src/web/routes/oauth.js @@ -7,7 +7,7 @@ const {SnowTransfer} = require("snowtransfer") const DiscordTypes = require("discord-api-types/v10") const fetch = require("node-fetch") -const {as} = require("../../passthrough") +const {as, db} = require("../../passthrough") const {id} = require("../../../addbot") const {reg} = require("../../matrix/read-registration") @@ -77,14 +77,19 @@ as.router.get("/oauth", defineEventHandler(async event => { const client = new SnowTransfer(`Bearer ${parsedToken.data.access_token}`) try { const guilds = await client.user.getGuilds() - const managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id) + var managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id) await session.update({managedGuilds}) } catch (e) { throw createError({status: 502, message: "API call failed", data: e.message}) } + // Set auto-create for the guild + // @ts-ignore + if (managedGuilds.includes(parsedQuery.data.guild_id)) { + db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, ?)").run(parsedQuery.data.guild_id, +!session.data.selfService) + } + if (parsedQuery.data.guild_id) { - // TODO: we probably need to create a matrix space and database entry immediately here so that self-service settings apply and so matrix users can be invited return sendRedirect(event, `/guild?guild_id=${parsedQuery.data.guild_id}`, 302) } From 5dbd79cf3920f8ebffb1753158e716bf28193f1d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 29 Sep 2024 04:40:38 +1300 Subject: [PATCH 129/346] Prompt to add redirect URI in setup --- scripts/setup.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 0fae913..a3902be 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -148,10 +148,15 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } } }) + bridgeOriginResponse.bridge_origin = bridgeOriginResponse.bridge_origin.replace(/\/+$/, "") // remove trailing slash await server.close() console.log("What is your Discord bot token?") + /** @type {SnowTransfer} */ // @ts-ignore + let snow = null + /** @type {{id: string, redirect_uris: string[]}} */ // @ts-ignore + let client = null /** @type {{discord_token: string}} */ const discordTokenResponse = await prompt({ type: "input", @@ -160,8 +165,8 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { validate: async token => { process.stdout.write(magenta(" checking, please wait...")) try { - const snow = new SnowTransfer(token) - await snow.user.getSelf() + snow = new SnowTransfer(token) + client = await snow.requestHandler.request(`/applications/@me`, {}, "get") return true } catch (e) { return e.message @@ -170,6 +175,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { }) console.log("What is your Discord client secret?") + console.log(`You can find it on the application page: https://discord.com/developers/applications/${client.id}/oauth2`) /** @type {{discord_client_secret: string}} */ const clientSecretResponse = await prompt({ type: "input", @@ -177,6 +183,25 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { message: "Client secret" }) + const expectedUri = `${bridgeOriginResponse.bridge_origin}/oauth` + if (!client.redirect_uris.includes(expectedUri)) { + console.log(`On the same application page, go to the Redirects section, and add this URI: ${cyan(expectedUri)}`) + await prompt({ + type: "invisible", + name: "redirect_uri", + message: "Press Enter when you've added it", + validate: async token => { + process.stdout.write(magenta("checking, please wait...")) + client = await snow.requestHandler.request(`/applications/@me`, {}, "get") + if (client.redirect_uris.includes(expectedUri)) { + return true + } else { + return "Redirect URI has not been added yet" + } + } + }) + } + const template = getTemplateRegistration(serverNameResponse.server_name) reg = { ...template, From 65170c128254732d23519eb1c169c78c89959b5c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 00:37:10 +1300 Subject: [PATCH 130/346] Registration YAML sample no longer needed --- registration.example.yaml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 registration.example.yaml diff --git a/registration.example.yaml b/registration.example.yaml deleted file mode 100644 index d38f5ae..0000000 --- a/registration.example.yaml +++ /dev/null @@ -1,25 +0,0 @@ -id: de8c56117637cb5d9f4ac216f612dc2adb1de4c09ae8d13553f28c33a28147c7 -hs_token: [a unique 64 character hex string] -as_token: [a unique 64 character hex string] -url: http://localhost:6693 -sender_localpart: _ooye_bot -protocols: - - discord -namespaces: - users: - - exclusive: true - regex: '@_ooye_.*' - aliases: - - exclusive: true - regex: '#_ooye_.*' -rate_limited: false -ooye: - namespace_prefix: _ooye_ - max_file_size: 5000000 - server_name: [the part after the colon in your matrix id, like cadence.moe] - server_origin: [the full protocol and domain of your actual matrix server's location, with no trailing slash, like https://matrix.cadence.moe] - content_length_workaround: false - include_user_id_in_mxid: false - invite: - # uncomment this to auto-invite the named user to newly created spaces and mark them as admin (PL 100) everywhere - # - '@cadence:cadence.moe' From bad8c5b8c249f9060aea44d903f1ca47cbadf8b0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 00:51:55 +1300 Subject: [PATCH 131/346] Test invite interaction & code coverage --- scripts/setup.js | 6 +- src/db/orm.test.js | 10 ++ src/discord/interactions/bridge.js | 115 ------------ src/discord/interactions/invite.js | 14 +- src/discord/interactions/invite.test.js | 228 ++++++++++++++++++++++++ src/discord/register-interactions.js | 17 -- src/discord/utils.js | 2 +- src/discord/utils.test.js | 73 ++++++++ src/matrix/power.test.js | 12 -- src/matrix/read-registration.js | 6 +- src/matrix/read-registration.test.js | 21 ++- src/types.d.ts | 7 +- test/data.js | 49 ++++- test/ooye-test-data.sql | 3 + test/test.js | 5 +- 15 files changed, 407 insertions(+), 161 deletions(-) delete mode 100644 src/discord/interactions/bridge.js create mode 100644 src/discord/interactions/invite.test.js delete mode 100644 src/matrix/power.test.js diff --git a/scripts/setup.js b/scripts/setup.js index a3902be..13c2492 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -7,6 +7,7 @@ const sqlite = require("better-sqlite3") const {scheduler} = require("timers/promises") const {isDeepStrictEqual} = require("util") const {createServer} = require("http") +const {join} = require("path") const {prompt} = require("enquirer") const Input = require("enquirer/lib/prompts/input") @@ -208,7 +209,6 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { url: bridgeOriginResponse.bridge_origin, ooye: { ...template.ooye, - ...serverNameResponse, ...bridgeOriginResponse, server_origin: serverOrigin, ...discordTokenResponse, @@ -335,8 +335,8 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { } // Upload those emojis to the chosen location db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id) - await uploadAutoEmoji(discord.snow, guild, "L1", "docs/img/L1.png") - await uploadAutoEmoji(discord.snow, guild, "L2", "docs/img/L2.png") + await uploadAutoEmoji(discord.snow, guild, "L1", join(__dirname, "../docs/img/L1.png")) + await uploadAutoEmoji(discord.snow, guild, "L2", join(__dirname, "../docs/img/L2.png")) } console.log("✅ Emojis are ready...") diff --git a/src/db/orm.test.js b/src/db/orm.test.js index 278723a..a8f10f4 100644 --- a/src/db/orm.test.js +++ b/src/db/orm.test.js @@ -58,3 +58,13 @@ test("orm: from: join direction works", t => { const hasNoOwnerInner = from("sim").join("sim_proxy", "user_id", "inner").select("user_id", "proxy_owner_id").where({sim_name: "crunch_god"}).get() t.deepEqual(hasNoOwnerInner, undefined) }) + +test("orm: select unsafe works (to select complex column names that can't be type verified)", t => { + const results = from("member_cache") + .join("member_power", "mxid") + .join("channel_room", "room_id") // only include rooms that are bridged + .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") + .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") + .all() + t.equal(results[0].power_level, 100) +}) diff --git a/src/discord/interactions/bridge.js b/src/discord/interactions/bridge.js deleted file mode 100644 index 1fbc57e..0000000 --- a/src/discord/interactions/bridge.js +++ /dev/null @@ -1,115 +0,0 @@ -// @ts-check - -const DiscordTypes = require("discord-api-types/v10") -const Ty = require("../../types") -const {discord, sync, db, select, from, as} = require("../../passthrough") -const assert = require("assert/strict") - -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - -/** @type {Map>} spaceID -> list of rooms */ -const cache = new Map() -/** @type {Map} roomID -> spaceID */ -const reverseCache = new Map() - -// Manage clearing the cache -sync.addTemporaryListener(as, "type:m.room.name", /** @param {Ty.Event.StateOuter} event */ async event => { - if (event.state_key !== "") return - const roomID = event.room_id - const spaceID = reverseCache.get(roomID) - if (!spaceID) return - const childRooms = await cache.get(spaceID) - if (!childRooms) return - if (event.content.name) { - const found = childRooms.find(r => r.value === roomID) - if (!found) return - found.name = event.content.name - } else { - cache.set(spaceID, Promise.resolve(childRooms.filter(r => r.value !== roomID))) - reverseCache.delete(roomID) - } -}) - -// Manage adding to the cache -async function getCachedHierarchy(spaceID) { - return cache.get(spaceID) || (() => { - const entry = (async () => { - const result = await api.getFullHierarchy(spaceID) - /** @type {{name: string, value: string}[]} */ - const childRooms = [] - for (const room of result) { - if (room.name && !room.name.match(/^\[[⛓️🔊]\]/) && room.room_type !== "m.space") { - childRooms.push({name: room.name, value: room.room_id}) - reverseCache.set(room.room_id, spaceID) - } - } - return childRooms - })() - cache.set(spaceID, entry) - return entry - })() -} - -/** @param {DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction */ -async function interactAutocomplete({id, token, data, guild_id}) { - const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() - if (!spaceID) { - return discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult, - data: { - choices: [ - { - name: `Error: This server needs to be bridged somewhere first...`, - value: "baby" - } - ] - } - }) - } - - let rooms = await getCachedHierarchy(spaceID) - // @ts-ignore - rooms = rooms.filter(r => r.name.includes(data.options[0].value)) - - await discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult, - data: { - choices: rooms - } - }) -} - -/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ -async function interactSubmit({id, token, data, guild_id}) { - const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() - if (!spaceID) { - return discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "Error: This server needs to be bridged somewhere first...", - flags: DiscordTypes.MessageFlags.Ephemeral - } - }) - } - - return discord.snow.interaction.createInteractionResponse(id, token, { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "Valid input. This would do something but it isn't implemented yet.", - flags: DiscordTypes.MessageFlags.Ephemeral - } - }) -} - -/** @param {DiscordTypes.APIGuildInteraction} interaction */ -async function interact(interaction) { - if (interaction.type === DiscordTypes.InteractionType.ApplicationCommandAutocomplete) { - return interactAutocomplete(interaction) - } else if (interaction.type === DiscordTypes.InteractionType.ApplicationCommand) { - // @ts-ignore - return interactSubmit(interaction) - } -} - -module.exports.interact = interact diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index 3b39d9d..bacf465 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -14,13 +14,14 @@ const api = sync.require("../../matrix/api") /** * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction + * @param {{api: typeof api}} di * @returns {Promise} */ -async function _interact({data, channel, guild_id}) { +async function _interact({data, channel, guild_id}, {api}) { // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options - const input = options?.[0].value || "" + const input = options?.[0]?.value || "" const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] if (!mxid) return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, @@ -110,9 +111,10 @@ async function _interact({data, channel, guild_id}) { /** * @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction + * @param {{api: typeof api}} di * @returns {Promise} */ -async function _interactButton({channel, message}) { +async function _interactButton({channel, message}, {api}) { const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() @@ -127,14 +129,16 @@ async function _interactButton({channel, message}) { } } +/* c8 ignore start */ + /** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) } /** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ async function interactButton(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction)) + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction, {api})) } module.exports.interact = interact diff --git a/src/discord/interactions/invite.test.js b/src/discord/interactions/invite.test.js new file mode 100644 index 0000000..431ff60 --- /dev/null +++ b/src/discord/interactions/invite.test.js @@ -0,0 +1,228 @@ +const {test} = require("supertape") +const DiscordTypes = require("discord-api-types/v10") +const {db, discord} = require("../../passthrough") +const {MatrixServerError} = require("../../matrix/mreq") +const {_interact, _interactButton} = require("./invite") + +test("invite: checks for missing matrix ID", async t => { + const msg = await _interact({ + data: { + options: [] + }, + channel: discord.channels.get("0"), + guild_id: "112760669178241024" + }, {}) + t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") +}) + +test("invite: checks for invalid matrix ID", async t => { + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence" + }] + }, + channel: discord.channels.get("0"), + guild_id: "112760669178241024" + }, {}) + t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") +}) + +test("invite: checks if channel exists or is autocreatable", async t => { + db.prepare("UPDATE guild_active SET autocreate = 0").run() + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("498323546729086986"), + guild_id: "112760669178241024" + }, {}) + t.equal(msg.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.") + db.prepare("UPDATE guild_active SET autocreate = 1").run() +}) + +test("invite: checks if user is already invited to space", async t => { + let called = 0 + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("112760669178241024"), + guild_id: "112760669178241024" + }, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.member") + t.equal(stateKey, "@cadence:cadence.moe") + return { + displayname: "cadence", + membership: "invite" + } + } + } + }) + t.equal(msg.data.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.") + t.equal(called, 1) +}) + +test("invite: invites if user is not in space", async t => { + let called = 0 + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("112760669178241024"), + guild_id: "112760669178241024" + }, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.member") + t.equal(stateKey, "@cadence:cadence.moe") + throw new MatrixServerError("State event doesn't exist or something") + }, + inviteToRoom: async (roomID, mxid) => { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(mxid, "@cadence:cadence.moe") + } + } + }) + t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the server.") + t.equal(called, 2) +}) + +test("invite: prompts to invite to room (if never joined)", async t => { + let called = 0 + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("112760669178241024"), + guild_id: "112760669178241024" + }, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(type, "m.room.member") + t.equal(stateKey, "@cadence:cadence.moe") + if (roomID === "!jjWAGMeQdNrVZSSfvz:cadence.moe") { // space ID + return { + displayname: "cadence", + membership: "join" + } + } else { + throw new MatrixServerError("State event doesn't exist or something") + } + } + } + }) + t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + t.equal(called, 2) +}) + +test("invite: prompts to invite to room (if left)", async t => { + let called = 0 + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("112760669178241024"), + guild_id: "112760669178241024" + }, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(type, "m.room.member") + t.equal(stateKey, "@cadence:cadence.moe") + if (roomID === "!jjWAGMeQdNrVZSSfvz:cadence.moe") { // space ID + return { + displayname: "cadence", + membership: "join" + } + } else { + return { + displayname: "cadence", + membership: "leave" + } + } + } + } + }) + t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + t.equal(called, 2) +}) + +test("invite button: invites to room when button clicked", async t => { + let called = 0 + const msg = await _interactButton({ + channel: discord.channels.get("112760669178241024"), + message: { + content: "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?" + } + }, { + api: { + inviteToRoom: async (roomID, mxid) => { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(mxid, "@cadence:cadence.moe") + } + } + }) + t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the channel.") + t.equal(called, 1) +}) + +test("invite: no-op if in room and space", async t => { + let called = 0 + const msg = await _interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("112760669178241024"), + guild_id: "112760669178241024" + }, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(type, "m.room.member") + t.equal(stateKey, "@cadence:cadence.moe") + return { + displayname: "cadence", + membership: "join" + } + } + } + }) + t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server and this channel.") + t.equal(called, 2) +}) diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index cd9203f..7b1e52d 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -7,7 +7,6 @@ const {id} = require("../../addbot") const matrixInfo = sync.require("./interactions/matrix-info.js") const invite = sync.require("./interactions/invite.js") const permissions = sync.require("./interactions/permissions.js") -const bridge = sync.require("./interactions/bridge.js") const reactions = sync.require("./interactions/reactions.js") const privacy = sync.require("./interactions/privacy.js") @@ -39,20 +38,6 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "user" } ] -}, { - name: "bridge", - contexts: [DiscordTypes.InteractionContextType.Guild], - type: DiscordTypes.ApplicationCommandType.ChatInput, - description: "Start bridging this channel to a Matrix room", - default_member_permissions: String(DiscordTypes.PermissionFlagsBits.ManageChannels), - options: [ - { - type: DiscordTypes.ApplicationCommandOptionType.String, - description: "Destination room to bridge to", - name: "room", - autocomplete: true - } - ] }, { name: "privacy", contexts: [DiscordTypes.InteractionContextType.Guild], @@ -94,8 +79,6 @@ async function dispatchInteraction(interaction) { await permissions.interact(interaction) } else if (interactionId === "permissions_edit") { await permissions.interactEdit(interaction) - } else if (interactionId === "bridge") { - await bridge.interact(interaction) } else if (interactionId === "Reactions") { await reactions.interact(interaction) } else if (interactionId === "privacy") { diff --git a/src/discord/utils.js b/src/discord/utils.js index dc96ff8..dea05ae 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -113,7 +113,7 @@ function isWebhookMessage(message) { * @param {Pick} message */ function isEphemeralMessage(message) { - return message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral) + return Boolean(message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral)) } /** @param {string} snowflake */ diff --git a/src/discord/utils.test.js b/src/discord/utils.test.js index 7c5f0c8..7900440 100644 --- a/src/discord/utils.test.js +++ b/src/discord/utils.test.js @@ -84,6 +84,67 @@ test("getPermissions: channel overwrite to allow role works", t => { t.equal((permissions & want), want) }) +test("getPermissions: channel overwrite to allow user works", t => { + const guildRoles = [ + { + version: 1695412489043, + unicode_emoji: null, + tags: {}, + position: 0, + permissions: "559623605571137", + name: "@everyone", + mentionable: false, + managed: false, + id: "1154868424724463687", + icon: null, + hoist: false, + flags: 0, + color: 0 + }, + { + version: 1695412604262, + unicode_emoji: null, + tags: { bot_id: "466378653216014359" }, + position: 1, + permissions: "536995904", + name: "PluralKit", + mentionable: false, + managed: true, + id: "1154868908336099444", + icon: null, + hoist: false, + flags: 0, + color: 0 + }, + { + version: 1698778936921, + unicode_emoji: null, + tags: {}, + position: 1, + permissions: "536870912", + name: "web hookers", + mentionable: false, + managed: false, + id: "1168988246680801360", + icon: null, + hoist: false, + flags: 0, + color: 0 + } + ] + const userRoles = [] + const userID = "353373325575323648" + const overwrites = [ + { type: 0, id: "1154868908336099444", deny: "0", allow: "1024" }, + { type: 0, id: "1154868424724463687", deny: "1024", allow: "0" }, + { type: 0, id: "1168988246680801360", deny: "0", allow: "1024" }, + { type: 1, id: "353373325575323648", deny: "0", allow: "1024" } + ] + const permissions = utils.getPermissions(userRoles, guildRoles, userID, overwrites) + const want = BigInt(1 << 10 | 1 << 16) + t.equal((permissions & want), want) +}) + test("hasSomePermissions: detects the permission", t => { const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"]) @@ -107,3 +168,15 @@ test("hasAllPermissions: doesn't detect not the permissions", t => { const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"]) t.equal(canRemoveMembers, false) }) + +test("isEphemeralMessage: detects ephemeral message", t => { + t.equal(utils.isEphemeralMessage(data.special_message.ephemeral_message), true) +}) + +test("isEphemeralMessage: doesn't detect normal message", t => { + t.equal(utils.isEphemeralMessage(data.message.simple_plaintext), false) +}) + +test("getPublicUrlForCdn: no-op on non-discord URL", t => { + t.equal(utils.getPublicUrlForCdn("https://cadence.moe"), "https://cadence.moe") +}) diff --git a/src/matrix/power.test.js b/src/matrix/power.test.js deleted file mode 100644 index 5423c4f..0000000 --- a/src/matrix/power.test.js +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-check - -const {test} = require("supertape") -const power = require("./power") - -test("power: get affected rooms", t => { - t.deepEqual(power._getAffectedRooms(), [{ - mxid: "@test_auto_invite:example.org", - power_level: 100, - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", - }]) -}) diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 9fb0535..d126851 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -9,7 +9,7 @@ const registrationFilePath = path.join(process.cwd(), "registration.yaml") /** @param {import("../types").AppServiceRegistrationConfig} reg */ function checkRegistration(reg) { - reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line + reg["ooye"].invite = reg.ooye.invite.filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line assert(reg.ooye?.max_file_size) assert(reg.ooye?.namespace_prefix) assert(reg.ooye?.server_name) @@ -19,6 +19,7 @@ function checkRegistration(reg) { assert.match(reg.url, /^https?:/, "url must start with http:// or https://") } +/* c8 ignore next 4 */ /** @param {import("../types").AppServiceRegistrationConfig} reg */ function writeRegistration(reg) { fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2)) @@ -52,6 +53,7 @@ function getTemplateRegistration(serverName) { socket: 6693, ooye: { namespace_prefix, + server_name: serverName, max_file_size: 5000000, content_length_workaround: false, include_user_id_in_mxid: false, @@ -66,6 +68,8 @@ function readRegistration() { try { const content = fs.readFileSync(registrationFilePath, "utf8") result = JSON.parse(content) + result.ooye.invite ||= [] + /* c8 ignore next */ } catch (e) {} return result } diff --git a/src/matrix/read-registration.test.js b/src/matrix/read-registration.test.js index 80ac09f..5fb3b55 100644 --- a/src/matrix/read-registration.test.js +++ b/src/matrix/read-registration.test.js @@ -1,5 +1,8 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") const {test} = require("supertape") -const {reg} = require("./read-registration") +const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration") test("reg: has necessary parameters", t => { const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"] @@ -8,3 +11,19 @@ test("reg: has necessary parameters", t => { propertiesToCheck ) }) + +test("check: passes on sample", t => { + checkRegistration(reg) + t.pass("all assertions passed") +}) + +test("check: fails on template as template is missing some required values that are gathered during setup", t => { + let err + try { + // @ts-ignore + checkRegistration(getTemplateRegistration("cadence.moe")) + } catch (e) { + err = e + } + t.ok(err, "one of the assertions failed as expected") +}) diff --git a/src/types.d.ts b/src/types.d.ts index 62d9b30..576bb59 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -55,9 +55,10 @@ export type InitialAppServiceRegistrationConfig = { socket?: string | number, ooye: { namespace_prefix: string - max_file_size: number, - content_length_workaround: boolean, - invite: string[], + server_name: string + max_file_size: number + content_length_workaround: boolean + invite: string[] include_user_id_in_mxid: boolean } } diff --git a/test/data.js b/test/data.js index eb2c42a..9dc2d72 100644 --- a/test/data.js +++ b/test/data.js @@ -4128,7 +4128,54 @@ module.exports = { guild_id: "112760669178241024" }, position: 0 - } + }, + ephemeral_message: { + webhook_id: "684280192553844747", + type: 20, + tts: false, + timestamp: "2024-09-29T11:22:04.865000+00:00", + position: 0, + pinned: false, + nonce: "1289910062243905536", + mentions: [], + mention_roles: [], + mention_everyone: false, + interaction_metadata: { + user: {baby: true}, + type: 2, + name: "invite", + id: "1289910063691206717", + command_type: 1, + authorizing_integration_owners: {baby: true} + }, + interaction: { + user: {baby: true}, + type: 2, + name: "invite", + id: "1289910063691206717" + }, + id: "1289910064995504182", + flags: 64, + embeds: [], + edited_timestamp: null, + content: "`@cadence:cadence.moe` is already in this server and this channel.", + components: [], + channel_id: "1100319550446252084", + author: { + username: "Matrix Bridge", + public_flags: 0, + id: "684280192553844747", + global_name: null, + discriminator: "5728", + clan: null, + bot: true, + avatar_decoration_data: null, + avatar: "48ae3c24f2a6ec5c60c41bdabd904018" + }, + attachments: [], + application_id: "684280192553844747" + }, + shard_id: 0 }, interaction_message: { thinking_interaction_without_bot_user: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 2c23561..b6f0951 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -3,6 +3,9 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0); +INSERT INTO guild_active (guild_id, autocreate) VALUES +('112760669178241024', 1); + INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES ('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL), ('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL), diff --git a/test/test.js b/test/test.js index 281df29..07146ab 100644 --- a/test/test.js +++ b/test/test.js @@ -23,8 +23,8 @@ reg.id = "baby" // don't actually take authenticated actions on the server reg.as_token = "baby" reg.hs_token = "baby" reg.ooye.bridge_origin = "https://bridge.example.org" -reg.ooye.invite = [] +/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync({watchFS: false}) const discord = { @@ -35,6 +35,7 @@ const discord = { id: "684280192553844747" }, channels: new Map([ + [data.channel.general.id, data.channel.general], ["497161350934560778", { guild_id: "497159726455455754" }], @@ -117,7 +118,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/matrix/kstate.test") require("../src/matrix/api.test") require("../src/matrix/file.test") - require("../src/matrix/power.test") require("../src/matrix/read-registration.test") require("../src/matrix/txnid.test") require("../src/d2m/actions/create-room.test") @@ -136,4 +136,5 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/event-to-message.test") require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test") + require("../src/discord/interactions/invite.test") })() From 61803c3838d494782ae4b3edea70f72f1bbcb8f3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 01:18:56 +1300 Subject: [PATCH 132/346] Add tests for matrix info interaction --- src/discord/interactions/matrix-info.js | 36 ++++++---- src/discord/interactions/matrix-info.test.js | 76 ++++++++++++++++++++ test/test.js | 1 + 3 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 src/discord/interactions/matrix-info.test.js diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index fac3804..f7bc31a 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -6,46 +6,58 @@ const {discord, sync, db, select, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ -async function interact({id, token, guild_id, channel, data}) { +/** + * @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction + * @param {{api: typeof api}} di + * @returns {Promise} + */ +async function _interact({guild_id, data}, {api}) { const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") - .select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get() + .select("name", "nick", "source", "channel_id", "room_id", "event_id").where({message_id: data.target_id, part: 0}).get() if (!message) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This message hasn't been bridged to Matrix.", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } const idInfo = `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\`` + const roomName = message.nick || message.name if (message.source === 1) { // from Discord const userID = data.resolved.messages[data.target_id].author.id - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord to [${message.nick || message.name}]() on Matrix.` + content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${message.channel_id}/${data.target_id} on Discord to [${roomName}]() on Matrix.` + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } // from Matrix const event = await api.getEvent(message.room_id, message.event_id) - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `Bridged [${event.sender}]()'s message in [${message.nick || message.name}]() on Matrix to https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord.` + content: `Bridged [${event.sender}]()'s message in [${roomName}]() on Matrix to https://discord.com/channels/${guild_id}/${message.channel_id}/${data.target_id} on Discord.` + idInfo, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } +} + +/* c8 ignore start */ + +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) } module.exports.interact = interact +module.exports._interact = _interact diff --git a/src/discord/interactions/matrix-info.test.js b/src/discord/interactions/matrix-info.test.js new file mode 100644 index 0000000..f4d0f52 --- /dev/null +++ b/src/discord/interactions/matrix-info.test.js @@ -0,0 +1,76 @@ +const {test} = require("supertape") +const data = require("../../../test/data") +const DiscordTypes = require("discord-api-types/v10") +const {db, discord} = require("../../passthrough") +const {MatrixServerError} = require("../../matrix/mreq") +const {_interact, _interactButton} = require("./matrix-info") + +test("matrix info: checks if message has bridged", async t => { + const msg = await _interact({ + data: { + target_id: "0" + }, + guild_id: "112760669178241024" + }, {}) + t.equal(msg.data.content, "This message hasn't been bridged to Matrix.") +}) + +test("matrix info: shows info for discord source message", async t => { + const msg = await _interact({ + data: { + target_id: "1141619794500649020", + resolved: { + messages: { + "1141619794500649020": data.message_update.edit_by_webhook + } + } + }, + guild_id: "497159726455455754" + }, {}) + t.equal( + msg.data.content, + "Bridged <@700285844094845050> https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020 on Discord to [amanda-spam]() on Matrix." + + "\n-# Room ID: `!CzvdIdUQXgUjDVKxeU:cadence.moe`" + + "\n-# Event ID: `$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10`" + ) +}) + +test("matrix info: shows info for matrix source message", async t => { + let called = 0 + const msg = await _interact({ + data: { + target_id: "1128118177155526666", + resolved: { + messages: { + "1141501302736695316": data.message.simple_reply_to_matrix_user + } + } + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + event_id: eventID, + room_id: roomID, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + } + } + } + }) + t.equal( + msg.data.content, + "Bridged [@cadence:cadence.moe]()'s message in [main]() on Matrix to https://discord.com/channels/112760669178241024/112760669178241024/1128118177155526666 on Discord." + + "\n-# Room ID: `!kLRqKKUQXcibIMtOpl:cadence.moe`" + + "\n-# Event ID: `$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4`" + ) + t.equal(called, 1) +}) diff --git a/test/test.js b/test/test.js index 07146ab..e0736c8 100644 --- a/test/test.js +++ b/test/test.js @@ -137,4 +137,5 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") + require("../src/discord/interactions/matrix-info.test") })() From 33915a595da748217bc349b541c2b1369a707792 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 01:42:12 +1300 Subject: [PATCH 133/346] Add tests for reactions interaction --- addbot.js | 1 + src/discord/interactions/matrix-info.test.js | 7 +- src/discord/interactions/reactions.js | 29 +++++-- src/discord/interactions/reactions.test.js | 87 ++++++++++++++++++++ test/addbot.test.js | 8 ++ test/test.js | 3 + 6 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 src/discord/interactions/reactions.test.js create mode 100644 test/addbot.test.js diff --git a/addbot.js b/addbot.js index e13c829..ef1cc63 100755 --- a/addbot.js +++ b/addbot.js @@ -9,6 +9,7 @@ function addbot() { return `Open this link to add the bot to a Discord server:\nhttps://discord.com/oauth2/authorize?client_id=${id}&scope=bot&permissions=1610883072 ` } +/* c8 ignore next 3 */ if (process.argv.find(a => a.endsWith("addbot") || a.endsWith("addbot.js"))) { console.log(addbot()) } diff --git a/src/discord/interactions/matrix-info.test.js b/src/discord/interactions/matrix-info.test.js index f4d0f52..5d0206e 100644 --- a/src/discord/interactions/matrix-info.test.js +++ b/src/discord/interactions/matrix-info.test.js @@ -1,11 +1,8 @@ const {test} = require("supertape") const data = require("../../../test/data") -const DiscordTypes = require("discord-api-types/v10") -const {db, discord} = require("../../passthrough") -const {MatrixServerError} = require("../../matrix/mreq") -const {_interact, _interactButton} = require("./matrix-info") +const {_interact} = require("./matrix-info") -test("matrix info: checks if message has bridged", async t => { +test("matrix info: checks if message is bridged", async t => { const msg = await _interact({ data: { target_id: "0" diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 67f3a68..0a6d5a6 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -8,19 +8,22 @@ const api = sync.require("../../matrix/api") /** @type {import("../../m2d/converters/utils")} */ const utils = sync.require("../../m2d/converters/utils") -/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ -/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ -async function interact({id, token, data}) { +/** + * @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction + * @param {{api: typeof api}} di + * @returns {Promise} + */ +async function _interact({data}, {api}) { const row = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") .select("event_id", "room_id").where({message_id: data.target_id}).get() if (!row) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This message hasn't been bridged to Matrix.", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") @@ -37,22 +40,30 @@ async function interact({id, token, data}) { } if (inverted.size === 0) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "Nobody from Matrix reacted to this message.", flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } } - return discord.snow.interaction.createInteractionResponse(id, token, { + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), flags: DiscordTypes.MessageFlags.Ephemeral } - }) + } +} + +/* c8 ignore start */ + +/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ +async function interact(interaction) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) } module.exports.interact = interact +module.exports._interact = _interact diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js new file mode 100644 index 0000000..c6de054 --- /dev/null +++ b/src/discord/interactions/reactions.test.js @@ -0,0 +1,87 @@ +const {test} = require("supertape") +const data = require("../../../test/data") +const DiscordTypes = require("discord-api-types/v10") +const {db, discord} = require("../../passthrough") +const {MatrixServerError} = require("../../matrix/mreq") +const {_interact} = require("./reactions") + +test("reactions: checks if message is bridged", async t => { + const msg = await _interact({ + data: { + target_id: "0" + } + }, {}) + t.equal(msg.data.content, "This message hasn't been bridged to Matrix.") +}) + +test("reactions: different response if nobody reacted", async t => { + const msg = await _interact({ + data: { + target_id: "1126786462646550579" + } + }, { + api: { + async getFullRelations(roomID, eventID) { + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg") + return [] + } + } + }) + t.equal(msg.data.content, "Nobody from Matrix reacted to this message.") +}) + +test("reactions: shows reactions if there are some, ignoring discord users", async t => { + let called = 1 + const msg = await _interact({ + data: { + target_id: "1126786462646550579" + } + }, { + api: { + async getFullRelations(roomID, eventID) { + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg") + return [{ + sender: "@cadence:cadence.moe", + content: { + "m.relates_to": { + key: "🐈", + rel_type: "m.annotation" + } + } + }, { + sender: "@rnl:cadence.moe", + content: { + "m.relates_to": { + key: "🐈", + rel_type: "m.annotation" + } + } + }, { + sender: "@cadence:cadence.moe", + content: { + "m.relates_to": { + key: "🐈‍⬛", + rel_type: "m.annotation" + } + } + }, { + sender: "@_ooye_rnl:cadence.moe", + content: { + "m.relates_to": { + key: "🐈", + rel_type: "m.annotation" + } + } + }] + } + } + }) + t.equal( + msg.data.content, + "🐈 ⮞ cadence [they] ⬩ @rnl:cadence.moe" + + "\n🐈‍⬛ ⮞ cadence [they]" + ) + t.equal(called, 1) +}) diff --git a/test/addbot.test.js b/test/addbot.test.js new file mode 100644 index 0000000..17c6dda --- /dev/null +++ b/test/addbot.test.js @@ -0,0 +1,8 @@ +// @ts-check + +const {addbot} = require("../addbot") +const {test} = require("supertape") + +test("addbot: returns message and invite link", t => { + t.equal(addbot(), `Open this link to add the bot to a Discord server:\nhttps://discord.com/oauth2/authorize?client_id=684280192553844747&scope=bot&permissions=1610883072 `) +}) diff --git a/test/test.js b/test/test.js index e0736c8..6695494 100644 --- a/test/test.js +++ b/test/test.js @@ -17,6 +17,7 @@ const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") const {reg} = require("../src/matrix/read-registration") +reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby" reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" reg.id = "baby" // don't actually take authenticated actions on the server @@ -113,6 +114,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not db.exec(fs.readFileSync(join(__dirname, "ooye-test-data.sql"), "utf8")) + require("./addbot.test") require("../src/db/orm.test") require("../src/discord/utils.test") require("../src/matrix/kstate.test") @@ -138,4 +140,5 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") require("../src/discord/interactions/matrix-info.test") + require("../src/discord/interactions/reactions.test") })() From f5853ccf951eb7b08b74b6e24b6d48e0f2acdf44 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 01:45:38 +1300 Subject: [PATCH 134/346] Fix check for enabling content_length_workaround --- src/matrix/mreq.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js index cdcf405..8a76b11 100644 --- a/src/matrix/mreq.js +++ b/src/matrix/mreq.js @@ -45,7 +45,7 @@ async function mreq(method, url, body, extra = {}) { const root = await res.json() if (!res.ok || root.errcode) { - if (root.error?.includes("Content-Length") || !reg.ooye.content_length_workaround) { + if (root.error?.includes("Content-Length") && !reg.ooye.content_length_workaround) { reg.ooye.content_length_workaround = true const root = await mreq(method, url, body, extra) console.error("OOYE cannot stream uploads to Synapse. The `content_length_workaround` option" From f77602afa6171f0841432c16f4e4a9c874db91a5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 16:26:12 +1300 Subject: [PATCH 135/346] Add tests for privacy interaction --- src/discord/interactions/privacy.js | 49 ++++++++++---- src/discord/interactions/privacy.test.js | 86 ++++++++++++++++++++++++ test/test.js | 1 + 3 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 src/discord/interactions/privacy.test.js diff --git a/src/discord/interactions/privacy.js b/src/discord/interactions/privacy.js index bb8c6c9..8227b5a 100644 --- a/src/discord/interactions/privacy.js +++ b/src/discord/interactions/privacy.js @@ -3,32 +3,38 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, db, select} = require("../../passthrough") const {id: botID} = require("../../../addbot") +const {InteractionMethods} = require("snowtransfer") /** @type {import("../../d2m/actions/create-space")} */ const createSpace = sync.require("../../d2m/actions/create-space") /** * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction + * @param {{createSpace: typeof createSpace}} di + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function interact({id, token, data, guild_id}) { +async function* _interact({data, guild_id}, {createSpace}) { // Check guild is bridged const current = select("guild_space", "privacy_level", {guild_id}).pluck().get() - if (current == null) return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "This server isn't bridged to Matrix, so you can't set the Matrix privacy level.", - flags: DiscordTypes.MessageFlags.Ephemeral - } + InteractionMethods.prototype.createInteractionResponse + if (current == null) { + return yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: "This server isn't bridged to Matrix, so you can't set the Matrix privacy level.", + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} } // Get input level /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options - const input = options?.[0].value || "" + const input = options?.[0]?.value || "" const levels = ["invite", "link", "directory"] const level = levels.findIndex(x => input === x) if (level === -1) { - return discord.snow.interaction.createInteractionResponse(id, token, { + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "**Usage: `/privacy `**. This will set who can join the space on Matrix-side. There are three levels:" @@ -38,22 +44,37 @@ async function interact({id, token, data, guild_id}) { + `\n**Current privacy level: \`${levels[current]}\`**`, flags: DiscordTypes.MessageFlags.Ephemeral } - }) + }} } - await discord.snow.interaction.createInteractionResponse(id, token, { + yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, data: { flags: DiscordTypes.MessageFlags.Ephemeral } - }) + }} db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(level, guild_id) await createSpace.syncSpaceFully(guild_id) // this is inefficient but OK to call infrequently on user request - await discord.snow.interaction.editOriginalInteractionResponse(botID, token, { + yield {editOriginalInteractionResponse: { content: `Privacy level updated to \`${levels[level]}\`.` - }) + }} +} + +/* c8 ignore start */ + +/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ +async function interact(interaction) { + for await (const response of _interact(interaction, {createSpace})) { + if (response.createInteractionResponse) { + // TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all. + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } } module.exports.interact = interact +module.exports._interact = _interact diff --git a/src/discord/interactions/privacy.test.js b/src/discord/interactions/privacy.test.js new file mode 100644 index 0000000..a94bbc7 --- /dev/null +++ b/src/discord/interactions/privacy.test.js @@ -0,0 +1,86 @@ +const {test} = require("supertape") +const DiscordTypes = require("discord-api-types/v10") +const {select, db} = require("../../passthrough") +const {_interact} = require("./privacy") + +/** + * @template T + * @param {AsyncIterable} ai + * @returns {Promise} + */ +async function fromAsync(ai) { + const result = [] + for await (const value of ai) { + result.push(value) + } + return result +} + +test("privacy: checks if guild is bridged", async t => { + const msgs = await fromAsync(_interact({ + data: { + options: [] + }, + guild_id: "0" + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "This server isn't bridged to Matrix, so you can't set the Matrix privacy level.") +}) + +test("privacy: reports usage if there is no parameter", async t => { + const msgs = await fromAsync(_interact({ + data: { + options: [] + }, + guild_id: "112760669178241024" + }, {})) + t.equal(msgs.length, 1) + t.match(msgs[0].createInteractionResponse.data.content, /Usage: `\/privacy/) +}) + +test("privacy: reports usage for invalid parameter", async t => { + const msgs = await fromAsync(_interact({ + data: { + options: [ + { + name: "level", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "info" + } + ] + }, + guild_id: "112760669178241024" + }, {})) + t.equal(msgs.length, 1) + t.match(msgs[0].createInteractionResponse.data.content, /Usage: `\/privacy/) +}) + +test("privacy: updates setting and calls syncSpace for valid parameter", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + options: [ + { + name: "level", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "directory" + } + ] + }, + guild_id: "112760669178241024" + }, { + createSpace: { + async syncSpaceFully(guildID) { + called++ + t.equal(guildID, "112760669178241024") + } + } + })) + t.equal(msgs.length, 2) + t.equal(msgs[0].createInteractionResponse.type, DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource) + t.equal(msgs[1].editOriginalInteractionResponse.content, "Privacy level updated to `directory`.") + t.equal(called, 1) + t.equal(select("guild_space", "privacy_level", {guild_id: "112760669178241024"}).pluck().get(), 2) + // Undo database changes + db.prepare("UPDATE guild_space SET privacy_level = 0 WHERE guild_id = ?").run("112760669178241024") +}) diff --git a/test/test.js b/test/test.js index 6695494..c6ab1bb 100644 --- a/test/test.js +++ b/test/test.js @@ -140,5 +140,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") require("../src/discord/interactions/matrix-info.test") + require("../src/discord/interactions/privacy.test") require("../src/discord/interactions/reactions.test") })() From b79b010568859aa0c28539275a9c83ef931c3db1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 16:46:20 +1300 Subject: [PATCH 136/346] Update heatsync dependency --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45de4cc..7fe7cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "entities": "^5.0.0", "get-stream": "^6.0.1", "h3": "^1.12.0", - "heatsync": "^2.5.3", + "heatsync": "^2.5.5", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "node-fetch": "^2.6.7", @@ -1929,9 +1929,9 @@ } }, "node_modules/heatsync": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.4.tgz", - "integrity": "sha512-KzsM+wR0MIykD80kCHNZCpNvFY4uC1Yze8R37eehJyGIvEepJd+7ubczh6FVoBFtK0nVEszt5Hl8AbzUvb+vMQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.5.5.tgz", + "integrity": "sha512-Sy2/X2a69W2W1xgp7GBY81naHtWXxwV8N6uzPTJLQXgq4oTMJeL6F/AUlGS+fUa/Pt5ioxzi7gvd8THMJ3GpyA==", "dependencies": { "backtracker": "^4.0.0" } diff --git a/package.json b/package.json index 857e2d2..68a4583 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "entities": "^5.0.0", "get-stream": "^6.0.1", "h3": "^1.12.0", - "heatsync": "^2.5.3", + "heatsync": "^2.5.5", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "node-fetch": "^2.6.7", From d72b162fe7e53cb7f95d34d3a1054fbeeb960408 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 17:24:26 +1300 Subject: [PATCH 137/346] Mobile design --- src/web/pug/home.pug | 2 +- src/web/pug/includes/template.pug | 2 +- src/web/pug/invite.pug | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web/pug/home.pug b/src/web/pug/home.pug index 1ad787d..6cd7d29 100644 --- a/src/web/pug/home.pug +++ b/src/web/pug/home.pug @@ -4,7 +4,7 @@ block body .s-page-title.mb24 h1.s-page-title--header Bridge a Discord server - .d-grid.grid__2.g24 + .d-grid.g24.grid__2(class="sm:grid__1") .s-card.bs-md.d-flex.fd-column h2 Easy mode p Add the bot to your Discord server. diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 94b5e92..8bb4415 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -53,7 +53,7 @@ html(lang="en") li(role="menuitem") a.s-topbar--item.s-user-card.d-flex.p4(href=`/guild?guild_id=${guild.id}`) +guild(guild) - .mx-auto.w100.wmx9.py24.px8#content + .mx-auto.w100.wmx9.py24.px8.fs-body1#content block body script. document.querySelectorAll("[popovertarget]").forEach(e => { diff --git a/src/web/pug/invite.pug b/src/web/pug/invite.pug index e346880..52b3ab2 100644 --- a/src/web/pug/invite.pug +++ b/src/web/pug/invite.pug @@ -3,7 +3,7 @@ extends includes/template.pug block body if !isValid .s-empty-state.wmx4.p48 - != icons.Spots.SpotAlertXL + != icons.Spots.SpotExpireXL p This QR code has expired. p Refresh the guild management page to generate a new one. From 3662ee5db61f71e892f7938034abbd9027df186a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 22:50:19 +1300 Subject: [PATCH 138/346] Fix interaction updates --- src/d2m/converters/message-to-event.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 582b26c..3466b4c 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -236,8 +236,11 @@ async function messageToEvent(message, guild, options = {}, di) { const interaction = message.interaction_metadata || message.interaction if (message.type === DiscordTypes.MessageType.ChatInputCommand && interaction && "name" in interaction) { // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top. - if (message.content) message.content = `\n${message.content}` - message.content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${message.content}` + let content = message.content + if (content) content = `\n${content}` + else if ((message.flags || 0) & DiscordTypes.MessageFlags.Loading) content = " — interaction loading..." + content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${content}` + message = {...message, content} // editToChanges reuses the object so we can't mutate it. have to clone it } /** From 9f9d1f615edf7cd636fa48b15c57f41733a67268 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 30 Sep 2024 23:35:09 +1300 Subject: [PATCH 139/346] Test coverage for all interactions --- src/discord/interactions/invite.js | 2 +- src/discord/interactions/matrix-info.js | 2 +- src/discord/interactions/permissions.js | 72 +++++-- src/discord/interactions/permissions.test.js | 199 +++++++++++++++++++ src/discord/interactions/privacy.js | 1 - src/discord/interactions/reactions.js | 2 +- src/discord/interactions/reactions.test.js | 4 - test/test.js | 1 + 8 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 src/discord/interactions/permissions.test.js diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index bacf465..b35f44f 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -3,7 +3,7 @@ const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") const assert = require("assert/strict") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, db, select} = require("../../passthrough") /** @type {import("../../d2m/actions/create-room")} */ const createRoom = sync.require("../../d2m/actions/create-room") diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index f7bc31a..b7551f1 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") diff --git a/src/discord/interactions/permissions.js b/src/discord/interactions/permissions.js index e010b1b..fea9ce0 100644 --- a/src/discord/interactions/permissions.js +++ b/src/discord/interactions/permissions.js @@ -2,34 +2,41 @@ const DiscordTypes = require("discord-api-types/v10") const Ty = require("../../types") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, select, from} = require("../../passthrough") const assert = require("assert/strict") +const {id: botID} = require("../../../addbot") +const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction - * @returns {Promise} + * @param {{api: typeof api}} di + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data, channel, guild_id}) { - const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() - assert(row) +async function* _interact({data, guild_id}, {api}) { + // Get message info + const row = from("event_message") + .join("message_channel", "message_id") + .select("event_id", "source", "channel_id") + .where({message_id: data.target_id}) + .get() // Can't operate on Discord users - if (row.source === 1) { // discord - return { + if (!row || row.source === 1) { // not bridged or sent by a discord user + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { - content: `This command is only meaningful for Matrix users.`, + content: `The permissions command can only be used on Matrix users.`, flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } // Get the message sender, the person that will be inspected/edited const eventID = row.event_id - const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() + const roomID = select("channel_room", "room_id", {channel_id: row.channel_id}).pluck().get() assert(roomID) const event = await api.getEvent(roomID, eventID) const sender = event.sender @@ -45,16 +52,16 @@ async function _interact({data, channel, guild_id}) { // Administrators equal to the bot cannot be demoted if (userPower >= 100) { - return { + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `\`${sender}\` has administrator permissions. This cannot be edited.`, flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } - return { + yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: `Showing permissions for \`${sender}\`. Click to edit.`, @@ -82,13 +89,15 @@ async function _interact({data, channel, guild_id}) { } ] } - } + }} } /** * @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction + * @param {{api: typeof api}} di + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function interactEdit({data, id, token, guild_id, message}) { +async function* _interactEdit({data, guild_id, message}, {api}) { // Get the person that will be inspected/edited const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] assert(mxid) @@ -96,13 +105,13 @@ async function interactEdit({data, id, token, guild_id, message}) { const permission = data.values[0] const power = permission === "moderator" ? 50 : 0 - await discord.snow.interaction.createInteractionResponse(id, token, { + yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.UpdateMessage, data: { content: `Updating \`${mxid}\` to **${permission}**, please wait...`, components: [] } - }) + }} // Get the space, where the power levels will be inspected/edited const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() @@ -112,17 +121,40 @@ async function interactEdit({data, id, token, guild_id, message}) { await api.setUserPowerCascade(spaceID, mxid, power) // ACK - await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, { + yield {editOriginalInteractionResponse: { content: `Updated \`${mxid}\` to **${permission}**.`, components: [] - }) + }} } + +/* c8 ignore start */ + /** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction)) + for await (const response of _interact(interaction, {api})) { + if (response.createInteractionResponse) { + // TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all. + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } +} + +/** @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction */ +async function interactEdit(interaction) { + for await (const response of _interactEdit(interaction, {api})) { + if (response.createInteractionResponse) { + // TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all. + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } } module.exports.interact = interact module.exports.interactEdit = interactEdit module.exports._interact = _interact +module.exports._interactEdit = _interactEdit diff --git a/src/discord/interactions/permissions.test.js b/src/discord/interactions/permissions.test.js new file mode 100644 index 0000000..64cda80 --- /dev/null +++ b/src/discord/interactions/permissions.test.js @@ -0,0 +1,199 @@ +const {test} = require("supertape") +const DiscordTypes = require("discord-api-types/v10") +const {select, db} = require("../../passthrough") +const {_interact, _interactEdit} = require("./permissions") + +/** + * @template T + * @param {AsyncIterable} ai + * @returns {Promise} + */ +async function fromAsync(ai) { + const result = [] + for await (const value of ai) { + result.push(value) + } + return result +} + +test("permissions: checks if message is bridged", async t => { + const msgs = await fromAsync(_interact({ + data: { + target_id: "0" + }, + guild_id: "0" + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "The permissions command can only be used on Matrix users.") +}) + +test("permissions: checks if message is sent by a matrix user", async t => { + const msgs = await fromAsync(_interact({ + data: { + target_id: "1126786462646550579" + }, + guild_id: "112760669178241024" + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "The permissions command can only be used on Matrix users.") +}) + +test("permissions: reports permissions of selected matrix user (implicit default)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {} + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.") + t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[0], {label: "Default", value: "default", default: true}) + t.equal(called, 2) +}) + +test("permissions: reports permissions of selected matrix user (moderator)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 50 + } + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "Showing permissions for `@cadence:cadence.moe`. Click to edit.") + t.deepEqual(msgs[0].createInteractionResponse.data.components[0].components[0].options[1], {label: "Moderator", value: "moderator", default: true}) + t.equal(called, 2) +}) + +test("permissions: reports permissions of selected matrix user (admin)", async t => { + let called = 0 + const msgs = await fromAsync(_interact({ + data: { + target_id: "1128118177155526666" + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + sender: "@cadence:cadence.moe" + } + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 100 + } + } + } + } + })) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "`@cadence:cadence.moe` has administrator permissions. This cannot be edited.") + t.notOk(msgs[0].createInteractionResponse.data.components) + t.equal(called, 2) +}) + +test("permissions: can update user to moderator", async t => { + let called = 0 + const msgs = await fromAsync(_interactEdit({ + data: { + target_id: "1128118177155526666", + values: ["moderator"] + }, + message: { + content: "Showing permissions for `@cadence:cadence.moe`. Click to edit." + }, + guild_id: "112760669178241024" + }, { + api: { + async setUserPowerCascade(roomID, mxid, power) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(mxid, "@cadence:cadence.moe") + t.equal(power, 50) + } + } + })) + t.equal(msgs.length, 2) + t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **moderator**, please wait...") + t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **moderator**.") + t.equal(called, 1) +}) + +test("permissions: can update user to default", async t => { + let called = 0 + const msgs = await fromAsync(_interactEdit({ + data: { + target_id: "1128118177155526666", + values: ["default"] + }, + message: { + content: "Showing permissions for `@cadence:cadence.moe`. Click to edit." + }, + guild_id: "112760669178241024" + }, { + api: { + async setUserPowerCascade(roomID, mxid, power) { + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID + t.equal(mxid, "@cadence:cadence.moe") + t.equal(power, 0) + } + } + })) + t.equal(msgs.length, 2) + t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **default**, please wait...") + t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **default**.") + t.equal(called, 1) +}) diff --git a/src/discord/interactions/privacy.js b/src/discord/interactions/privacy.js index 8227b5a..841167e 100644 --- a/src/discord/interactions/privacy.js +++ b/src/discord/interactions/privacy.js @@ -16,7 +16,6 @@ const createSpace = sync.require("../../d2m/actions/create-space") async function* _interact({data, guild_id}, {createSpace}) { // Check guild is bridged const current = select("guild_space", "privacy_level", {guild_id}).pluck().get() - InteractionMethods.prototype.createInteractionResponse if (current == null) { return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 0a6d5a6..1a5a9ca 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const {discord, sync, db, select, from} = require("../../passthrough") +const {discord, sync, select, from} = require("../../passthrough") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js index c6de054..6e29f57 100644 --- a/src/discord/interactions/reactions.test.js +++ b/src/discord/interactions/reactions.test.js @@ -1,8 +1,4 @@ const {test} = require("supertape") -const data = require("../../../test/data") -const DiscordTypes = require("discord-api-types/v10") -const {db, discord} = require("../../passthrough") -const {MatrixServerError} = require("../../matrix/mreq") const {_interact} = require("./reactions") test("reactions: checks if message is bridged", async t => { diff --git a/test/test.js b/test/test.js index c6ab1bb..ee63834 100644 --- a/test/test.js +++ b/test/test.js @@ -140,6 +140,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") require("../src/discord/interactions/matrix-info.test") + require("../src/discord/interactions/permissions.test") require("../src/discord/interactions/privacy.test") require("../src/discord/interactions/reactions.test") })() From 086e8cdc25eca11515de725b666a14bdcde470bb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 3 Oct 2024 03:26:49 +1300 Subject: [PATCH 140/346] Add privacy level controls on web --- src/web/pug/guild.pug | 59 +++++++++++++++++++++++++++++-- src/web/pug/includes/template.pug | 4 +++ src/web/routes/guild-settings.js | 26 ++++++++++++-- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 1e27f2e..250b23b 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -95,8 +95,11 @@ block body src.searchParams.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) img(width=size height=size src=src.toString()) - h2.mt48.fs-headline1 Linked channels + h2.mt48.fs-headline1 Moderation + h2.mt48.fs-headline1 Matrix setup + + h3.mt32.fs-category Linked channels - function getPosition(channel) { let position = 0 @@ -139,9 +142,61 @@ block body p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in. - let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) - input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" hx-swap="none" checked=value) + input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) .is-loading#autocreate-loading + h3.mt32.fs-category Privacy level + .s-card + - let p = select("guild_space", "privacy_level", {guild_id}).pluck().get() + form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this") + input(type="hidden" name="guild_id" value=guild_id) + .d-flex.ai-center.mb4 + label.s-label.fl-grow1 + | How people can join on Matrix + span.is-loading#privacy-level-loading + .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr") + input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(p === 2)) + label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory") + != icons.Icons.IconPlusSm + != icons.Icons.IconInternationalSm + .fl-grow1 Directory + + input(type="radio" name="level" value="link" id="privacy-level-link" checked=(p === 1)) + label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link") + != icons.Icons.IconPlusSm + != icons.Icons.IconLinkSm + .fl-grow1 Link + + input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(p === 0)) + label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite") + svg.svg-icon(width="14" height="14" viewBox="0 0 14 14") + != icons.Icons.IconLockSm + .fl-grow1 Invite + + p.s-description.m0 In-app direct invite from another user; /invite on Discord; web form + p.s-description.m0 Shareable invite links, like Discord + p.s-description.m0 Publicly listed in directory, like Discord server discovery + + + //- + fieldset.s-check-group + legend.s-label How people can join on Matrix + .s-check-control + input.s-radio(type="radio" name="privacy-level" id="privacy-level-invite" value="invite" checked) + label.s-label(for="privacy-level-invite") + | Invite + p.s-description In-app direct invite on Matrix; invite command on Discord; invite form on web + .s-check-control + input.s-radio(type="radio" name="privacy-level" id="privacy-level-link" value="link") + label.s-label(for="privacy-level-link") + | Link + p.s-description All of the above, and shareable invite links (like Discord) + .s-check-control + input.s-radio(type="radio" name="privacy-level" id="privacy-level-directory" value="directory") + label.s-label(for="privacy-level-directory") + | Public + p.s-description All of the above, and publicly visible in the Matrix space directory (like Server Discovery) + h3.mt32.fs-category Manually link channels form.d-flex.g16.ai-start(method="post" action="/api/link") .fl-grow2.s-btn-group.fd-column.w40 diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 8bb4415..8345d76 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -26,6 +26,10 @@ html(lang="en") --theme-dark-primary-color-s: 53%; --theme-dark-primary-color-l: 63%; } + .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental input[type="radio"]:checked ~ label:not(.s-toggle-switch--label-off) { + --_ts-multiple-bg: var(--green-400); + --_ts-multiple-fc: var(--white); + } body.themed.theme-system header.s-topbar .s-topbar--skip-link(href="#content") Skip to main content diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 7940853..1c33854 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -1,15 +1,25 @@ // @ts-check +const assert = require("assert/strict") const {z} = require("zod") const {defineEventHandler, sendRedirect, useSession, createError, readValidatedBody} = require("h3") -const {as, db} = require("../../passthrough") +const {as, db, sync} = require("../../passthrough") const {reg} = require("../../matrix/read-registration") +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") + +/** @type {["invite", "link", "directory"]} */ +const levels = ["invite", "link", "directory"] const schema = { autocreate: z.object({ guild_id: z.string(), autocreate: z.string().optional() + }), + privacyLevel: z.object({ + guild_id: z.string(), + level: z.enum(levels) }) } @@ -19,5 +29,17 @@ as.router.post("/api/autocreate", defineEventHandler(async event => { if (!(session.data.managedGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id) - return sendRedirect(event, `/guild?guild_id=${parsedBody.guild_id}`, 302) + return null // 204 +})) + +as.router.post("/api/privacy-level", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.privacyLevel.parse) + const session = await useSession(event, {password: reg.as_token}) + if (!(session.data.managedGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) + + const i = levels.indexOf(parsedBody.level) + assert.notEqual(i, -1) + db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(i, parsedBody.guild_id) + await createSpace.syncSpaceFully(parsedBody.guild_id) // this is inefficient but OK to call infrequently on user request + return null // 204 })) From 4287a329f511fba40727606ea60d266303e0fc83 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 3 Oct 2024 17:21:42 +1300 Subject: [PATCH 141/346] Display list of unlinked rooms --- src/matrix/api.js | 12 ++++++++++++ src/web/pug/guild.pug | 17 ++++++++++++----- src/web/routes/invite.js | 18 +++++++++++------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/matrix/api.js b/src/matrix/api.js index 4866495..f640efe 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -123,6 +123,17 @@ function getJoinedMembers(roomID) { return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`) } +/** + * "Get the list of members for this room." This includes joined, invited, knocked, left, and banned members unless a filter is provided. + * The endpoint also supports `at` and `not_membership` URL parameters, but they are not exposed in this wrapper yet. + * @param {string} roomID + * @param {"join" | "invite" | "knock" | "leave" | "ban"} [membership] The kind of membership to filter for. Only one choice allowed. + * @returns {Promise<{chunk: Ty.Event.Outer[]}>} + */ +function getMembers(roomID, membership) { + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/members`, {membership}) +} + /** * @param {string} roomID * @param {{from?: string, limit?: any}} pagination @@ -339,6 +350,7 @@ module.exports.getEventForTimestamp = getEventForTimestamp module.exports.getAllState = getAllState module.exports.getStateEvent = getStateEvent module.exports.getJoinedMembers = getJoinedMembers +module.exports.getMembers = getMembers module.exports.getHierarchy = getHierarchy module.exports.getFullHierarchy = getFullHierarchy module.exports.getRelations = getRelations diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 250b23b..68b1e56 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -121,6 +121,9 @@ block body let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => [0, 5].includes(c.type)) unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) + + let linkedRoomIDs = linkedChannels.map(c => c.room_id) + let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id)) .s-card.bs-sm.p0 .s-table-container table.s-table.s-table__bx-simple @@ -147,7 +150,6 @@ block body h3.mt32.fs-category Privacy level .s-card - - let p = select("guild_space", "privacy_level", {guild_id}).pluck().get() form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this") input(type="hidden" name="guild_id" value=guild_id) .d-flex.ai-center.mb4 @@ -155,19 +157,19 @@ block body | How people can join on Matrix span.is-loading#privacy-level-loading .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr") - input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(p === 2)) + input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2)) label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory") != icons.Icons.IconPlusSm != icons.Icons.IconInternationalSm .fl-grow1 Directory - input(type="radio" name="level" value="link" id="privacy-level-link" checked=(p === 1)) + input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1)) label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link") != icons.Icons.IconPlusSm != icons.Icons.IconLinkSm .fl-grow1 Link - input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(p === 0)) + input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0)) label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite") svg.svg-icon(width="14" height="14" viewBox="0 0 14 14") != icons.Icons.IconLockSm @@ -207,7 +209,12 @@ block body else .s-empty-state.p8 All Discord channels are linked. .fl-grow1.s-btn-group.fd-column.w30 - .s-empty-state.p8 I don't know how to get the Matrix room list yet... + each room in unlinkedRooms + input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id) + label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id) + +matrix(room, true) + else + .s-empty-state.p8 All Matrix rooms are linked. div button.s-btn.s-btn__icon.s-btn__filled != icons.Icons.IconLink diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js index 94fa367..0837de0 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/invite.js @@ -36,14 +36,18 @@ const validNonce = new LRUCache({max: 200}) as.router.get("/guild", defineEventHandler(async event => { const {guild_id} = await getValidatedQuery(event, schema.guild.parse) - const nonce = randomUUID() - if (guild_id) { - // Security note: the nonce alone is valid for updating the guild - // We have not verified the user has sufficient permissions in the guild at generation time - // These permissions are checked later during page rendering and the generated nonce is only revealed if the permissions are sufficient - validNonce.set(nonce, guild_id) + const session = await useSession(event, {password: reg.as_token}) + const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get() + if (!guild_id || !row || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { + return pugSync.render(event, "guild.pug", {guild_id}) } - return pugSync.render(event, "guild.pug", {nonce}) + + const nonce = randomUUID() + validNonce.set(nonce, guild_id) + const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "") + const banned = await api.getMembers(row.space_id, "ban") + const rooms = await api.getFullHierarchy(row.space_id) + return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, rooms, ...row}) })) as.router.get("/invite", defineEventHandler(async event => { From 5a86c07eb9f2a2a64dfb2870711e88cb0d2b0fa3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 4 Oct 2024 02:21:57 +1300 Subject: [PATCH 142/346] Host QR codes locally --- package-lock.json | 7 +++++++ package.json | 1 + readme.md | 1 + src/matrix/api.js | 2 +- src/web/pug/guild.pug | 15 ++++++++++----- src/web/routes/qr.js | 19 +++++++++++++++++++ src/web/server.js | 1 + 7 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 src/web/routes/qr.js diff --git a/package-lock.json b/package-lock.json index 7fe7cb0..1189af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", + "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", "zod": "^3.23.8" }, @@ -3237,6 +3238,12 @@ "pathe": "^1.1.2" } }, + "node_modules/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 68a4583..fb7fbc9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "snowtransfer": "^0.10.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", + "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", "zod": "^3.23.8" }, diff --git a/readme.md b/readme.md index daebb1e..f88b6ea 100644 --- a/readme.md +++ b/readme.md @@ -195,5 +195,6 @@ Total transitive production dependencies: 147 * (0) prettier-bytes: It does what I want and has no dependencies. * (2) snowtransfer: Discord API library with bring-your-own-caching that I trust. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. +* (0) uqr: QR code SVG generator. Used on the website to scan in an invite link. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. * (0) zod: Input validation for the web server. It's popular and easy to use. diff --git a/src/matrix/api.js b/src/matrix/api.js index f640efe..7f6be01 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -131,7 +131,7 @@ function getJoinedMembers(roomID) { * @returns {Promise<{chunk: Ty.Event.Outer[]}>} */ function getMembers(roomID, membership) { - return mreq.mreq("GET", `/client/v3/rooms/${roomID}/members`, {membership}) + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/members`, undefined, {membership}) } /** diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 68b1e56..acc1acb 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -45,6 +45,8 @@ mixin matrix(row, radio=false, badge="") else .s-user-card--link.fs-body1 a(href=`https://matrix.to/#/${row.room_id}`)= row.nick || row.name + if row.join_rule === "invite" + +badge-private block body if !guild_id && session.data.managedGuilds @@ -91,9 +93,9 @@ block body div - let size = 105 - let src = new URL(`https://api.qrserver.com/v1/create-qr-code/?qzone=1&format=svg&size=${size}x${size}`) - src.searchParams.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) - img(width=size height=size src=src.toString()) + let p = new URLSearchParams() + p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) + img(width=size height=size src=`/qr?${p}`) h2.mt48.fs-headline1 Moderation @@ -123,7 +125,10 @@ block body unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) let linkedRoomIDs = linkedChannels.map(c => c.room_id) - let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id)) + let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id) && !r.room_type) + // https://discord.com/developers/docs/topics/threads#active-archived-threads + // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name + unlinkedRooms = unlinkedRooms.filter(r => !r.name.match(/^\[(🔒)?⛓️\]/)) .s-card.bs-sm.p0 .s-table-container table.s-table.s-table__bx-simple @@ -175,7 +180,7 @@ block body != icons.Icons.IconLockSm .fl-grow1 Invite - p.s-description.m0 In-app direct invite from another user; /invite on Discord; web form + p.s-description.m0 In-app direct invite from another user p.s-description.m0 Shareable invite links, like Discord p.s-description.m0 Publicly listed in directory, like Discord server discovery diff --git a/src/web/routes/qr.js b/src/web/routes/qr.js new file mode 100644 index 0000000..314b66a --- /dev/null +++ b/src/web/routes/qr.js @@ -0,0 +1,19 @@ +// @ts-check + +const {z} = require("zod") +const {defineEventHandler, getValidatedQuery} = require("h3") + +const {as} = require("../../passthrough") + +const uqr = require("uqr") + +const schema = { + qr: z.object({ + data: z.string().max(128) + }) +} + +as.router.get("/qr", defineEventHandler(async event => { + const {data} = await getValidatedQuery(event, schema.qr.parse) + return new Response(uqr.renderSVG(data, {pixelSize: 3}), {headers: {"content-type": "image/svg+xml"}}) +})) diff --git a/src/web/server.js b/src/web/server.js index 387439f..d1b4cb8 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -26,6 +26,7 @@ sync.require("./routes/download-discord") sync.require("./routes/invite") sync.require("./routes/guild-settings") sync.require("./routes/oauth") +sync.require("./routes/qr") // Files From 6f7ed829b80d3886ab2f1739f433b8a4258eee48 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 5 Oct 2024 02:23:58 +1300 Subject: [PATCH 143/346] Create and populate guild_id column --- scripts/setup.js | 1 - scripts/start-server.js | 1 - src/d2m/discord-packets.js | 45 ++++++++++++------- .../0015-add-guild-id-to-channel-room.sql | 5 +++ start.js | 1 - test/test.js | 1 - 6 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 src/db/migrations/0015-add-guild-id-to-channel-room.sql diff --git a/scripts/setup.js b/scripts/setup.js index 13c2492..f1817b7 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -39,7 +39,6 @@ const passthrough = require("../src/passthrough") const db = new sqlite("ooye.db") const migrate = require("../src/db/migrate") -/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, {sync, db}) diff --git a/scripts/start-server.js b/scripts/start-server.js index 430b3ba..f09c458 100755 --- a/scripts/start-server.js +++ b/scripts/start-server.js @@ -12,7 +12,6 @@ const {reg} = require("../src/matrix/read-registration") const passthrough = require("../src/passthrough") const db = new sqlite("ooye.db") -/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() Object.assign(passthrough, {sync, db}) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index f619f2b..5956ac5 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -4,7 +4,11 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") -const { sync } = passthrough +const {sync, db} = passthrough + +function populateGuildID(guildID, channelID) { + db.prepare("UPDATE channel_room SET guild_id = ? WHERE channel_id = ?").run(guildID, channelID) +} const utils = { /** @@ -36,13 +40,16 @@ const utils = { channel.guild_id = message.d.id arr.push(channel.id) client.channels.set(channel.id, channel) + populateGuildID(message.d.id, channel.id) } for (const thread of message.d.threads || []) { // @ts-ignore thread.guild_id = message.d.id arr.push(thread.id) client.channels.set(thread.id, thread) + populateGuildID(message.d.id, thread.id) } + if (listen === "full") { eventDispatcher.checkMissedExpressions(message.d) eventDispatcher.checkMissedPins(client, message.d) @@ -91,7 +98,11 @@ const utils = { } else if (message.t === "THREAD_CREATE") { client.channels.set(message.d.id, message.d) - + if (message.d["guild_id"]) { + populateGuildID(message.d["guild_id"], message.d.id) + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { client.channels.set(message.d.id, message.d) @@ -113,21 +124,21 @@ const utils = { client.guildChannelMap.delete(message.d.id) - } else if (message.t === "CHANNEL_CREATE" || message.t === "CHANNEL_DELETE") { - if (message.t === "CHANNEL_CREATE") { - client.channels.set(message.d.id, message.d) - if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) - } - } else { - client.channels.delete(message.d.id) - if (message.d["guild_id"]) { - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels) { - const previous = channels.indexOf(message.d.id) - if (previous !== -1) channels.splice(previous, 1) - } + } else if (message.t === "CHANNEL_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have + populateGuildID(message.d["guild_id"], message.d.id) + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } + + } else if (message.t === "CHANNEL_DELETE") { + client.channels.delete(message.d.id) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels) { + const previous = channels.indexOf(message.d.id) + if (previous !== -1) channels.splice(previous, 1) } } } diff --git a/src/db/migrations/0015-add-guild-id-to-channel-room.sql b/src/db/migrations/0015-add-guild-id-to-channel-room.sql new file mode 100644 index 0000000..81342e4 --- /dev/null +++ b/src/db/migrations/0015-add-guild-id-to-channel-room.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE channel_room ADD COLUMN guild_id TEXT; + +COMMIT; diff --git a/start.js b/start.js index be434f0..4ee547b 100755 --- a/start.js +++ b/start.js @@ -9,7 +9,6 @@ const {reg} = require("./src/matrix/read-registration") const passthrough = require("./src/passthrough") const db = new sqlite("ooye.db") -/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync() Object.assign(passthrough, {sync, db}) diff --git a/test/test.js b/test/test.js index ee63834..5f20a80 100644 --- a/test/test.js +++ b/test/test.js @@ -25,7 +25,6 @@ reg.as_token = "baby" reg.hs_token = "baby" reg.ooye.bridge_origin = "https://bridge.example.org" -/** @type {import("heatsync").default} */ // @ts-ignore const sync = new HeatSync({watchFS: false}) const discord = { From da5525a5429fe04ecd11350b07958bf2a8c5e72c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 14 Oct 2024 13:09:30 +1300 Subject: [PATCH 144/346] Make invite interaction async Fix potential lag issues --- src/discord/interactions/invite.js | 84 ++++++++++++------------- src/discord/interactions/invite.test.js | 61 +++++++++++------- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index b35f44f..0afc6d8 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -1,8 +1,9 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const Ty = require("../../types") const assert = require("assert/strict") +const {InteractionMethods} = require("snowtransfer") +const {id: botID} = require("../../../addbot") const {discord, sync, db, select} = require("../../passthrough") /** @type {import("../../d2m/actions/create-room")} */ @@ -15,21 +16,21 @@ const api = sync.require("../../matrix/api") /** * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction * @param {{api: typeof api}} di - * @returns {Promise} + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data, channel, guild_id}, {api}) { +async function* _interact({data, channel, guild_id}, {api}) { // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options const input = options?.[0]?.value || "" const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] - if (!mxid) return { + if (!mxid) return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} const guild = discord.guilds.get(guild_id) assert(guild) @@ -37,15 +38,22 @@ async function _interact({data, channel, guild_id}, {api}) { // Ensure guild and room are bridged db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id) const existing = createRoom.existsOrAutocreatable(channel, guild_id) - if (existing === 0) return { + if (existing === 0) return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} assert(existing) // can't be null or undefined as we just inserted the guild_active row + yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} + const spaceID = await createSpace.ensureSpace(guild) const roomID = await createRoom.ensureRoom(channel.id) @@ -55,24 +63,17 @@ async function _interact({data, channel, guild_id}, {api}) { spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) } catch (e) {} if (spaceMember && spaceMember.membership === "invite") { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, + }} } // Invite Matrix user if not in space if (!spaceMember || spaceMember.membership !== "join") { await api.inviteToRoom(spaceID, mxid) - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `You invited \`${mxid}\` to the server.` - } - } + return yield {editOriginalInteractionResponse: { + content: `You invited \`${mxid}\` to the server.` + }} } // The Matrix user *is* in the space, maybe we want to invite them to this channel? @@ -81,32 +82,24 @@ async function _interact({data, channel, guild_id}, {api}) { roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) } catch (e) {} if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, - flags: DiscordTypes.MessageFlags.Ephemeral, + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, + components: [{ + type: DiscordTypes.ComponentType.ActionRow, components: [{ - type: DiscordTypes.ComponentType.ActionRow, - components: [{ - type: DiscordTypes.ComponentType.Button, - custom_id: "invite_channel", - style: DiscordTypes.ButtonStyle.Primary, - label: "Sure", - }] + type: DiscordTypes.ComponentType.Button, + custom_id: "invite_channel", + style: DiscordTypes.ButtonStyle.Primary, + label: "Sure", }] - } - } + }] + }} } // The Matrix user *is* in the space and in the channel. - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`${mxid}\` is already in this server and this channel.`, - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: `\`${mxid}\` is already in this server and this channel.`, + }} } /** @@ -133,7 +126,14 @@ async function _interactButton({channel, message}, {api}) { /** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) + for await (const response of _interact(interaction, {api})) { + if (response.createInteractionResponse) { + // TODO: Test if it is reasonable to remove `await` from these calls. Or zip these calls with the next interaction iteration and use Promise.all. + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } } /** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ diff --git a/src/discord/interactions/invite.test.js b/src/discord/interactions/invite.test.js index 431ff60..1890b76 100644 --- a/src/discord/interactions/invite.test.js +++ b/src/discord/interactions/invite.test.js @@ -4,19 +4,32 @@ const {db, discord} = require("../../passthrough") const {MatrixServerError} = require("../../matrix/mreq") const {_interact, _interactButton} = require("./invite") +/** + * @template T + * @param {AsyncIterable} ai + * @returns {Promise} + */ +async function fromAsync(ai) { + const result = [] + for await (const value of ai) { + result.push(value) + } + return result +} + test("invite: checks for missing matrix ID", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [] }, channel: discord.channels.get("0"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") }) test("invite: checks for invalid matrix ID", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -26,13 +39,13 @@ test("invite: checks for invalid matrix ID", async t => { }, channel: discord.channels.get("0"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") }) test("invite: checks if channel exists or is autocreatable", async t => { db.prepare("UPDATE guild_active SET autocreate = 0").run() - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -42,14 +55,14 @@ test("invite: checks if channel exists or is autocreatable", async t => { }, channel: discord.channels.get("498323546729086986"), guild_id: "112760669178241024" - }, {}) - t.equal(msg.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.") + }, {})) + t.equal(msgs[0].createInteractionResponse.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.") db.prepare("UPDATE guild_active SET autocreate = 1").run() }) test("invite: checks if user is already invited to space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -72,14 +85,14 @@ test("invite: checks if user is already invited to space", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.") t.equal(called, 1) }) test("invite: invites if user is not in space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -104,14 +117,14 @@ test("invite: invites if user is not in space", async t => { t.equal(mxid, "@cadence:cadence.moe") } } - }) - t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the server.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "You invited `@cadence:cadence.moe` to the server.") t.equal(called, 2) }) test("invite: prompts to invite to room (if never joined)", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -137,14 +150,14 @@ test("invite: prompts to invite to room (if never joined)", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") t.equal(called, 2) }) test("invite: prompts to invite to room (if left)", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -173,8 +186,8 @@ test("invite: prompts to invite to room (if left)", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?") t.equal(called, 2) }) @@ -200,7 +213,7 @@ test("invite button: invites to room when button clicked", async t => { test("invite: no-op if in room and space", async t => { let called = 0 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { options: [{ name: "user", @@ -222,7 +235,7 @@ test("invite: no-op if in room and space", async t => { } } } - }) - t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server and this channel.") + })) + t.equal(msgs[1].editOriginalInteractionResponse.content, "`@cadence:cadence.moe` is already in this server and this channel.") t.equal(called, 2) }) From c127923f4dc462a4f7e9be5a29cb5f50b6858c89 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Oct 2024 16:35:47 +1300 Subject: [PATCH 145/346] Make the link button do something --- src/web/pug/guild.pug | 9 +++--- src/web/routes/link.js | 63 ++++++++++++++++++++++++++++++++++++++++++ src/web/server.js | 3 +- 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 src/web/routes/link.js diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index acc1acb..1d03b68 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -205,7 +205,7 @@ block body p.s-description All of the above, and publicly visible in the Matrix space directory (like Server Discovery) h3.mt32.fs-category Manually link channels - form.d-flex.g16.ai-start(method="post" action="/api/link") + form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this") .fl-grow2.s-btn-group.fd-column.w40 each channel in unlinkedChannels input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) @@ -220,7 +220,8 @@ block body +matrix(room, true) else .s-empty-state.p8 All Matrix rooms are linked. + input(type="hidden" name="guild_id" value=guild_id) div - button.s-btn.s-btn__icon.s-btn__filled - != icons.Icons.IconLink - = ` Connect` + button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator + != icons.Icons.IconMerge + = ` Link` diff --git a/src/web/routes/link.js b/src/web/routes/link.js new file mode 100644 index 0000000..90cfd53 --- /dev/null +++ b/src/web/routes/link.js @@ -0,0 +1,63 @@ +// @ts-check + +const {z} = require("zod") +const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3") +const Ty = require("../../types") + +const {discord, db, as, sync, select, from} = require("../../passthrough") +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") +/** @type {import("../../d2m/actions/create-room")} */ +const createRoom = sync.require("../../d2m/actions/create-room") +const {reg} = require("../../matrix/read-registration") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +const schema = { + link: z.object({ + guild_id: z.string(), + matrix: z.string(), + discord: z.string() + }) +} + +as.router.post("/api/link", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.link.parse) + const session = await useSession(event, {password: reg.as_token}) + + // Check guild ID or nonce + const guildID = parsedBody.guild_id + if (!(session.data.managedGuilds || []).includes(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) + + // Check guild is bridged + const guild = discord.guilds.get(guildID) + if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"}) + const spaceID = await createSpace.ensureSpace(guild) + + // Check channel exists + const channel = discord.channels.get(parsedBody.discord) + if (!channel) throw createError({status: 400, message: "Bad Request", data: "Discord channel does not exist"}) + + // Check channel and room are not already bridged + const row = from("channel_room").select("channel_id", "room_id").and("WHERE channel_id = ? OR room_id = ?").get(parsedBody.discord, parsedBody.matrix) + if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} and room ID ${row.room_id} are already bridged and cannot be reused`}) + + // Check room exists and bridge is joined and bridge has PL 100 + const self = `@${reg.sender_localpart}:${reg.ooye.server_name}` + /** @type {Ty.Event.M_Room_Member} */ + const memberEvent = await api.getStateEvent(parsedBody.matrix, "m.room.member", self) + if (memberEvent.membership !== "join") throw createError({status: 400, message: "Bad Request", data: "Matrix room does not exist"}) + /** @type {Ty.Event.M_Power_Levels} */ + const powerLevelsStateContent = await api.getStateEvent(parsedBody.matrix, "m.room.power_levels", "") + const selfPowerLevel = powerLevelsStateContent.users?.[self] || powerLevelsStateContent.users_default || 0 + if (selfPowerLevel < (powerLevelsStateContent.state_default || 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"}) + + // Insert database entry + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, guild_id) VALUES (?, ?, ?, ?)").run(parsedBody.discord, parsedBody.matrix, channel.name, guildID) + + // Sync room data and space child + createRoom.syncRoom(parsedBody.discord) + + return null // 204 +})) diff --git a/src/web/server.js b/src/web/server.js index d1b4cb8..f9f12a1 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -23,8 +23,9 @@ pugSync.createRoute(as.router, "/ok", "ok.pug") sync.require("./routes/download-matrix") sync.require("./routes/download-discord") -sync.require("./routes/invite") sync.require("./routes/guild-settings") +sync.require("./routes/invite") +sync.require("./routes/link") sync.require("./routes/oauth") sync.require("./routes/qr") From 4167a01ed1cdeb7df2c588504f1628b62bfdb476 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Oct 2024 16:51:20 +1300 Subject: [PATCH 146/346] Add test template for forwarded message --- src/d2m/converters/message-to-event.test.js | 12 ++++ test/data.js | 61 +++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 415f48d..20630d6 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1014,3 +1014,15 @@ test("message2event: @everyone within a link", async t => { "m.mentions": {} }]) }) + +test("message2event: forwarded image", async t => { + const events = await messageToEvent(data.message.forwarded_image) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://github.com/@everyone", + format: "org.matrix.custom.html", + formatted_body: `https://github.com/@everyone`, + "m.mentions": {} + }]) +}) diff --git a/test/data.js b/test/data.js index 9dc2d72..6b49108 100644 --- a/test/data.js +++ b/test/data.js @@ -2129,6 +2129,67 @@ module.exports = { mention_everyone: false, tts: false } + }, + forwarded_image: { type: 0, + content: "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2024-10-16T22:25:01.973000+00:00", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1296237495993892916", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "a8829abe66866d7797b36f0bfac01086", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "1019762340922663022", + message_id: "1019779830469894234" + }, + position: 0, + message_snapshots: [ + { + message: { + type: 0, + content: "", + mentions: [], + mention_roles: [], + attachments: [ + { + id: "1296237494987133070", + filename: "100km.gif", + size: 2965649, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif?ex=67118ebd&is=67103d3d&hm=8ed76d424f92f11366989f2ebc713d4f8206706ef712571e934da45b59944f77&", proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1296237494987133070/100km.gif?ex=67118ebd&is=67103d3d&hm=8ed76d424f92f11366989f2ebc713d4f8206706ef712571e934da45b59944f77&", width: 300, + height: 300, + content_type: "image/gif" + } + ], + embeds: [], + timestamp: "2022-09-15T01:20:58.177000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] } }, pk_message: { From e5f7c7fdcb2fb2f3bddfce70793383f6eaf147e5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 31 Oct 2024 11:53:34 +1300 Subject: [PATCH 147/346] Proxy discord attachment links within embeds --- src/d2m/converters/message-to-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 3466b4c..c141e29 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -602,9 +602,9 @@ async function messageToEvent(message, guild, options = {}, di) { let chosenImage = embed.image?.url // the thumbnail seems to be used for "article" type but displayed big at the bottom by discord if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url - if (chosenImage) rep.addParagraph(`📸 ${chosenImage}`) + if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) - if (embed.video?.url) rep.addParagraph(`🎞️ ${embed.video.url}`) + if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`) if (embed.footer?.text) rep.addLine(`— ${embed.footer.text}`, tag`— ${embed.footer.text}`) let {body, formatted_body: html} = rep.get() From cce432aeeed0367a33c2ccbc7835d86f5d92d68f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 31 Oct 2024 11:55:54 +1300 Subject: [PATCH 148/346] Compatibility: send {} with room joins Now compatible with the spec and with condu(wu)it. --- src/matrix/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/api.js b/src/matrix/api.js index 7f6be01..79dd7f0 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -60,7 +60,7 @@ async function createRoom(content) { */ async function joinRoom(roomIDOrAlias, mxid) { /** @type {Ty.R.RoomJoined} */ - const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid)) + const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid), {}) return root.room_id } From ac165845d72e34b3e72ea7816e3dad6656ccbb44 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 31 Oct 2024 14:42:15 +1300 Subject: [PATCH 149/346] Remove unused parameter --- src/d2m/converters/message-to-event.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index c141e29..cf8d4c2 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -428,8 +428,7 @@ async function messageToEvent(message, guild, options = {}, di) { return {body, html} } - // FIXME: What was the scanMentions parameter supposed to activate? It's unused. - async function addTextEvent(body, html, msgtype, {scanMentions}) { + async function addTextEvent(body, html, msgtype) { // Star * prefix for fallback edits if (options.includeEditFallbackStar) { body = "* " + body @@ -530,7 +529,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Text content appears first const {body, html} = await transformContent(message.content) - await addTextEvent(body, html, msgtype, {scanMentions: true}) + await addTextEvent(body, html, msgtype) } // Then attachments @@ -612,7 +611,7 @@ async function messageToEvent(message, guild, options = {}, di) { html = `
    ${html}
    ` // Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person - await addTextEvent(body, html, "m.notice", {scanMentions: false}) + await addTextEvent(body, html, "m.notice") } // Then stickers From 49948ae2c1460ad9af0447485a753a1ea2d4d00d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 31 Oct 2024 17:34:25 +1300 Subject: [PATCH 150/346] Support forwarded messages --- src/d2m/converters/message-to-event.js | 59 ++++++++++++++++++++- src/d2m/converters/message-to-event.test.js | 33 +++++++++--- test/ooye-test-data.sql | 3 +- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index cf8d4c2..195bddd 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -428,6 +428,12 @@ async function messageToEvent(message, guild, options = {}, di) { return {body, html} } + /** + * After converting Discord content to Matrix plaintext and HTML content, post-process the bodies and push the resulting text event + * @param {string} body matrix event plaintext body + * @param {string} html matrix event HTML body + * @param {string} msgtype matrix event msgtype (maybe m.text or m.notice) + */ async function addTextEvent(body, html, msgtype) { // Star * prefix for fallback edits if (options.includeEditFallbackStar) { @@ -436,7 +442,7 @@ async function messageToEvent(message, guild, options = {}, di) { } const flags = message.flags || 0 - if (flags & 2) { + if (flags & DiscordTypes.MessageFlags.IsCrosspost) { body = `[🔀 ${message.author.username}]\n` + body html = `🔀 ${message.author.username}
    ` + html } @@ -508,7 +514,57 @@ async function messageToEvent(message, guild, options = {}, di) { message.content = "changed the channel name to **" + message.content + "**" } + // Forwarded content appears first + if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_snapshots?.length) { + // Forwarded notice + const eventID = select("event_message", "event_id", {message_id: message.message_reference.message_id}).pluck().get() + const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get() + const forwardedNotice = new mxUtils.MatrixStringBuilder() + if (eventID && room) { + const via = await getViaServersMemo(room.room_id) + forwardedNotice.addLine( + `[🔀 Forwarded from #${room.nick || room.name}]`, + tag`🔀 Forwarded from ${room.nick || room.name}` + ) + } else if (room) { + const via = await getViaServersMemo(room.room_id) + forwardedNotice.addLine( + `[🔀 Forwarded from #${room.nick || room.name}]`, + tag`🔀 Forwarded from ${room.nick || room.name}` + ) + } else { + forwardedNotice.addLine( + `[🔀 Forwarded message]`, + tag`🔀 Forwarded message` + ) + } + // Forwarded content + // @ts-ignore + const forwardedEvents = await messageToEvent(message.message_snapshots[0].message, guild, {includeReplyFallback: false, includeEditFallbackStar: false}, di) + + // Indent + for (const event of forwardedEvents) { + if (["m.text", "m.notice"].includes(event.msgtype)) { + event.msgtype = "m.notice" + event.body = event.body.split("\n").map(l => "» " + l).join("\n") + event.formatted_body = `
    ${event.formatted_body}
    ` + } + } + + // Try to merge the forwarded content with the forwarded notice + let {body, formatted_body} = forwardedNotice.get() + if (forwardedEvents.length >= 1 && ["m.text", "m.notice"].includes(forwardedEvents[0].msgtype)) { // Try to merge the forwarded content and the forwarded notice + forwardedNotice.add("\n", "
    ") + forwardedEvents[0].body = body + forwardedEvents[0].body + forwardedEvents[0].formatted_body = formatted_body + forwardedEvents[0].formatted_body + } else { + await addTextEvent(body, formatted_body, "m.notice") + } + events.push(...forwardedEvents) + } + + // Then text content if (message.content) { // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] @@ -527,7 +583,6 @@ async function messageToEvent(message, guild, options = {}, di) { } } - // Text content appears first const {body, html} = await transformContent(message.content) 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 20630d6..7213bb8 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1017,12 +1017,29 @@ test("message2event: @everyone within a link", async t => { test("message2event: forwarded image", async t => { const events = await messageToEvent(data.message.forwarded_image) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "https://github.com/@everyone", - format: "org.matrix.custom.html", - formatted_body: `https://github.com/@everyone`, - "m.mentions": {} - }]) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "[🔀 Forwarded message]", + format: "org.matrix.custom.html", + formatted_body: "🔀 Forwarded message", + "m.mentions": {}, + msgtype: "m.notice", + }, + { + $type: "m.room.message", + body: "100km.gif", + external_url: "https://bridge.example.org/download/discordcdn/112760669178241024/1296237494987133070/100km.gif", + filename: "100km.gif", + info: { + h: 300, + mimetype: "image/gif", + size: 2965649, + w: 300, + }, + "m.mentions": {}, + msgtype: "m.image", + url: "mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh", + }, + ]) }) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index b6f0951..7e22e9f 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -123,7 +123,8 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/emojis/288858540888686602.png', 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'), ('https://cdn.discordapp.com/attachments/112760669178241024/1197621094786531358/Ins_1960637570.mp4', 'mxc://cadence.moe/kMqLycqMURhVpwleWkmASpnU'), ('https://cdn.discordapp.com/attachments/1099031887500034088/1112476845502365786/voice-message.ogg', 'mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB'), -('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'); +('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'), +('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'); INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), From b23b81819208aef58124eadf6e14211d97f4bb34 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 31 Oct 2024 17:34:50 +1300 Subject: [PATCH 151/346] Use attachment proxy for external_url --- .../message-to-event.embeds.test.js | 2 +- src/d2m/converters/message-to-event.js | 22 +++++++++---------- src/d2m/converters/message-to-event.test.js | 14 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/d2m/converters/message-to-event.embeds.test.js b/src/d2m/converters/message-to-event.embeds.test.js index ef7e9b8..4d76624 100644 --- a/src/d2m/converters/message-to-event.embeds.test.js +++ b/src/d2m/converters/message-to-event.embeds.test.js @@ -67,7 +67,7 @@ test("message2event embeds: image embed and attachment", async t => { 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&", + external_url: "https://bridge.example.org/download/discordcdn/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg", filename: "Screenshot_20231001_034036.jpg", info: { h: 1170, diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 195bddd..73106cd 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -103,7 +103,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ * @param {DiscordTypes.APIAttachment} attachment */ async function attachmentToEvent(mentions, attachment) { - const publicURL = dUtils.getPublicUrlForCdn(attachment.url) + const external_url = dUtils.getPublicUrlForCdn(attachment.url) const emoji = attachment.content_type?.startsWith("image/jp") ? "📸" : attachment.content_type?.startsWith("image/") ? "🖼️" @@ -117,9 +117,9 @@ async function attachmentToEvent(mentions, attachment) { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", - body: `${emoji} Uploaded SPOILER file: ${publicURL} (${pb(attachment.size)})`, + body: `${emoji} Uploaded SPOILER file: ${external_url} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `
    ${emoji} Uploaded SPOILER file: ${publicURL} (${pb(attachment.size)})
    ` + formatted_body: `
    ${emoji} Uploaded SPOILER file: ${external_url} (${pb(attachment.size)})
    ` } } // for large files, always link them instead of uploading so I don't use up all the space in the content repo @@ -128,9 +128,9 @@ async function attachmentToEvent(mentions, attachment) { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", - body: `${emoji} Uploaded file: ${publicURL} (${pb(attachment.size)})`, + body: `${emoji} Uploaded file: ${external_url} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` + formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` } } else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { return { @@ -138,7 +138,7 @@ async function attachmentToEvent(mentions, attachment) { "m.mentions": mentions, msgtype: "m.image", url: await file.uploadDiscordFileToMxc(attachment.url), - external_url: attachment.url, + external_url, body: attachment.description || attachment.filename, filename: attachment.filename, info: { @@ -154,7 +154,7 @@ async function attachmentToEvent(mentions, attachment) { "m.mentions": mentions, msgtype: "m.video", url: await file.uploadDiscordFileToMxc(attachment.url), - external_url: attachment.url, + external_url, body: attachment.description || attachment.filename, filename: attachment.filename, info: { @@ -170,7 +170,7 @@ async function attachmentToEvent(mentions, attachment) { "m.mentions": mentions, msgtype: "m.audio", url: await file.uploadDiscordFileToMxc(attachment.url), - external_url: attachment.url, + external_url, body: attachment.description || attachment.filename, filename: attachment.filename, info: { @@ -185,7 +185,7 @@ async function attachmentToEvent(mentions, attachment) { "m.mentions": mentions, msgtype: "m.file", url: await file.uploadDiscordFileToMxc(attachment.url), - external_url: attachment.url, + external_url, body: attachment.description || attachment.filename, filename: attachment.filename, info: { @@ -197,8 +197,8 @@ async function attachmentToEvent(mentions, attachment) { } /** - * @param {import("discord-api-types/v10").APIMessage} message - * @param {import("discord-api-types/v10").APIGuild} guild + * @param {DiscordTypes.APIMessage} message + * @param {DiscordTypes.APIGuild} guild * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean}} options default values: * - includeReplyFallback: true * - includeEditFallbackStar: false diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 7213bb8..115d99a 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -337,7 +337,7 @@ test("message2event: attachment with no content", async t => { msgtype: "m.image", url: "mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM", body: "image.png", - external_url: "https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png", + external_url: "https://bridge.example.org/download/discordcdn/497161332244742154/1124628646431297546/image.png", filename: "image.png", info: { mimetype: "image/png", @@ -373,7 +373,7 @@ test("message2event: stickers", async t => { msgtype: "m.image", url: "mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus", body: "image.png", - external_url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", + external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1106366167486038016/image.png", filename: "image.png", info: { mimetype: "image/png", @@ -427,7 +427,7 @@ test("message2event: skull webp attachment with content", async t => { mimetype: "image/webp", size: 74290 }, - external_url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp", + external_url: "https://bridge.example.org/download/discordcdn/112760669178241024/1128084747910918195/skull.webp", filename: "skull.webp", url: "mxc://cadence.moe/sDxWmDErBhYBxtDcJQgBETes" }]) @@ -461,7 +461,7 @@ test("message2event: reply to skull webp attachment with content", async t => { mimetype: "image/jpeg", size: 85906 }, - external_url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + external_url: "https://bridge.example.org/download/discordcdn/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", filename: "RDT_20230704_0936184915846675925224905.jpg", url: "mxc://cadence.moe/WlAbFSiNRIHPDEwKdyPeGywa" }]) @@ -551,7 +551,7 @@ test("message2event: reply with a video", async t => { body: "Ins_1960637570.mp4", filename: "Ins_1960637570.mp4", url: "mxc://cadence.moe/kMqLycqMURhVpwleWkmASpnU", - external_url: "https://cdn.discordapp.com/attachments/112760669178241024/1197621094786531358/Ins_1960637570.mp4?ex=65bbee8f&is=65a9798f&hm=ae14f7824c3d526c5e11c162e012e1ee405fd5776e1e9302ed80ccd86503cfda&", + external_url: "https://bridge.example.org/download/discordcdn/112760669178241024/1197621094786531358/Ins_1960637570.mp4", info: { h: 854, mimetype: "video/mp4", @@ -572,7 +572,7 @@ test("message2event: voice message", async t => { t.deepEqual(events, [{ $type: "m.room.message", body: "voice-message.ogg", - external_url: "https://cdn.discordapp.com/attachments/1099031887500034088/1112476845502365786/voice-message.ogg?ex=65c92d4c&is=65b6b84c&hm=0654bab5027474cbe23875954fa117cf44d8914c144cd151879590fa1baf8b1c&", + external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", filename: "voice-message.ogg", info: { duration: 3960.0000381469727, @@ -595,7 +595,7 @@ test("message2event: misc file", async t => { }, { $type: "m.room.message", body: "the.yml", - external_url: "https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml?ex=65cd6270&is=65baed70&hm=8c5f1b571784e3c7f99628492298815884e351ae0dc7c2ae40dd22d97caf27d9&", + external_url: "https://bridge.example.org/download/discordcdn/122155380120748034/1174514575220158545/the.yml", filename: "the.yml", info: { mimetype: "text/plain; charset=utf-8", From 1b539cfa64d39a793ae4727bae3355f556abcd73 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 1 Nov 2024 16:39:56 +1300 Subject: [PATCH 152/346] Forwarding text messages --- src/d2m/converters/message-to-event.js | 5 +- src/d2m/converters/message-to-event.test.js | 58 ++++++++++++++++ test/data.js | 74 +++++++++++++++++++++ test/ooye-test-data.sql | 6 +- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 73106cd..afdb4b8 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -555,9 +555,8 @@ async function messageToEvent(message, guild, options = {}, di) { // Try to merge the forwarded content with the forwarded notice let {body, formatted_body} = forwardedNotice.get() if (forwardedEvents.length >= 1 && ["m.text", "m.notice"].includes(forwardedEvents[0].msgtype)) { // Try to merge the forwarded content and the forwarded notice - forwardedNotice.add("\n", "
    ") - forwardedEvents[0].body = body + forwardedEvents[0].body - forwardedEvents[0].formatted_body = formatted_body + forwardedEvents[0].formatted_body + forwardedEvents[0].body = body + "\n" + forwardedEvents[0].body + forwardedEvents[0].formatted_body = formatted_body + "
    " + forwardedEvents[0].formatted_body } else { await addTextEvent(body, formatted_body, "m.notice") } diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 115d99a..4ae342c 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1043,3 +1043,61 @@ test("message2event: forwarded image", async t => { }, ]) }) + +test("message2event: constructed forwarded message", async t => { + const events = await messageToEvent(data.message.constructed_forwarded_message, {}, {}, { + api: { + async getJoinedMembers(roomID) { + return { + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} + } + } + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "[🔀 Forwarded from #wonderland]" + + "\n» What's cooking, good looking? :hipposcope:", + format: "org.matrix.custom.html", + formatted_body: `🔀 Forwarded from wonderland` + + `
    What's cooking, good looking? :hipposcope:
    `, + "m.mentions": {}, + msgtype: "m.notice", + }, + { + $type: "m.room.message", + body: "100km.gif", + external_url: "https://bridge.example.org/download/discordcdn/112760669178241024/1296237494987133070/100km.gif", + filename: "100km.gif", + info: { + h: 300, + mimetype: "image/gif", + size: 2965649, + w: 300, + }, + "m.mentions": {}, + msgtype: "m.image", + url: "mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh", + }, + { + $type: "m.room.message", + body: "» | ## This man" + + "\n» | " + + "\n» | ## This man is 100 km away from your house" + + "\n» | " + + "\n» | ### Distance away" + + "\n» | 99 km" + + "\n» | " + + "\n» | ### Distance away" + + "\n» | 98 km", + format: "org.matrix.custom.html", + formatted_body: "

    This man

    This man is 100 km away from your house

    Distance away
    99 km

    Distance away
    98 km

    ", + "m.mentions": {}, + msgtype: "m.notice" + } + ]) +}) diff --git a/test/data.js b/test/data.js index 6b49108..8adc459 100644 --- a/test/data.js +++ b/test/data.js @@ -2190,6 +2190,80 @@ module.exports = { } } ] + }, + constructed_forwarded_message: { type: 0, + content: "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2024-10-16T22:25:01.973000+00:00", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1296237495993892916", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "a8829abe66866d7797b36f0bfac01086", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "176333891320283136", + message_id: "1191567971970191490" + }, + position: 0, + message_snapshots: [ + { + message: { + type: 0, + content: "What's cooking, good looking? <:hipposcope:393635038903926784>", + mentions: [], + mention_roles: [], + attachments: [ + { + id: "1296237494987133070", + filename: "100km.gif", + size: 2965649, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif?ex=67118ebd&is=67103d3d&hm=8ed76d424f92f11366989f2ebc713d4f8206706ef712571e934da45b59944f77&", proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1296237494987133070/100km.gif?ex=67118ebd&is=67103d3d&hm=8ed76d424f92f11366989f2ebc713d4f8206706ef712571e934da45b59944f77&", width: 300, + height: 300, + content_type: "image/gif" + } + ], + embeds: [{ + type: "rich", + title: "This man is 100 km away from your house", + author: { + name: "This man" + }, + fields: [{ + name: "Distance away", + value: "99 km" + }, { + name: "Distance away", + value: "98 km" + }] + }], + timestamp: "2022-09-15T01:20:58.177000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] } }, pk_message: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 7e22e9f..af7ea7b 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -64,7 +64,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1273204543739396116', '687028734322147344'), ('1273743950028607530', '1100319550446252084'), ('1278002262400176128', '1100319550446252084'), -('1278001833876525057', '1100319550446252084'); +('1278001833876525057', '1100319550446252084'), +('1191567971970191490', '176333891320283136'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -103,7 +104,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU', 'm.room.message', 'm.text', '1273204543739396116', 0, 0, 0), ('$W1nsDhNIojWrcQOdnOD9RaEvrz2qyZErQoNhPRs1nK4', 'm.room.message', 'm.text', '1273743950028607530', 0, 0, 0), ('$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF', 'm.room.message', 'm.text', '1278002262400176128', 0, 0, 1), -('$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM', 'm.room.message', 'm.text', '1278001833876525057', 0, 0, 1); +('$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM', 'm.room.message', 'm.text', '1278001833876525057', 0, 0, 1), +('$tBIT8mO7XTTCgIINyiAIy6M2MSoPAdJenRl_RLyYuaE', 'm.room.message', 'm.text', '1191567971970191490', 0, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), From 0d8b9d570546121b6818eee50c1afda873f07ba0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 1 Nov 2024 16:50:28 +1300 Subject: [PATCH 153/346] Forwarded messages code coverage and plaintext fix --- src/d2m/converters/message-to-event.js | 31 ++++++------ src/d2m/converters/message-to-event.test.js | 35 +++++++++++++- test/data.js | 52 +++++++++++++++++++++ 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index afdb4b8..251ce90 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -199,9 +199,10 @@ async function attachmentToEvent(mentions, attachment) { /** * @param {DiscordTypes.APIMessage} message * @param {DiscordTypes.APIGuild} guild - * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean}} options default values: + * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean, alwaysReturnFormattedBody?: boolean}} options default values: * - includeReplyFallback: true * - 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. * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API */ async function messageToEvent(message, guild, options = {}, di) { @@ -496,7 +497,7 @@ async function messageToEvent(message, guild, options = {}, di) { const isPlaintext = body === html - if (!isPlaintext) { + if (!isPlaintext || options.alwaysReturnFormattedBody) { Object.assign(newTextMessageEvent, { format: "org.matrix.custom.html", formatted_body: html @@ -520,18 +521,20 @@ async function messageToEvent(message, guild, options = {}, di) { const eventID = select("event_message", "event_id", {message_id: message.message_reference.message_id}).pluck().get() const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get() const forwardedNotice = new mxUtils.MatrixStringBuilder() - if (eventID && room) { + if (room) { + const roomName = room && (room.nick || room.name) const via = await getViaServersMemo(room.room_id) - forwardedNotice.addLine( - `[🔀 Forwarded from #${room.nick || room.name}]`, - tag`🔀 Forwarded from ${room.nick || room.name}` - ) - } else if (room) { - const via = await getViaServersMemo(room.room_id) - forwardedNotice.addLine( - `[🔀 Forwarded from #${room.nick || room.name}]`, - tag`🔀 Forwarded from ${room.nick || room.name}` - ) + if (eventID) { + forwardedNotice.addLine( + `[🔀 Forwarded from #${roomName}]`, + tag`🔀 Forwarded from ${roomName}` + ) + } else { + forwardedNotice.addLine( + `[🔀 Forwarded from #${roomName}]`, + tag`🔀 Forwarded from ${roomName}` + ) + } } else { forwardedNotice.addLine( `[🔀 Forwarded message]`, @@ -541,7 +544,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Forwarded content // @ts-ignore - const forwardedEvents = await messageToEvent(message.message_snapshots[0].message, guild, {includeReplyFallback: false, includeEditFallbackStar: false}, di) + const forwardedEvents = await messageToEvent(message.message_snapshots[0].message, guild, {includeReplyFallback: false, includeEditFallbackStar: false, alwaysReturnFormattedBody: true}, di) // Indent for (const event of forwardedEvents) { diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 4ae342c..50f0908 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1047,7 +1047,7 @@ test("message2event: forwarded image", async t => { test("message2event: constructed forwarded message", async t => { const events = await messageToEvent(data.message.constructed_forwarded_message, {}, {}, { api: { - async getJoinedMembers(roomID) { + async getJoinedMembers() { return { joined: { "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, @@ -1101,3 +1101,36 @@ test("message2event: constructed forwarded message", async t => { } ]) }) + +test("message2event: constructed forwarded text", async t => { + const events = await messageToEvent(data.message.constructed_forwarded_text, {}, {}, { + api: { + async getJoinedMembers() { + return { + joined: { + "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, + "@user:matrix.org": {display_name: null, avatar_url: null} + } + } + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "[🔀 Forwarded from #amanda-spam]" + + "\n» What's cooking, good looking?", + format: "org.matrix.custom.html", + formatted_body: `🔀 Forwarded from amanda-spam` + + `
    What's cooking, good looking?
    `, + "m.mentions": {}, + msgtype: "m.notice", + }, + { + $type: "m.room.message", + body: "What's cooking everybody ‼️", + "m.mentions": {}, + msgtype: "m.text", + } + ]) +}) diff --git a/test/data.js b/test/data.js index 8adc459..1c7983a 100644 --- a/test/data.js +++ b/test/data.js @@ -2264,6 +2264,58 @@ module.exports = { } } ] + }, + constructed_forwarded_text: { type: 0, + content: "What's cooking everybody ‼️", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2024-10-16T22:25:01.973000+00:00", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1296237495993892916", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "a8829abe66866d7797b36f0bfac01086", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: null, + clan: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "497161350934560778", + message_id: "0" + }, + position: 0, + message_snapshots: [ + { + message: { + type: 0, + content: "What's cooking, good looking?", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2022-09-15T01:20:58.177000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] } }, pk_message: { From 14115c0e063010ad40bce866893b80cff3bdee03 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 1 Nov 2024 17:25:11 +1300 Subject: [PATCH 154/346] Attempt retrigger after speedbump --- src/d2m/event-dispatcher.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index af8eedf..c68e11a 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -281,9 +281,6 @@ module.exports = { // Otherwise, if there are embeds, then the system generated URL preview embeds. if (!(typeof data.content === "string" || "embeds" in data)) return - // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return - if (data.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: data.webhook_id}).pluck().get() if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. @@ -295,6 +292,9 @@ module.exports = { const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return + // Check that the sending-to room exists, and deal with Eventual Consistency(TM) + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return + /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore const message = data From 15e5b17b0d3a532034dec98990aba0b6b25befbc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 2 Nov 2024 19:22:30 +1300 Subject: [PATCH 155/346] When inviting bot, check it has bot scope --- src/discord/interactions/invite.js | 15 ++++++++++++--- src/discord/interactions/invite.test.js | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/discord/interactions/invite.js b/src/discord/interactions/invite.js index 0afc6d8..8940363 100644 --- a/src/discord/interactions/invite.js +++ b/src/discord/interactions/invite.js @@ -12,6 +12,8 @@ const createRoom = sync.require("../../d2m/actions/create-room") const createSpace = sync.require("../../d2m/actions/create-space") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/read-registration")} */ +const {reg} = sync.require("../../matrix/read-registration") /** * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction @@ -19,6 +21,16 @@ const api = sync.require("../../matrix/api") * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ async function* _interact({data, channel, guild_id}, {api}) { + // Check guild exists - it might not exist if the application was added with applications.commands scope and not bot scope + const guild = discord.guilds.get(guild_id) + if (!guild) return yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, + data: { + content: `I can't perform actions in this server because there is no bot presence in the server. You should try re-adding this bot to the server, making sure that it has bot scope (not just commands).\nIf you add the bot from ${reg.ooye.bridge_origin} this should work automatically.`, + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} + // Get named MXID /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore const options = data.options @@ -32,9 +44,6 @@ async function* _interact({data, channel, guild_id}, {api}) { } }} - const guild = discord.guilds.get(guild_id) - assert(guild) - // Ensure guild and room are bridged db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id) const existing = createRoom.existsOrAutocreatable(channel, guild_id) diff --git a/src/discord/interactions/invite.test.js b/src/discord/interactions/invite.test.js index 1890b76..8290718 100644 --- a/src/discord/interactions/invite.test.js +++ b/src/discord/interactions/invite.test.js @@ -43,6 +43,21 @@ test("invite: checks for invalid matrix ID", async t => { t.equal(msgs[0].createInteractionResponse.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`") }) +test("invite: checks if guild exists", async t => { // it might not exist if the application was added with applications.commands scope and not bot scope + const msgs = await fromAsync(_interact({ + data: { + options: [{ + name: "user", + type: DiscordTypes.ApplicationCommandOptionType.String, + value: "@cadence:cadence.moe" + }] + }, + channel: discord.channels.get("0"), + guild_id: "0" + }, {})) + t.match(msgs[0].createInteractionResponse.data.content, /there is no bot presence in the server/) +}) + test("invite: checks if channel exists or is autocreatable", async t => { db.prepare("UPDATE guild_active SET autocreate = 0").run() const msgs = await fromAsync(_interact({ From 07d6eb3c1272c2526a4749724c07c4fd530893d4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 2 Nov 2024 20:35:52 +1300 Subject: [PATCH 156/346] Fix existingPartZero assertion error --- src/d2m/actions/retrigger.js | 33 +++++++++++++++++++++++---- src/d2m/converters/edit-to-changes.js | 30 ++++++++++++++---------- src/d2m/event-dispatcher.js | 2 +- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js index 030ffbf..aa79a79 100644 --- a/src/d2m/actions/retrigger.js +++ b/src/d2m/actions/retrigger.js @@ -12,6 +12,7 @@ function debugRetrigger(message) { } } +const paused = new Set() const emitter = new EventEmitter() /** @@ -25,13 +26,15 @@ const emitter = new EventEmitter() * @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered */ function eventNotFoundThenRetrigger(messageID, fn, ...rest) { - const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() - if (eventID) { - debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) - return false // event was found so don't retrigger + if (!paused.has(messageID)) { + const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() + if (eventID) { + debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) + return false // event was found so don't retrigger + } } - debugRetrigger(`[retrigger] WAIT mid <-> eid = ${messageID} <-> ${eventID}`) + debugRetrigger(`[retrigger] WAIT mid = ${messageID}`) emitter.once(messageID, () => { debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`) fn(...rest) @@ -46,6 +49,25 @@ function eventNotFoundThenRetrigger(messageID, fn, ...rest) { return true // event was not found, then retrigger } +/** + * Anything calling retrigger during the callback will be paused and retriggered after the callback resolves. + * @template T + * @param {string} messageID + * @param {Promise} promise + * @returns {Promise} + */ +async function pauseChanges(messageID, promise) { + try { + debugRetrigger(`[retrigger] PAUSE mid = ${messageID}`) + paused.add(messageID) + return await promise + } finally { + debugRetrigger(`[retrigger] RESUME mid = ${messageID}`) + paused.delete(messageID) + messageFinishedBridging(messageID) + } +} + /** * Triggers any pending operations that were waiting on the corresponding event ID. * @param {string} messageID @@ -59,3 +81,4 @@ function messageFinishedBridging(messageID) { module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger module.exports.messageFinishedBridging = messageFinishedBridging +module.exports.pauseChanges = pauseChanges diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index f93c510..c38c24e 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -122,35 +122,41 @@ async function editToChanges(message, guild, api) { eventsToReplace = eventsToReplace.filter(eventCanBeEdited) // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. + // This would be disrupted if existing events that are (reaction_)part = 0 will be redacted. + // If that is the case, pick a different existing or newly sent event to be (reaction_)part = 0. /** @type {({column: string, eventID: string, value?: number} | {column: string, nextEvent: true})[]} */ const promotions = [] for (const column of ["part", "reaction_part"]) { const candidatesForParts = unchangedEvents.concat(eventsToReplace) // If no events with part = 0 exist (or will exist), we need to do some management. if (!candidatesForParts.some(e => e.old[column] === 0)) { + // Try to find an existing event to promote. Bigger order is better. if (candidatesForParts.length) { - // We can choose an existing event to promote. Bigger order is better. const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.old.event_subtype === "m.text") candidatesForParts.sort((a, b) => order(b) - order(a)) if (column === "part") { promotions.push({column, eventID: candidatesForParts[0].old.event_id}) // part should be the first one + } else if (eventsToSend.length) { + promotions.push({column, nextEvent: true}) // reaction_part should be the last one } else { promotions.push({column, eventID: candidatesForParts[candidatesForParts.length - 1].old.event_id}) // reaction_part should be the last one } - } else { - // No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0. + } + // Or, if there are no existing events to promote and new events will be sent, whatever gets sent will be the next part = 0. + else { promotions.push({column, nextEvent: true}) } } - // If adding events, try to keep reactions attached to the bottom of the group (unless reactions have already been added) - if (eventsToSend.length && !promotions.length) { - const existingReaction = select("reaction", "message_id", {message_id: message.id}).pluck().get() - if (!existingReaction) { - const existingPartZero = candidatesForParts.find(p => p.old.reaction_part === 0) - assert(existingPartZero) // will exist because a reaction_part=0 always exists and no events are being removed - promotions.push({column: "reaction_part", eventID: existingPartZero.old.event_id, value: 1}) // update the current reaction_part to 1 - promotions.push({column: "reaction_part", nextEvent: true}) // the newly created event will have reaction_part = 0 - } + } + + // If adding events, try to keep reactions attached to the bottom of the group (unless reactions have already been added) + if (eventsToSend.length && !promotions.length) { + const existingReaction = select("reaction", "message_id", {message_id: message.id}).pluck().get() + if (!existingReaction) { + const existingPartZero = unchangedEvents.concat(eventsToReplace).find(p => p.old.reaction_part === 0) + assert(existingPartZero) // will exist because a reaction_part=0 always exists and no events are being removed + promotions.push({column: "reaction_part", eventID: existingPartZero.old.event_id, value: 1}) // update the current reaction_part to 1 + promotions.push({column: "reaction_part", nextEvent: true}) // the newly created event will have reaction_part = 0 } } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c68e11a..c84d6a8 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -304,7 +304,7 @@ module.exports = { assert(guild) // @ts-ignore - await editMessage.editMessage(message, guild, row) + await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row)) }, /** From a63d173a9a2cdc03c129e5f8bb4e0ed57b290d3d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 25 Nov 2024 16:30:10 +1300 Subject: [PATCH 157/346] Remove redundant/invalid checks from setup --- scripts/setup.js | 13 +------------ src/m2d/converters/utils.js | 4 +--- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index f1817b7..a8b068b 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -230,7 +230,6 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { // Done with user prompts, reg is now guaranteed to be valid const api = require("../src/matrix/api") const file = require("../src/matrix/file") - const utils = require("../src/m2d/converters/utils") const DiscordClient = require("../src/d2m/discord-client") const discord = new DiscordClient(reg.ooye.discord_token, "no") passthrough.discord = discord @@ -267,21 +266,11 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}` - // ensure registration is correctly set... - assert(reg.sender_localpart.startsWith(reg.ooye.namespace_prefix), "appservice's localpart must be in the namespace it controls") - assert(utils.eventSenderIsFromDiscord(mxid), "appservice's mxid must be in the namespace it controls") - assert(reg.ooye.server_origin.match(/^https?:\/\//), "server origin must start with http or https") - assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash") - const botID = Buffer.from(reg.ooye.discord_token.split(".")[0], "base64").toString() - assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") - assert.match(reg.url, /^https?:/, "url must start with http:// or https://") - - console.log("✅ Configuration looks good...") - // database ddl... await migrate.migrate(db) // add initial rows to database, like adding the bot to sim... + const botID = Buffer.from(reg.ooye.discord_token.split(".")[0], "base64").toString() db.prepare("INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(botID, reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), reg.sender_localpart, mxid) console.log("✅ Database is ready...") diff --git a/src/m2d/converters/utils.js b/src/m2d/converters/utils.js index c538627..17cb0fd 100644 --- a/src/m2d/converters/utils.js +++ b/src/m2d/converters/utils.js @@ -30,9 +30,7 @@ const NEWLINE_ELEMENTS = BLOCK_ELEMENTS.concat(["BR"]) */ function eventSenderIsFromDiscord(sender) { // If it's from a user in the bridge's namespace, then it originated from discord - // This includes messages sent by the appservice's bot user, because that is what's used for webhooks - // TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored... - // TODO that only applies to the above todo: But you'd have to watch out for the /icon command, where the bridge bot would set the room avatar, and that shouldn't be reflected into the room a second time. + // This could include messages sent by the appservice's bot user, because that is what's used for webhooks if (userRegex.some(x => sender.match(x))) { return true } From 7ff2a38cdb651e7421073f6ec3b0963832e18689 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 26 Nov 2024 12:17:31 +1300 Subject: [PATCH 158/346] Move room linking logic out of template --- src/web/pug/guild.pug | 26 -------------------------- src/web/routes/invite.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 1d03b68..29ad9ad 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -102,33 +102,7 @@ block body h2.mt48.fs-headline1 Matrix setup h3.mt32.fs-category Linked channels - - - function getPosition(channel) { - let position = 0 - let looking = channel - while (looking.parent_id) { - looking = discord.channels.get(looking.parent_id) - position = looking.position * 1000 - } - if (channel.position) position += channel.position - return position - } - let channelIDs = discord.guildChannelMap.get(guild_id) - let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all() - let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c})).filter(c => c.channel) - let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id) - linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel)) - - let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) - let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => [0, 5].includes(c.type)) - unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) - - let linkedRoomIDs = linkedChannels.map(c => c.room_id) - let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id) && !r.room_type) - // https://discord.com/developers/docs/topics/threads#active-archived-threads - // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name - unlinkedRooms = unlinkedRooms.filter(r => !r.name.match(/^\[(🔒)?⛓️\]/)) .s-card.bs-sm.p0 .s-table-container table.s-table.s-table__bx-simple diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js index 0837de0..5ccf1f0 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/invite.js @@ -34,6 +34,39 @@ const schema = { /** @type {LRUCache} nonce to guild id */ const validNonce = new LRUCache({max: 200}) +function getChannelRoomsLinks(guildID, rooms) { + function getPosition(channel) { + let position = 0 + let looking = channel + while (looking.parent_id) { + looking = discord.channels.get(looking.parent_id) + position = looking.position * 1000 + } + if (channel.position) position += channel.position + return position + } + + let channelIDs = discord.guildChannelMap.get(guildID) + assert(channelIDs) + + let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all() + let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c})).filter(c => c.channel) + let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id) + linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel)) + + let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) + let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => [0, 5].includes(c.type)) + unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) + + let linkedRoomIDs = linkedChannels.map(c => c.room_id) + let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id) && !r.room_type) + // https://discord.com/developers/docs/topics/threads#active-archived-threads + // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name + unlinkedRooms = unlinkedRooms.filter(r => !r.name.match(/^\[(🔒)?⛓️\]/)) + + return {linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms} +} + as.router.get("/guild", defineEventHandler(async event => { const {guild_id} = await getValidatedQuery(event, schema.guild.parse) const session = await useSession(event, {password: reg.as_token}) @@ -47,7 +80,8 @@ as.router.get("/guild", defineEventHandler(async event => { const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "") const banned = await api.getMembers(row.space_id, "ban") const rooms = await api.getFullHierarchy(row.space_id) - return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, rooms, ...row}) + const links = getChannelRoomsLinks(guild_id, rooms) + return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, rooms, ...links, ...row}) })) as.router.get("/invite", defineEventHandler(async event => { From 559d9329f25c35a3618d774927d5297cf4efb904 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 30 Nov 2024 22:56:22 +1300 Subject: [PATCH 159/346] Fix voice messages not being delivered --- src/d2m/converters/message-to-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 251ce90..f2720bd 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -176,7 +176,7 @@ async function attachmentToEvent(mentions, attachment) { info: { mimetype: attachment.content_type, size: attachment.size, - duration: attachment.duration_secs ? attachment.duration_secs * 1000 : undefined + duration: attachment.duration_secs ? Math.round(attachment.duration_secs * 1000) : undefined } } } else { From bded9296afe11c30526cecbd184683c0d99112bd Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Dec 2024 12:29:16 +1300 Subject: [PATCH 160/346] Fix guild page being broken when unlinked --- src/web/routes/invite.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js index 5ccf1f0..0f94fa7 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/invite.js @@ -55,7 +55,7 @@ function getChannelRoomsLinks(guildID, rooms) { linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel)) let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) - let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => [0, 5].includes(c.type)) + let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)).filter(c => c && [0, 5].includes(c.type)) unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) let linkedRoomIDs = linkedChannels.map(c => c.room_id) @@ -72,7 +72,8 @@ as.router.get("/guild", defineEventHandler(async event => { const session = await useSession(event, {password: reg.as_token}) const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get() if (!guild_id || !row || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { - return pugSync.render(event, "guild.pug", {guild_id}) + const links = getChannelRoomsLinks(guild_id, []) + return pugSync.render(event, "guild.pug", {guild_id, ...links}) } const nonce = randomUUID() From a35860cb15888839b4c06ed2bc20a516a2de6cda Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Dec 2024 12:43:00 +1300 Subject: [PATCH 161/346] Handle more guild page situations --- src/web/routes/invite.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/web/routes/invite.js b/src/web/routes/invite.js index 0f94fa7..83973c0 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/invite.js @@ -5,6 +5,7 @@ const {z} = require("zod") const {defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3") const {randomUUID} = require("crypto") const {LRUCache} = require("lru-cache") +const Ty = require("../../types") const {discord, as, sync, select} = require("../../passthrough") /** @type {import("../pug-sync")} */ @@ -34,6 +35,10 @@ const schema = { /** @type {LRUCache} nonce to guild id */ const validNonce = new LRUCache({max: 200}) +/** + * @param {string} guildID + * @param {Ty.R.Hierarchy[]} rooms + */ function getChannelRoomsLinks(guildID, rooms) { function getPosition(channel) { let position = 0 @@ -62,7 +67,7 @@ function getChannelRoomsLinks(guildID, rooms) { let unlinkedRooms = rooms.filter(r => !linkedRoomIDs.includes(r.room_id) && !r.room_type) // https://discord.com/developers/docs/topics/threads#active-archived-threads // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name - unlinkedRooms = unlinkedRooms.filter(r => !r.name.match(/^\[(🔒)?⛓️\]/)) + unlinkedRooms = unlinkedRooms.filter(r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) return {linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms} } @@ -71,18 +76,27 @@ as.router.get("/guild", defineEventHandler(async event => { const {guild_id} = await getValidatedQuery(event, schema.guild.parse) const session = await useSession(event, {password: reg.as_token}) const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get() - if (!guild_id || !row || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { - const links = getChannelRoomsLinks(guild_id, []) - return pugSync.render(event, "guild.pug", {guild_id, ...links}) + + // Permission problems + if (!guild_id || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { + return pugSync.render(event, "guild.pug", {guild_id}) } const nonce = randomUUID() validNonce.set(nonce, guild_id) + + // Unlinked guild + if (!row) { + const links = getChannelRoomsLinks(guild_id, []) + return pugSync.render(event, "guild.pug", {guild_id, nonce, ...links}) + } + + // Linked guild const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "") const banned = await api.getMembers(row.space_id, "ban") const rooms = await api.getFullHierarchy(row.space_id) const links = getChannelRoomsLinks(guild_id, rooms) - return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, rooms, ...links, ...row}) + return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, ...links, ...row}) })) as.router.get("/invite", defineEventHandler(async event => { From 88a232fb4a27875cd02bbf1f4a3e9fbd8cb9192c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Dec 2024 15:06:10 +1300 Subject: [PATCH 162/346] Cope if the username is already registered --- scripts/setup.js | 10 +--------- src/matrix/api.js | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index a8b068b..8db46a7 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -276,15 +276,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { console.log("✅ Database is ready...") // ensure appservice bot user is registered... - try { - await api.register(reg.sender_localpart) - } catch (e) { - if (e.errcode === "M_USER_IN_USE" || e.data?.error === "Internal server error") { - // "Internal server error" is the only OK error because older versions of Synapse say this if you try to register the same username twice. - } else { - throw e - } - } + await api.register(reg.sender_localpart) // upload initial images... const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element.png") diff --git a/src/matrix/api.js b/src/matrix/api.js index 79dd7f0..a75f959 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -35,14 +35,21 @@ function path(p, mxid, otherParams = {}) { /** * @param {string} username - * @returns {Promise} */ -function register(username) { +async function register(username) { console.log(`[api] register: ${username}`) - return mreq.mreq("POST", "/client/v3/register", { - type: "m.login.application_service", - username - }) + try { + await mreq.mreq("POST", "/client/v3/register", { + type: "m.login.application_service", + username + }) + } catch (e) { + if (e.errcode === "M_USER_IN_USE" || e.data?.error === "Internal server error") { + // "Internal server error" is the only OK error because older versions of Synapse say this if you try to register the same username twice. + } else { + throw e + } + } } /** From e00ce22aadacc018846128182cfa67fae45fa5c4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Dec 2024 15:42:32 +1300 Subject: [PATCH 163/346] Replace into guild_active from homepage Allow user to change their mind about auto-create by redoing oauth flow. --- src/web/routes/oauth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js index 2f12dd1..82aebe4 100644 --- a/src/web/routes/oauth.js +++ b/src/web/routes/oauth.js @@ -86,7 +86,7 @@ as.router.get("/oauth", defineEventHandler(async event => { // Set auto-create for the guild // @ts-ignore if (managedGuilds.includes(parsedQuery.data.guild_id)) { - db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, ?)").run(parsedQuery.data.guild_id, +!session.data.selfService) + db.prepare("REPLACE INTO guild_active (guild_id, autocreate) VALUES (?, ?)").run(parsedQuery.data.guild_id, +!session.data.selfService) } if (parsedQuery.data.guild_id) { From 4f040e40d6f1d621f60e35305eaa056ba917001c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 2 Dec 2024 16:33:18 +1300 Subject: [PATCH 164/346] Autocreate space if autocreating the room --- docs/self-service-room-creation-rules.md | 3 --- src/d2m/actions/create-room.js | 9 +++++---- src/d2m/actions/create-space.js | 2 +- src/d2m/converters/message-to-event.test.js | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/self-service-room-creation-rules.md b/docs/self-service-room-creation-rules.md index c47ca14..70892fc 100644 --- a/docs/self-service-room-creation-rules.md +++ b/docs/self-service-room-creation-rules.md @@ -61,8 +61,6 @@ So there will be 3 states of whether a guild is self-service or not. At first, i Pressing buttons on web or using the /invite command on a guild will insert a row into guild_active, allowing it to be bridged. -One more thing. Before v3, when a Matrix room was autocreated it would autocreate the space as well, if it needed to. But now, since nothing will be created until the user takes an action, the guild will always be created directly in response to a request. So room creation can now trust that the guild exists already. - So here's all the technical changes needed to support self-service in v3: - New guild_active table showing whether, and how, a guild is bridged. @@ -70,5 +68,4 @@ So here's all the technical changes needed to support self-service in v3: - When bot is added through "easy mode" web button, REPLACE INTO state 1 and ensureSpace. - When bot is added through "self-service" web button, REPLACE INTO state 0. - Event dispatcher will only ensureRoom if the guild_active state is 1. -- createRoom can trust that the space exists because we check that in a calling function. - createRoom will only create other dependencies if the guild is autocreate. diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 2d8d1af..ece8ef7 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -15,6 +15,8 @@ const api = sync.require("../../matrix/api") const ks = sync.require("../../matrix/kstate") /** @type {import("../../discord/utils")} */ const utils = sync.require("../../discord/utils") +/** @type {import("./create-space")} */ +const createSpace = sync.require("./create-space") /** * There are 3 levels of room privacy: @@ -93,11 +95,9 @@ function convertNameAndTopic(channel, guild, customName) { async function channelToKState(channel, guild, di) { // @ts-ignore const parentChannel = discord.channels.get(channel.parent_id) - const guildRow = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get() - assert(guildRow) /** Used for membership/permission checks. */ - let guildSpaceID = guildRow.space_id + const guildSpaceID = await createSpace.ensureSpace(guild) /** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */ let parentSpaceID = guildSpaceID if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) { @@ -117,7 +117,8 @@ async function channelToKState(channel, guild, di) { avatarEventContent.url = {$url: file.guildIcon(guild)} } - const privacyLevel = guildRow.privacy_level + const privacyLevel = select("guild_space", "privacy_level", {guild_id: guild.id}).pluck().get() + assert(privacyLevel != null) // already ensured the space exists let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] if (channel["thread_metadata"]) history_visibility = "world_readable" diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index 0c3d5e3..04d1413 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -93,7 +93,7 @@ async function _syncSpace(guild, shouldActuallySync) { if (!row) { const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get() - assert.equal(autocreate, 1, `refusing to implicitly create guild ${guild.id}. set the guild_active data first before calling ensureSpace/syncSpace.`) + assert.equal(autocreate, 1, `refusing to implicitly create a space for guild ${guild.id}. set the guild_active data first before calling ensureSpace/syncSpace.`) const creation = (async () => { const guildKState = await guildToKState(guild, createRoom.DEFAULT_PRIVACY_LEVEL) // New spaces will have to use the default privacy level; we obviously can't look up the existing entry diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 50f0908..6f77744 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -575,7 +575,7 @@ test("message2event: voice message", async t => { external_url: "https://bridge.example.org/download/discordcdn/1099031887500034088/1112476845502365786/voice-message.ogg", filename: "voice-message.ogg", info: { - duration: 3960.0000381469727, + duration: 3960, mimetype: "audio/ogg", size: 10584, }, From bf01db13d66f4251dda4f20f3ef16a44f593d4ab Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 3 Dec 2024 01:11:53 +1300 Subject: [PATCH 165/346] Check server before checking well-known --- scripts/setup.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 8db46a7..2782ad8 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -68,10 +68,7 @@ async function uploadAutoEmoji(snow, guild, name, filename) { return emoji } -async function validateHomeserverOrigin(serverUrlPrompt, url) { - if (!url.match(/^https?:\/\//)) return "Must be a URL" - if (url.match(/\/$/)) return "Must not end with a slash" - process.stdout.write(magenta(" checking, please wait...")) +async function suggestWellKnown(serverUrlPrompt, url, otherwise) { try { var json = await fetch(`${url}/.well-known/matrix/client`).then(res => res.json()) let baseURL = json["m.homeserver"].base_url.replace(/\/$/, "") @@ -80,19 +77,28 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { return `Did you mean: ${bold(baseURL)}? (Enter to accept)` } } catch (e) {} + return otherwise +} + +async function validateHomeserverOrigin(serverUrlPrompt, url) { + if (!url.match(/^https?:\/\//)) return "Must be a URL" + if (url.match(/\/$/)) return "Must not end with a slash" + process.stdout.write(magenta(" checking, please wait...")) try { var res = await fetch(`${url}/_matrix/client/versions`) + if (res.status !== 200) { + return suggestWellKnown(serverUrlPrompt, url, `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})`) + } } catch (e) { return e.message } - if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})` try { var json = await res.json() if (!Array.isArray(json?.versions) || !json.versions.includes("v1.11")) { return `OOYE needs Matrix version v1.11, but ${url} doesn't support this` } } catch (e) { - return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)` + return suggestWellKnown(serverUrlPrompt, url, `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)`) } return true } From 53379a962dbfd63265338e384121d77451ae4fed Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 7 Dec 2024 19:20:01 +1300 Subject: [PATCH 166/346] This has never actually occurred --- src/m2d/converters/event-to-message.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 346d8c9..ebbe9e1 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -600,7 +600,6 @@ async function eventToMessage(event, guild, di) { contentPreview = ": " + contentPreviewChunks[0] if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." } else { - console.log("Unable to generate reply preview for this replied-to event because we stripped all of it:", repliedToEvent) contentPreview = "" } } From c599dff5907d9579676e5a03fe1fdf0934f56114 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Dec 2024 01:06:19 +1300 Subject: [PATCH 167/346] Tests and coverage for web --- package.json | 2 +- src/types.d.ts | 4 +- src/web/pug-sync.js | 2 + src/web/pug/guild.pug | 241 ++++++++++-------------- src/web/pug/guild_access_denied.pug | 22 +++ src/web/routes/download-discord.js | 14 +- src/web/routes/download-discord.test.js | 49 +++++ src/web/routes/download-matrix.js | 15 +- src/web/routes/download-matrix.test.js | 36 ++++ src/web/routes/{invite.js => guild.js} | 31 +-- src/web/routes/guild.test.js | 199 +++++++++++++++++++ src/web/routes/qr.test.js | 17 ++ src/web/server.js | 3 +- src/web/server.test.js | 32 ++++ test/ooye-test-data.sql | 12 +- test/test.js | 16 +- test/web.js | 69 +++++++ 17 files changed, 593 insertions(+), 171 deletions(-) create mode 100644 src/web/pug/guild_access_denied.pug create mode 100644 src/web/routes/download-discord.test.js create mode 100644 src/web/routes/download-matrix.test.js rename src/web/routes/{invite.js => guild.js} (85%) create mode 100644 src/web/routes/guild.test.js create mode 100644 src/web/routes/qr.test.js create mode 100644 src/web/server.test.js create mode 100644 test/web.js diff --git a/package.json b/package.json index fb7fbc9..e79a6b0 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "start": "node start.js", "setup": "node scripts/setup.js", "addbot": "node addbot.js", - "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", + "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", "cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/matrix/mreq.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" } diff --git a/src/types.d.ts b/src/types.d.ts index 576bb59..178a560 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -114,7 +114,7 @@ export namespace Event { sender: string content: T origin_server_ts: number - unsigned: any + unsigned?: any event_id: string } @@ -137,7 +137,7 @@ export namespace Event { content: any state_key: string origin_server_ts: number - unsigned: any + unsigned?: any event_id: string user_id: string age: number diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js index 32e7acc..aa1a945 100644 --- a/src/web/pug-sync.js +++ b/src/web/pug-sync.js @@ -42,6 +42,7 @@ function render(event, filename, locals) { {session} // Session is always session because it has to be trusted )) }) + /* c8 ignore start */ } catch (e) { pugCache.set(path, async (event) => { setResponseStatus(event, 500, "Internal Template Error") @@ -49,6 +50,7 @@ function render(event, filename, locals) { return e.toString() }) } + /* c8 ignore stop */ } if (!pugCache.has(path)) { diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 29ad9ad..481ad01 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -11,7 +11,7 @@ mixin badge-private | Private mixin discord(channel, radio=false) - - let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, "", channel.permission_overwrites) + - let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, null, channel.permission_overwrites) .s-user-card.s-user-card__small if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) != icons.Icons.IconLock @@ -49,153 +49,110 @@ mixin matrix(row, radio=false, badge="") +badge-private block body - if !guild_id && session.data.managedGuilds - .s-empty-state.wmx4.p48 - != icons.Spots.SpotEmptyXL - p Select a server from the top right corner to continue. - p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.] + .s-page-title.mb24 + h1.s-page-title--header= guild.name - else if !session.data.managedGuilds - .s-empty-state.wmx4.p48 - != icons.Spots.SpotEmptyXL - p You need to log in to manage your servers. - a.s-btn.s-btn__icon.s-btn__filled(href="/oauth") - != icons.Icons.IconDiscord - = ` Log in with Discord` + .d-flex.g16 + .fl-grow1 + h2.fs-headline1 Invite a Matrix user - else if !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id) - .s-empty-state.wmx4.p48 - != icons.Spots.SpotAlertXL - p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord. - p If you've checked your permissions, try #[a(href="/oauth") logging in again.] - - else - - let guild = discord.guilds.get(guild_id) - - .s-page-title.mb24 - h1.s-page-title--header= guild.name - - .d-flex.g16 - .fl-grow1 - h2.fs-headline1 Invite a Matrix user - - form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") - label.s-label(for="mxid") Matrix ID - input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") - label.s-label(for="permissions") Permissions - .s-select - select#permissions(name="permissions") - option(value="default") Default - option(value="moderator") Moderator - input(type="hidden" name="guild_id" value=guild_id) - .grid--row-start2 - button.s-btn.s-btn__filled.htmx-indicator Invite - div - - - let size = 105 - let p = new URLSearchParams() - p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) - img(width=size height=size src=`/qr?${p}`) - - h2.mt48.fs-headline1 Moderation - - h2.mt48.fs-headline1 Matrix setup - - h3.mt32.fs-category Linked channels - - .s-card.bs-sm.p0 - .s-table-container - table.s-table.s-table__bx-simple - each row in linkedChannelsWithDetails - tr - td.w40: +discord(row.channel) - td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm - td: +matrix(row) - else - tr - td(colspan="3") - .s-empty-state No channels linked between Discord and Matrix yet... - - h3.mt32.fs-category Auto-create - .s-card - form.d-flex.ai-center.g8 - label.s-label.fl-grow1(for="autocreate") - | Create new Matrix rooms automatically - p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in. - - let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get() + form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") + label.s-label(for="mxid") Matrix ID + input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") + label.s-label(for="permissions") Permissions + .s-select + select#permissions(name="permissions") + option(value="default") Default + option(value="moderator") Moderator input(type="hidden" name="guild_id" value=guild_id) - input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) - .is-loading#autocreate-loading + .grid--row-start2 + button.s-btn.s-btn__filled.htmx-indicator Invite + div + - + let size = 105 + let p = new URLSearchParams() + p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) + img(width=size height=size src=`/qr?${p}`) - h3.mt32.fs-category Privacy level - .s-card - form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this") - input(type="hidden" name="guild_id" value=guild_id) - .d-flex.ai-center.mb4 - label.s-label.fl-grow1 - | How people can join on Matrix - span.is-loading#privacy-level-loading - .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr") - input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2)) - label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory") - != icons.Icons.IconPlusSm - != icons.Icons.IconInternationalSm - .fl-grow1 Directory + h2.mt48.fs-headline1 Moderation - input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1)) - label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link") - != icons.Icons.IconPlusSm - != icons.Icons.IconLinkSm - .fl-grow1 Link + h2.mt48.fs-headline1 Matrix setup - input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0)) - label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite") - svg.svg-icon(width="14" height="14" viewBox="0 0 14 14") - != icons.Icons.IconLockSm - .fl-grow1 Invite + h3.mt32.fs-category Linked channels - p.s-description.m0 In-app direct invite from another user - p.s-description.m0 Shareable invite links, like Discord - p.s-description.m0 Publicly listed in directory, like Discord server discovery - - - //- - fieldset.s-check-group - legend.s-label How people can join on Matrix - .s-check-control - input.s-radio(type="radio" name="privacy-level" id="privacy-level-invite" value="invite" checked) - label.s-label(for="privacy-level-invite") - | Invite - p.s-description In-app direct invite on Matrix; invite command on Discord; invite form on web - .s-check-control - input.s-radio(type="radio" name="privacy-level" id="privacy-level-link" value="link") - label.s-label(for="privacy-level-link") - | Link - p.s-description All of the above, and shareable invite links (like Discord) - .s-check-control - input.s-radio(type="radio" name="privacy-level" id="privacy-level-directory" value="directory") - label.s-label(for="privacy-level-directory") - | Public - p.s-description All of the above, and publicly visible in the Matrix space directory (like Server Discovery) - - h3.mt32.fs-category Manually link channels - form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this") - .fl-grow2.s-btn-group.fd-column.w40 - each channel in unlinkedChannels - input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) - label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id) - +discord(channel, true, "Announcement") + .s-card.bs-sm.p0 + .s-table-container + table.s-table.s-table__bx-simple + each row in linkedChannelsWithDetails + tr + td.w40: +discord(row.channel) + td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm + td: +matrix(row) else - .s-empty-state.p8 All Discord channels are linked. - .fl-grow1.s-btn-group.fd-column.w30 - each room in unlinkedRooms - input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id) - label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id) - +matrix(room, true) - else - .s-empty-state.p8 All Matrix rooms are linked. + tr + td(colspan="3") + .s-empty-state No channels linked between Discord and Matrix yet... + + h3.mt32.fs-category Auto-create + .s-card + form.d-flex.ai-center.g8 + label.s-label.fl-grow1(for="autocreate") + | Create new Matrix rooms automatically + p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in. + - let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get() input(type="hidden" name="guild_id" value=guild_id) - div - button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator - != icons.Icons.IconMerge - = ` Link` + input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value) + .is-loading#autocreate-loading + + h3.mt32.fs-category Privacy level + .s-card + form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this") + input(type="hidden" name="guild_id" value=guild_id) + .d-flex.ai-center.mb4 + label.s-label.fl-grow1 + | How people can join on Matrix + span.is-loading#privacy-level-loading + .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr") + input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2)) + label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory") + != icons.Icons.IconPlusSm + != icons.Icons.IconInternationalSm + .fl-grow1 Directory + + input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1)) + label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link") + != icons.Icons.IconPlusSm + != icons.Icons.IconLinkSm + .fl-grow1 Link + + input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0)) + label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite") + svg.svg-icon(width="14" height="14" viewBox="0 0 14 14") + != icons.Icons.IconLockSm + .fl-grow1 Invite + + p.s-description.m0 In-app direct invite from another user + p.s-description.m0 Shareable invite links, like Discord + p.s-description.m0 Publicly listed in directory, like Discord server discovery + + h3.mt32.fs-category Manually link channels + form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this") + .fl-grow2.s-btn-group.fd-column.w40 + each channel in unlinkedChannels + input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) + label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id) + +discord(channel, true, "Announcement") + else + .s-empty-state.p8 All Discord channels are linked. + .fl-grow1.s-btn-group.fd-column.w30 + each room in unlinkedRooms + input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id) + label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id) + +matrix(room, true) + else + .s-empty-state.p8 All Matrix rooms are linked. + input(type="hidden" name="guild_id" value=guild_id) + div + button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator + != icons.Icons.IconMerge + = ` Link` diff --git a/src/web/pug/guild_access_denied.pug b/src/web/pug/guild_access_denied.pug new file mode 100644 index 0000000..2305605 --- /dev/null +++ b/src/web/pug/guild_access_denied.pug @@ -0,0 +1,22 @@ +extends includes/template.pug + +block body + if !session.data.managedGuilds + .s-empty-state.wmx4.p48 + != icons.Spots.SpotEmptyXL + p You need to log in to manage your servers. + a.s-btn.s-btn__icon.s-btn__filled(href="/oauth") + != icons.Icons.IconDiscord + = ` Log in with Discord` + + else if !guild_id + .s-empty-state.wmx4.p48 + != icons.Spots.SpotEmptyXL + p Select a server from the top right corner to continue. + p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.] + + else if !discord.guilds.has(guild_id) || !session.data.managedGuilds.includes(guild_id) + .s-empty-state.wmx4.p48 + != icons.Spots.SpotAlertXL + p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord. + p If you've checked your permissions, try #[a(href="/oauth") logging in again.] diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js index ee64074..bbf33b0 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert/strict") -const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError} = require("h3") +const {defineEventHandler, getValidatedRouterParams, sendRedirect, createError, H3Event} = require("h3") const {z} = require("zod") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore @@ -19,6 +19,15 @@ const schema = { }) } +/** + * @param {H3Event} event + * @returns {import("snowtransfer").SnowTransfer} + */ +function getSnow(event) { + /* c8 ignore next */ + return event.context.snow || discord.snow +} + /** @type {Map>} */ const cache = new Map() @@ -56,7 +65,8 @@ function defineMediaProxyHandler(domain) { if (!timeUntilExpiry(refreshed)) promise = undefined } if (!promise) { - promise = discord.snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) + const snow = getSnow(event) + promise = snow.channel.refreshAttachmentURLs([url]).then(x => x.refreshed_urls[0].refreshed) cache.set(url, promise) refreshed = await promise const time = timeUntilExpiry(refreshed) diff --git a/src/web/routes/download-discord.test.js b/src/web/routes/download-discord.test.js new file mode 100644 index 0000000..b0b0077 --- /dev/null +++ b/src/web/routes/download-discord.test.js @@ -0,0 +1,49 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") +const {test} = require("supertape") +const {router} = require("../../../test/web") +const {MatrixServerError} = require("../../matrix/mreq") + +const snow = { + channel: { + async refreshAttachmentURLs(attachments) { + if (typeof attachments === "string") attachments = [attachments] + return { + refreshed_urls: attachments.map(a => ({ + original: a, + refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}` + })) + } + } + } +} + +test("web download discord: access denied if not a known attachment", async t => { + const [error] = await tryToCatch(() => + router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", { + params: { + channel_id: "1", + attachment_id: "2", + file_name: "image.png" + }, + snow + }) + ) + t.ok(error) +}) + +test("web download discord: works if a known attachment", async t => { + const event = {} + await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", { + params: { + channel_id: "655216173696286746", + attachment_id: "1314358913482621010", + file_name: "image.png" + }, + event, + snow + }) + t.equal(event.node.res.statusCode, 302) + t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/) +}) diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index f996716..8f790c5 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert/strict") -const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError} = require("h3") +const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError, H3Event} = require("h3") const {z} = require("zod") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore @@ -11,9 +11,6 @@ require("xxhash-wasm")().then(h => hasher = h) const {sync, as, select} = require("../../passthrough") -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - const schema = { params: z.object({ server_name: z.string(), @@ -21,6 +18,15 @@ const schema = { }) } +/** + * @param {H3Event} event + * @returns {import("../../matrix/api")} + */ +function getAPI(event) { + /* c8 ignore next */ + return event.context.api || sync.require("../../matrix/api") +} + as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => { const params = await getValidatedRouterParams(event, schema.params.parse) @@ -36,6 +42,7 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn }) } + const api = getAPI(event) const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`) const contentType = res.headers.get("content-type") diff --git a/src/web/routes/download-matrix.test.js b/src/web/routes/download-matrix.test.js new file mode 100644 index 0000000..d44271a --- /dev/null +++ b/src/web/routes/download-matrix.test.js @@ -0,0 +1,36 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") +const {test} = require("supertape") +const {router} = require("../../../test/web") +const fetch = require("node-fetch") + +test("web download matrix: access denied if not a known attachment", async t => { + const [error] = await tryToCatch(() => + router.test("get", "/download/matrix/:server_name/:media_id", { + params: { + server_name: "cadence.moe", + media_id: "1" + } + }) + ) + t.ok(error) +}) + +test("web download matrix: works if a known attachment", async t => { + const event = {} + await router.test("get", "/download/matrix/:server_name/:media_id", { + params: { + server_name: "cadence.moe", + media_id: "KrwlqopRyMxnEBcWDgpJZPxh", + }, + event, + api: { + async getMedia(mxc, init) { + return new fetch.Response("", {status: 200, headers: {"content-type": "image/png"}}) + } + } + }) + t.equal(event.node.res.statusCode, 200) + t.equal(event.node.res.getHeader("content-type"), "image/png") +}) diff --git a/src/web/routes/invite.js b/src/web/routes/guild.js similarity index 85% rename from src/web/routes/invite.js rename to src/web/routes/guild.js index 83973c0..fe24483 100644 --- a/src/web/routes/invite.js +++ b/src/web/routes/guild.js @@ -2,7 +2,7 @@ const assert = require("assert/strict") const {z} = require("zod") -const {defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3") +const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3") const {randomUUID} = require("crypto") const {LRUCache} = require("lru-cache") const Ty = require("../../types") @@ -14,9 +14,6 @@ const pugSync = sync.require("../pug-sync") const createSpace = sync.require("../../d2m/actions/create-space") const {reg} = require("../../matrix/read-registration") -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - const schema = { guild: z.object({ guild_id: z.string().optional() @@ -32,6 +29,15 @@ const schema = { }) } +/** + * @param {H3Event} event + * @returns {import("../../matrix/api")} + */ +function getAPI(event) { + /* c8 ignore next */ + return event.context.api || sync.require("../../matrix/api") +} + /** @type {LRUCache} nonce to guild id */ const validNonce = new LRUCache({max: 200}) @@ -76,10 +82,12 @@ as.router.get("/guild", defineEventHandler(async event => { const {guild_id} = await getValidatedQuery(event, schema.guild.parse) const session = await useSession(event, {password: reg.as_token}) const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get() + // @ts-ignore + const guild = discord.guilds.get(guild_id) // Permission problems - if (!guild_id || !discord.guilds.has(guild_id) || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { - return pugSync.render(event, "guild.pug", {guild_id}) + if (!guild_id || !guild || !session.data.managedGuilds || !session.data.managedGuilds.includes(guild_id)) { + return pugSync.render(event, "guild_access_denied.pug", {guild_id}) } const nonce = randomUUID() @@ -92,11 +100,12 @@ as.router.get("/guild", defineEventHandler(async event => { } // Linked guild + const api = getAPI(event) const mods = await api.getStateEvent(row.space_id, "m.room.power_levels", "") const banned = await api.getMembers(row.space_id, "ban") const rooms = await api.getFullHierarchy(row.space_id) const links = getChannelRoomsLinks(guild_id, rooms) - return pugSync.render(event, "guild.pug", {guild_id, nonce, mods, banned, ...links, ...row}) + return pugSync.render(event, "guild.pug", {guild, guild_id, nonce, mods, banned, ...links, ...row}) })) as.router.get("/invite", defineEventHandler(async event => { @@ -110,6 +119,7 @@ as.router.get("/invite", defineEventHandler(async event => { as.router.post("/api/invite", defineEventHandler(async event => { const parsedBody = await readValidatedBody(event, schema.invite.parse) const session = await useSession(event, {password: reg.as_token}) + const api = getAPI(event) // Check guild ID or nonce if (parsedBody.guild_id) { @@ -136,15 +146,14 @@ as.router.post("/api/invite", defineEventHandler(async event => { spaceMember = await api.getStateEvent(spaceID, "m.room.member", parsedBody.mxid) } catch (e) {} - if (!spaceMember || spaceMember.membership !== "invite" || spaceMember.membership !== "join") { + if (!spaceMember || !["invite", "join"].includes(spaceMember.membership)) { // Invite await api.inviteToRoom(spaceID, parsedBody.mxid) } // Permissions - if (parsedBody.permissions === "moderator") { - await api.setUserPowerCascade(spaceID, parsedBody.mxid, 50) - } + const powerLevel = parsedBody.permissions === "moderator" ? 50 : 0 + await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel) if (parsedBody.guild_id) { return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302) diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js new file mode 100644 index 0000000..b5b4080 --- /dev/null +++ b/src/web/routes/guild.test.js @@ -0,0 +1,199 @@ +// @ts-check + +const tryToCatch = require("try-to-catch") +const {test} = require("supertape") +const {router} = require("../../../test/web") +const {MatrixServerError} = require("../../matrix/mreq") + +let nonce + +test("web guild: access denied when not logged in", async t => { + const content = await router.test("get", "/guild?guild_id=112760669178241024", { + sessionData: { + }, + }) + t.match(content, /You need to log in to manage your servers./) +}) + +test("web guild: asks to select guild if not selected", async t => { + const content = await router.test("get", "/guild", { + sessionData: { + managedGuilds: [] + }, + }) + t.match(content, /Select a server from the top right corner to continue./) +}) + +test("web guild: access denied when guild id messed up", async t => { + const content = await router.test("get", "/guild?guild_id=1", { + sessionData: { + managedGuilds: [] + }, + }) + t.match(content, /the selected server doesn't exist/) +}) + + + + +test("web invite: access denied with invalid nonce", async t => { + const content = await router.test("get", "/invite?nonce=1") + t.match(content, /This QR code has expired./) +}) + +test("web guild: can view guild", async t => { + const content = await router.test("get", "/guild?guild_id=112760669178241024", { + sessionData: { + managedGuilds: ["112760669178241024"] + }, + api: { + async getStateEvent(roomID, type, key) { + return {} + }, + async getMembers(roomID, membership) { + return {chunk: []} + }, + async getFullHierarchy(roomID) { + return [] + } + } + }) + t.match(content, / { + const content = await router.test("get", `/invite?nonce=${nonce}`) + t.match(content, /Invite a Matrix user/) +}) + + + + +test("api invite: access denied with nothing", async t => { + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "moderator" + } + }) + ) + t.equal(error.message, "Missing guild ID") +}) + +test("api invite: access denied when not in guild", async t => { + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "moderator", + guild_id: "112760669178241024" + } + }) + ) + t.equal(error.message, "Forbidden") +}) + +test("api invite: can invite with valid nonce", async t => { + let called = 0 + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "moderator", + nonce + }, + api: { + async getStateEvent(roomID, type, key) { + called++ + return {membership: "leave"} + }, + async inviteToRoom(roomID, mxidToInvite, mxid) { + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + called++ + }, + async setUserPowerCascade(roomID, mxid, power) { + t.equal(power, 50) // moderator + called++ + } + } + }) + ) + t.notOk(error) + t.equal(called, 3) +}) + +test("api invite: access denied when nonce has been used", async t => { + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "moderator", + nonce + } + }) + ) + t.equal(error.message, "Nonce expired") +}) + +test("api invite: can invite to a moderated guild", async t => { + let called = 0 + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "default", + guild_id: "112760669178241024" + }, + sessionData: { + managedGuilds: ["112760669178241024"] + }, + api: { + async getStateEvent(roomID, type, key) { + called++ + throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "Event not found or something"}) + }, + async inviteToRoom(roomID, mxidToInvite, mxid) { + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + called++ + }, + async setUserPowerCascade(roomID, mxid, power) { + t.equal(power, 0) + called++ + } + } + }) + ) + t.notOk(error) + t.equal(called, 3) +}) + +test("api invite: does not reinvite joined users", async t => { + let called = 0 + const [error] = await tryToCatch(() => + router.test("post", `/api/invite`, { + body: { + mxid: "@cadence:cadence.moe", + permissions: "default", + guild_id: "112760669178241024" + }, + sessionData: { + managedGuilds: ["112760669178241024"] + }, + api: { + async getStateEvent(roomID, type, key) { + called++ + return {membership: "join"} + }, + async setUserPowerCascade(roomID, mxid, power) { + t.equal(power, 0) + called++ + } + } + }) + ) + t.notOk(error) + t.equal(called, 2) +}) diff --git a/src/web/routes/qr.test.js b/src/web/routes/qr.test.js new file mode 100644 index 0000000..5b17f87 --- /dev/null +++ b/src/web/routes/qr.test.js @@ -0,0 +1,17 @@ +const {test} = require("supertape") +const {router} = require("../../../test/web") +const getStream = require("get-stream") + +test("web qr: returns svg", async t => { + /** @type {Response} */ + const res = await router.test("get", "/qr?data=hello+world", { + params: { + server_name: "cadence.moe", + media_id: "1" + } + }) + t.equal(res.status, 200) + t.equal(res.headers.get("content-type"), "image/svg+xml") + const content = await getStream(res.body) + t.match(content, / { + t.match(await router.test("get", "/", {}), /Add the bot to your Discord server./) +}) + +test("web server: can get htmx", async t => { + t.match(await router.test("get", "/static/htmx.min.js", {}), /htmx=/) +}) + +test("web server: can get css", async t => { + t.match(await router.test("get", "/static/stacks.min.css", {}), /--stacks-/) +}) + +test("web server: can get icon", async t => { + const content = await router.test("get", "/icon.png", {}) + t.ok(content instanceof Buffer) +}) + +test("web server: compresses static resources", async t => { + const content = await router.test("get", "/static/stacks.min.css", { + headers: { + "accept-encoding": "gzip" + } + }) + t.ok(content instanceof ReadableStream) +}) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index af7ea7b..38fed25 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -163,9 +163,13 @@ INSERT INTO member_power (mxid, room_id, power_level) VALUES INSERT INTO lottie (sticker_id, mxc_url) VALUES ('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR'); -INSERT INTO "auto_emoji" ("name","emoji_id","guild_id") VALUES -('L1','1144820033948762203','529176156398682115'), -('L2','1144820084079087647','529176156398682115'), -('_','_','529176156398682115'); +INSERT INTO auto_emoji (name, emoji_id, guild_id) VALUES +('L1', '1144820033948762203', '529176156398682115'), +('L2', '1144820084079087647', '529176156398682115'), +('_', '_', '529176156398682115'); + +INSERT INTO media_proxy (permitted_hash) VALUES +(-429802515645771439), +(4558604729745184757); COMMIT; diff --git a/test/test.js b/test/test.js index 5f20a80..9e595ae 100644 --- a/test/test.js +++ b/test/test.js @@ -20,9 +20,9 @@ const {reg} = require("../src/matrix/read-registration") reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby" reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" -reg.id = "baby" // don't actually take authenticated actions on the server -reg.as_token = "baby" -reg.hs_token = "baby" +reg.id = "baby" +reg.as_token = "don't actually take authenticated actions on the server" +reg.hs_token = "don't actually take authenticated actions on the server" reg.ooye.bridge_origin = "https://bridge.example.org" const sync = new HeatSync({watchFS: false}) @@ -31,6 +31,9 @@ const discord = { guilds: new Map([ [data.guild.general.id, data.guild.general] ]), + guildChannelMap: new Map([ + [data.guild.general.id, [data.channel.general.id]], + ]), application: { id: "684280192553844747" }, @@ -97,7 +100,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not ]) }, {timeout: 60000}) } - /* c8 ignore end */ + /* c8 ignore stop */ const p = migrate.migrate(db) test("migrate: migration works", async t => { @@ -142,4 +145,9 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/discord/interactions/permissions.test") require("../src/discord/interactions/privacy.test") require("../src/discord/interactions/reactions.test") + require("../src/web/server.test") + require("../src/web/routes/download-discord.test") + require("../src/web/routes/download-matrix.test") + require("../src/web/routes/guild.test") + require("../src/web/routes/qr.test") })() diff --git a/test/web.js b/test/web.js new file mode 100644 index 0000000..e22a97c --- /dev/null +++ b/test/web.js @@ -0,0 +1,69 @@ +const passthrough = require("../src/passthrough") +const h3 = require("h3") +const http = require("http") +const {SnowTransfer} = require("snowtransfer") + +class Router { + constructor() { + /** @type {Map} */ + this.routes = new Map() + for (const method of ["get", "post", "put", "patch", "delete"]) { + this[method] = function(url, handler) { + const key = `${method} ${url}` + this.routes.set(`${key}`, handler) + } + } + } + + /** + * @param {string} method + * @param {string} inputUrl + * @param {{event?: any, params?: any, body?: any, sessionData?: any, api?: Partial, snow?: {[k in keyof SnowTransfer]?: Partial}, headers?: any}} [options] + */ + test(method, inputUrl, options = {}) { + const url = new URL(inputUrl, "http://a") + const key = `${method} ${options.route || url.pathname}` + /* c8 ignore next */ + if (!this.routes.has(key)) throw new Error(`Route not found: "${key}"`) + + const req = { + method: method.toUpperCase(), + headers: options.headers || {}, + url + } + const event = options.event || {} + + if (typeof options.body === "object" && options.body.constructor === Object) { + options.body = JSON.stringify(options.body) + req.headers["content-type"] = "application/json" + } + + return this.routes.get(key)(Object.assign(event, { + method: method.toUpperCase(), + path: `${url.pathname}${url.search}`, + _requestBody: options.body, + node: { + req, + res: new http.ServerResponse(req) + }, + context: { + api: options.api, + params: options.params, + snow: options.snow, + sessions: { + h3: { + id: "h3", + createdAt: 0, + data: options.sessionData || {} + } + } + } + })) + } +} + +const router = new Router() + +passthrough.as = {router} + +module.exports.router = router From 75140a5b588ffbdd1503861c4fce08f0d826f13a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Dec 2024 01:16:02 +1300 Subject: [PATCH 168/346] Allow creating admins on web --- src/matrix/api.js | 9 +++++---- src/web/pug/guild.pug | 1 + src/web/pug/invite.pug | 1 + src/web/routes/guild.js | 7 +++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/matrix/api.js b/src/matrix/api.js index a75f959..532e326 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -301,14 +301,15 @@ async function setUserPower(roomID, mxid, power) { /** * Set a user's power level for a whole room hierarchy. - * @param {string} roomID + * @param {string} spaceID * @param {string} mxid * @param {number} power */ -async function setUserPowerCascade(roomID, mxid, power) { - assert(roomID[0] === "!") +async function setUserPowerCascade(spaceID, mxid, power) { + assert(spaceID[0] === "!") assert(mxid[0] === "@") - const rooms = await getFullHierarchy(roomID) + const rooms = await getFullHierarchy(spaceID) + await setUserPower(spaceID, mxid, power) for (const room of rooms) { await setUserPower(room.room_id, mxid, power) } diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 481ad01..d9ece95 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -64,6 +64,7 @@ block body select#permissions(name="permissions") option(value="default") Default option(value="moderator") Moderator + option(value="admin") Admin input(type="hidden" name="guild_id" value=guild_id) .grid--row-start2 button.s-btn.s-btn__filled.htmx-indicator Invite diff --git a/src/web/pug/invite.pug b/src/web/pug/invite.pug index 52b3ab2..7f7ff2e 100644 --- a/src/web/pug/invite.pug +++ b/src/web/pug/invite.pug @@ -27,6 +27,7 @@ block body select#permissions(name="permissions") option(value="default") Default option(value="moderator") Moderator + option(value="admin") Admin input(type="hidden" name="nonce" value=nonce) div button.s-btn.s-btn__filled.htmx-indicator Invite diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index fe24483..716314c 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -20,7 +20,7 @@ const schema = { }), invite: z.object({ mxid: z.string().regex(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/), - permissions: z.enum(["default", "moderator"]), + permissions: z.enum(["default", "moderator", "admin"]), guild_id: z.string().optional(), nonce: z.string().optional() }), @@ -152,7 +152,10 @@ as.router.post("/api/invite", defineEventHandler(async event => { } // Permissions - const powerLevel = parsedBody.permissions === "moderator" ? 50 : 0 + const powerLevel = + ( parsedBody.permissions === "admin" ? 100 + : parsedBody.permissions === "moderator" ? 50 + : 0) await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel) if (parsedBody.guild_id) { From 8a6b8ee32a40f89eb4f629abbd244a7b37900434 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Dec 2024 01:20:42 +1300 Subject: [PATCH 169/346] Allow creating admins from interaction menu --- src/discord/interactions/permissions.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/discord/interactions/permissions.js b/src/discord/interactions/permissions.js index fea9ce0..0a573e3 100644 --- a/src/discord/interactions/permissions.js +++ b/src/discord/interactions/permissions.js @@ -82,6 +82,10 @@ async function* _interact({data, guild_id}, {api}) { label: "Moderator", value: "moderator", default: userPower >= 50 && userPower < 100 + }, { + label: "Admin (you cannot undo this!)", + value: "admin", + default: userPower === 100 } ] } @@ -103,7 +107,10 @@ async function* _interactEdit({data, guild_id, message}, {api}) { assert(mxid) const permission = data.values[0] - const power = permission === "moderator" ? 50 : 0 + const power = + ( permission === "admin" ? 100 + : permission === "moderator" ? 50 + : 0) yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.UpdateMessage, From 20b575c5f7c6e74310ccd415c52ca2b30c82e961 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 24 Dec 2024 01:25:09 +1300 Subject: [PATCH 170/346] Mention PluralKit support on the readme --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index f88b6ea..40161d6 100644 --- a/readme.md +++ b/readme.md @@ -41,6 +41,7 @@ Most features you'd expect in both directions, plus a little extra spice: * Custom emoji list syncing * Custom emojis in messages * Custom room names/avatars can be applied on Matrix-side +* PluralKit members have persistent user accounts * Larger files from Discord are linked instead of reuploaded to Matrix (links don't expire) * Simulated user accounts are named @the_persons_username rather than @112233445566778899 From 0fe02dce22c3a8f9321a09af750e65bdc5620590 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 28 Dec 2024 20:01:27 +1300 Subject: [PATCH 171/346] Fix web page exploding for unlinked guilds Now it should at least show something, though features like invite won't work correctly. More work needed. --- src/web/routes/guild.js | 2 +- src/web/routes/guild.test.js | 27 +++++++++++++--- test/data.js | 63 ++++++++++++++++++++++++++++++++++++ test/test.js | 4 ++- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 716314c..6916d56 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -96,7 +96,7 @@ as.router.get("/guild", defineEventHandler(async event => { // Unlinked guild if (!row) { const links = getChannelRoomsLinks(guild_id, []) - return pugSync.render(event, "guild.pug", {guild_id, nonce, ...links}) + return pugSync.render(event, "guild.pug", {guild, guild_id, nonce, ...links}) } // Linked guild diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index b5b4080..f89dfee 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -33,15 +33,34 @@ test("web guild: access denied when guild id messed up", async t => { t.match(content, /the selected server doesn't exist/) }) - - - test("web invite: access denied with invalid nonce", async t => { const content = await router.test("get", "/invite?nonce=1") t.match(content, /This QR code has expired./) }) -test("web guild: can view guild", async t => { + + +test("web guild: can view unbridged guild", async t => { + const content = await router.test("get", "/guild?guild_id=66192955777486848", { + sessionData: { + managedGuilds: ["66192955777486848"] + }, + api: { + async getStateEvent(roomID, type, key) { + return {} + }, + async getMembers(roomID, membership) { + return {chunk: []} + }, + async getFullHierarchy(roomID) { + return [] + } + } + }) + t.match(content, / { const content = await router.test("get", "/guild?guild_id=112760669178241024", { sessionData: { managedGuilds: ["112760669178241024"] diff --git a/test/data.js b/test/data.js index 1c7983a..3133d27 100644 --- a/test/data.js +++ b/test/data.js @@ -189,6 +189,69 @@ module.exports = { max_stage_video_channel_users: 300, system_channel_flags: 0|0, safety_alerts_channel_id: null + }, + fna: { + application_id: null, + roles: [], + activity_instances: [], + banner: null, + stickers: [], + joined_at: "2020-04-25T07:36:09.644000+00:00", + default_message_notifications: 1, + afk_timeout: 60, + clan: null, + hub_type: null, + afk_channel_id: "216367750216548362", + discovery_splash: null, + splash: null, + explicit_content_filter: 0, + max_members: 500000, + premium_subscription_count: 0, + voice_states: [], + id: "66192955777486848", + premium_tier: 0, + name: "Function & Arg", + premium_progress_bar_enabled: false, + icon: "8bfeb3237cd8697d1d1cd5c626ca8cea", + large: true, + verification_level: 0, + public_updates_channel_id: null, + stage_instances: [], + rules_channel_id: null, + emojis: [], + owner_id: "66186356581208064", + threads: [], + max_stage_video_channel_users: 50, + description: null, + unavailable: false, + features: [ + "CHANNEL_ICON_EMOJIS_GENERATED", + "NEW_THREAD_PERMISSIONS", + "THREADS_ENABLED", + "SOUNDBOARD" + ], + latest_onboarding_question_id: null, + max_video_channel_users: 25, + home_header: null, + mfa_level: 0, + system_channel_id: null, + guild_scheduled_events: [], + nsfw_level: 0, + vanity_url_code: null, + member_count: 966, + presences: [], + application_command_counts: {}, + system_channel_flags: 0, + preferred_locale: "en-US", + region: "deprecated", + inventory_settings: null, + soundboard_sounds: [], + version: 1711491959939, + incidents_data: null, + embedded_activities: [], + nsfw: false, + safety_alerts_channel_id: null, + lazy: true } }, user: { diff --git a/test/test.js b/test/test.js index 9e595ae..241b7a1 100644 --- a/test/test.js +++ b/test/test.js @@ -29,10 +29,12 @@ const sync = new HeatSync({watchFS: false}) const discord = { guilds: new Map([ - [data.guild.general.id, data.guild.general] + [data.guild.general.id, data.guild.general], + [data.guild.fna.id, data.guild.fna], ]), guildChannelMap: new Map([ [data.guild.general.id, [data.channel.general.id]], + [data.guild.fna.id, []], ]), application: { id: "684280192553844747" From d7063916a58fdcbd0c57643f0db48e83d5fa1876 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 5 Jan 2025 19:02:55 +1300 Subject: [PATCH 172/346] During setup, echo any unrecognised requests --- scripts/setup.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 2782ad8..109c012 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -103,6 +103,13 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { return true } +function defineEchoHandler() { + return defineEventHandler(event => { + return "Out Of Your Element is listening.\n" + + `Received a ${event.method} request on path ${event.path}\n` + }) +} + ;(async () => { // create registration file with prompts... if (!reg) { @@ -127,7 +134,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { const serverOrigin = await serverOriginPrompt.run() const app = createApp() - app.use(defineEventHandler(() => "Out Of Your Element is listening.\n")) + app.use(defineEchoHandler()) const server = createServer(toNodeListener(app)) await server.listen(6693) @@ -147,7 +154,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { const res = await fetch(url) if (res.status !== 200) return `Server returned status code ${res.status}` const text = await res.text() - if (text !== "Out Of Your Element is listening.\n") return `Server does not point to OOYE` + if (!text.startsWith("Out Of Your Element is listening.")) return `Server does not point to OOYE` return true } catch (e) { return e.message @@ -241,6 +248,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) { passthrough.discord = discord const {as} = require("../src/matrix/appservice") + as.router.use("/**", defineEchoHandler()) console.log("⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)") let itWorks = false From 97043d90cc333224194ab52106e9dd3cec13d9fc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 5 Jan 2025 19:27:40 +1300 Subject: [PATCH 173/346] await it a bit further up the chain --- src/d2m/actions/create-space.js | 4 ++-- src/d2m/event-dispatcher.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index 04d1413..acd0d8f 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -235,8 +235,8 @@ async function syncSpaceExpressions(data, checkBeforeSync) { api.sendState(spaceID, "im.ponies.room_emotes", eventKey, content) } - update(spaceID, "emojis", "moe.cadence.ooye.pack.emojis", expression.emojisToState) - update(spaceID, "stickers", "moe.cadence.ooye.pack.stickers", expression.stickersToState) + await update(spaceID, "emojis", "moe.cadence.ooye.pack.emojis", expression.emojisToState) + await update(spaceID, "stickers", "moe.cadence.ooye.pack.stickers", expression.stickersToState) } module.exports.createSpace = createSpace diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c84d6a8..943e40e 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -179,7 +179,7 @@ module.exports = { */ async checkMissedExpressions(guild) { const data = {guild_id: guild.id, ...guild} - createSpace.syncSpaceExpressions(data, true) + await createSpace.syncSpaceExpressions(data, true) }, /** From 6411279efd151801be16145ce6447ad84b353b92 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 6 Jan 2025 15:31:34 +1300 Subject: [PATCH 174/346] Use relative paths on web --- package-lock.json | 12 +++++++++--- package.json | 1 + src/web/pug-sync.js | 5 +++-- src/web/pug/guild.pug | 2 +- src/web/pug/guild_access_denied.pug | 6 +++--- src/web/pug/home.pug | 4 ++-- src/web/pug/includes/template.pug | 12 ++++++------ 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1189af7..1852245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", + "get-relative-path": "^1.0.2", "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.5", @@ -1535,9 +1536,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1843,6 +1844,11 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-relative-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-relative-path/-/get-relative-path-1.0.2.tgz", + "integrity": "sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==" + }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", diff --git a/package.json b/package.json index e79a6b0..1705c02 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", + "get-relative-path": "^1.0.2", "get-stream": "^6.0.1", "h3": "^1.12.0", "heatsync": "^2.5.5", diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js index aa1a945..057fd0a 100644 --- a/src/web/pug-sync.js +++ b/src/web/pug-sync.js @@ -3,11 +3,11 @@ const assert = require("assert/strict") const fs = require("fs") const {join} = require("path") +const getRelativePath = require("get-relative-path") const h3 = require("h3") const {defineEventHandler, defaultContentType, setResponseStatus, useSession, getQuery} = h3 const {compileFile} = require("@cloudrac3r/pug") -const {as} = require("../passthrough") const {reg} = require("../matrix/read-registration") // Pug @@ -35,11 +35,12 @@ function render(event, filename, locals) { pugCache.set(path, async (event, locals) => { defaultContentType(event, "text/html; charset=utf-8") const session = await useSession(event, {password: reg.as_token}) + const rel = x => getRelativePath(event.path, x) return template(Object.assign({}, getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything globals, // Globals locals, // Explicit locals overwrite globals in case we need to DI something - {session} // Session is always session because it has to be trusted + {session, event, rel} // These are assigned last so they overwrite everything else. It would be catastrophically bad if they can't be trusted. )) }) /* c8 ignore start */ diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index d9ece95..9f12d38 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -73,7 +73,7 @@ block body let size = 105 let p = new URLSearchParams() p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) - img(width=size height=size src=`/qr?${p}`) + img(width=size height=size src=rel(`/qr?${p}`)) h2.mt48.fs-headline1 Moderation diff --git a/src/web/pug/guild_access_denied.pug b/src/web/pug/guild_access_denied.pug index 2305605..2b47e08 100644 --- a/src/web/pug/guild_access_denied.pug +++ b/src/web/pug/guild_access_denied.pug @@ -5,7 +5,7 @@ block body .s-empty-state.wmx4.p48 != icons.Spots.SpotEmptyXL p You need to log in to manage your servers. - a.s-btn.s-btn__icon.s-btn__filled(href="/oauth") + a.s-btn.s-btn__icon.s-btn__filled(href=rel("/oauth")) != icons.Icons.IconDiscord = ` Log in with Discord` @@ -13,10 +13,10 @@ block body .s-empty-state.wmx4.p48 != icons.Spots.SpotEmptyXL p Select a server from the top right corner to continue. - p If the server you're looking for isn't there, try #[a(href="/oauth?action=add") logging in again.] + p If the server you're looking for isn't there, try #[a(href=rel("/oauth?action=add")) logging in again.] else if !discord.guilds.has(guild_id) || !session.data.managedGuilds.includes(guild_id) .s-empty-state.wmx4.p48 != icons.Spots.SpotAlertXL p Either the selected server doesn't exist, or you don't have the Manage Server permission on Discord. - p If you've checked your permissions, try #[a(href="/oauth") logging in again.] + p If you've checked your permissions, try #[a(href=rel("/oauth")) logging in again.] diff --git a/src/web/pug/home.pug b/src/web/pug/home.pug index 6cd7d29..a906423 100644 --- a/src/web/pug/home.pug +++ b/src/web/pug/home.pug @@ -10,7 +10,7 @@ block body p Add the bot to your Discord server. p It will automatically create new Matrix rooms for you. .fl-grow1 - a.s-btn.s-btn__filled.s-btn__icon(href="/oauth?action=add") + a.s-btn.s-btn__filled.s-btn__icon(href=rel("/oauth?action=add")) != icons.Icons.IconPlus = ` Add to server` .s-card.bs-md.d-flex.fd-column @@ -19,6 +19,6 @@ block body p Choose this option if you already have a community set up on Matrix. p Or, choose this if you're migrating from a different bridge. .fl-grow1 - a.s-btn.s-btn__outlined.s-btn__icon(href="/oauth?action=add-self-service") + a.s-btn.s-btn__outlined.s-btn__icon(href=rel("/oauth?action=add-self-service")) != icons.Icons.IconUnorderedList = ` Set up self-service` diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 8345d76..6a0fb76 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -14,7 +14,7 @@ html(lang="en") head title Out Of Your Element - link(rel="stylesheet" type="text/css" href="/static/stacks.min.css") + link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css")) meta(name="htmx-config" content='{"indicatorClass":"is-loading"}') style. @@ -34,13 +34,13 @@ html(lang="en") header.s-topbar .s-topbar--skip-link(href="#content") Skip to main content .s-topbar--container.wmx9 - a.s-topbar--logo(href="/") - img.s-avatar.s-avatar__32(src="/icon.png") + a.s-topbar--logo(href=rel("/")) + img.s-avatar.s-avatar__32(src=rel("/icon.png")) nav.s-topbar--navigation ul.s-topbar--content li.ps-relative if !session.data.managedGuilds || session.data.managedGuilds.length === 0 - a.s-btn.s-btn__icon.as-center(href="/oauth") + a.s-btn.s-btn__icon.as-center(href=rel("/oauth")) != icons.Icons.IconDiscord = ` Log in` else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id) @@ -55,7 +55,7 @@ html(lang="en") ul.s-menu(role="menu") each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g) li(role="menuitem") - a.s-topbar--item.s-user-card.d-flex.p4(href=`/guild?guild_id=${guild.id}`) + a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`)) +guild(guild) .mx-auto.w100.wmx9.py24.px8.fs-body1#content block body @@ -68,4 +68,4 @@ html(lang="en") document.styleSheets[0].insertRule(t) }) }) - script(src="/static/htmx.min.js") + script(src=rel("/static/htmx.min.js")) From 16ac99781c509a98b266337f1e3ac509be14bbb6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 6 Jan 2025 17:19:33 +1300 Subject: [PATCH 175/346] Better feedback after interrupting/resuming setup --- scripts/setup.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 109c012..1fbbcff 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -230,11 +230,20 @@ function defineEchoHandler() { registration.reg = reg checkRegistration(reg) writeRegistration(reg) - console.log(`✅ Registration file saved as ${registrationFilePath}`) + console.log(`✅ Your responses have been saved as ${registrationFilePath}`) } else { - console.log(`✅ Valid registration file found at ${registrationFilePath}`) + try { + checkRegistration(reg) + console.log(`✅ Skipped questions - reusing data from ${registrationFilePath}`) + } catch (e) { + console.log(`❌ Failed to reuse data from ${registrationFilePath}`) + console.log("Consider deleting this file. You can re-run setup to safely make a new one.") + console.log("") + console.log(e.toString().replace(/^ *\n/gm, "")) + process.exit(1) + } } - console.log(` In ${cyan("Synapse")}, you need to add it to homeserver.yaml and ${cyan("restart Synapse")}.`) + console.log(` In ${cyan("Synapse")}, you need to reference that file in your homeserver.yaml and ${cyan("restart Synapse")}.`) console.log(" https://element-hq.github.io/synapse/latest/application_services.html") console.log(` In ${cyan("Conduit")}, you need to send the file contents to the #admins room.`) console.log(" https://docs.conduit.rs/appservices.html") @@ -249,7 +258,12 @@ function defineEchoHandler() { const {as} = require("../src/matrix/appservice") as.router.use("/**", defineEchoHandler()) - console.log("⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)") + + console.log("⏳ Waiting for you to register the file with your homeserver... (Ctrl+C to cancel)") + process.once("SIGINT", () => { + console.log("(Ctrl+C) Quit early. Please re-run setup later and allow it to complete.") + process.exit(1) + }) let itWorks = false let lastError = null From 84d61a11184e0542ff5659d13794ff81a9d85f94 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 6 Jan 2025 21:12:05 +1300 Subject: [PATCH 176/346] Use relative path for post-oauth redirect --- src/web/routes/oauth.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js index 82aebe4..12c991d 100644 --- a/src/web/routes/oauth.js +++ b/src/web/routes/oauth.js @@ -6,6 +6,7 @@ const {defineEventHandler, getValidatedQuery, sendRedirect, getQuery, useSession const {SnowTransfer} = require("snowtransfer") const DiscordTypes = require("discord-api-types/v10") const fetch = require("node-fetch") +const getRelativePath = require("get-relative-path") const {as, db} = require("../../passthrough") const {id} = require("../../../addbot") @@ -90,8 +91,8 @@ as.router.get("/oauth", defineEventHandler(async event => { } if (parsedQuery.data.guild_id) { - return sendRedirect(event, `/guild?guild_id=${parsedQuery.data.guild_id}`, 302) + return sendRedirect(event, getRelativePath(event.path, `/guild?guild_id=${parsedQuery.data.guild_id}`), 302) } - return sendRedirect(event, "/", 302) + return sendRedirect(event, getRelativePath(event.path, "/"), 302) })) From 4c62124cee94fa28d4c1dbddf4403beb09e04f07 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 6 Jan 2025 21:53:48 +1300 Subject: [PATCH 177/346] Improve invite QR generation --- src/web/pug/guild.pug | 6 +----- src/web/routes/guild.js | 12 ++++++++++-- src/web/routes/guild.test.js | 2 +- src/web/routes/qr.js | 19 ------------------- src/web/routes/qr.test.js | 17 ----------------- src/web/server.js | 1 - test/test.js | 1 - 7 files changed, 12 insertions(+), 46 deletions(-) delete mode 100644 src/web/routes/qr.js delete mode 100644 src/web/routes/qr.test.js diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 9f12d38..536ea4c 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -69,11 +69,7 @@ block body .grid--row-start2 button.s-btn.s-btn__filled.htmx-indicator Invite div - - - let size = 105 - let p = new URLSearchParams() - p.set("data", `https://bridge.cadence.moe/invite?nonce=${nonce}`) - img(width=size height=size src=rel(`/qr?${p}`)) + != svg h2.mt48.fs-headline1 Moderation diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 6916d56..5944131 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -6,6 +6,7 @@ const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getVa const {randomUUID} = require("crypto") const {LRUCache} = require("lru-cache") const Ty = require("../../types") +const uqr = require("uqr") const {discord, as, sync, select} = require("../../passthrough") /** @type {import("../pug-sync")} */ @@ -93,10 +94,17 @@ as.router.get("/guild", defineEventHandler(async event => { const nonce = randomUUID() validNonce.set(nonce, guild_id) + const data = `${reg.ooye.bridge_origin}/invite?nonce=${nonce}` + // necessary to scale the svg pixel-perfectly on the page + // https://github.com/unjs/uqr/blob/244952a8ba2d417f938071b61e11fb1ff95d6e75/src/svg.ts#L24 + const generatedSvg = uqr.renderSVG(data, {pixelSize: 3}) + const svg = generatedSvg.replace(/viewBox="0 0 ([0-9]+) ([0-9]+)"/, `data-nonce="${nonce}" width="$1" height="$2" $&`) + assert.notEqual(svg, generatedSvg) + // Unlinked guild if (!row) { const links = getChannelRoomsLinks(guild_id, []) - return pugSync.render(event, "guild.pug", {guild, guild_id, nonce, ...links}) + return pugSync.render(event, "guild.pug", {guild, guild_id, svg, ...links}) } // Linked guild @@ -105,7 +113,7 @@ as.router.get("/guild", defineEventHandler(async event => { const banned = await api.getMembers(row.space_id, "ban") const rooms = await api.getFullHierarchy(row.space_id) const links = getChannelRoomsLinks(guild_id, rooms) - return pugSync.render(event, "guild.pug", {guild, guild_id, nonce, mods, banned, ...links, ...row}) + return pugSync.render(event, "guild.pug", {guild, guild_id, svg, mods, banned, ...links, ...row}) })) as.router.get("/invite", defineEventHandler(async event => { diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index f89dfee..3952d14 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -78,7 +78,7 @@ test("web guild: can view bridged guild", async t => { } }) t.match(content, / { - const {data} = await getValidatedQuery(event, schema.qr.parse) - return new Response(uqr.renderSVG(data, {pixelSize: 3}), {headers: {"content-type": "image/svg+xml"}}) -})) diff --git a/src/web/routes/qr.test.js b/src/web/routes/qr.test.js deleted file mode 100644 index 5b17f87..0000000 --- a/src/web/routes/qr.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const {test} = require("supertape") -const {router} = require("../../../test/web") -const getStream = require("get-stream") - -test("web qr: returns svg", async t => { - /** @type {Response} */ - const res = await router.test("get", "/qr?data=hello+world", { - params: { - server_name: "cadence.moe", - media_id: "1" - } - }) - t.equal(res.status, 200) - t.equal(res.headers.get("content-type"), "image/svg+xml") - const content = await getStream(res.body) - t.match(content, / Date: Tue, 7 Jan 2025 12:23:39 +1300 Subject: [PATCH 178/346] Sync pins back from Matrix to Discord --- src/m2d/actions/update-pins.js | 25 ++++++++++++++++++++++++ src/m2d/converters/diff-pins.js | 17 ++++++++++++++++ src/m2d/converters/diff-pins.test.js | 11 +++++++++++ src/m2d/event-dispatcher.js | 29 ++++++++++++++++++++++++++++ src/types.d.ts | 4 ++++ test/test.js | 1 + 6 files changed, 87 insertions(+) create mode 100644 src/m2d/actions/update-pins.js create mode 100644 src/m2d/converters/diff-pins.js create mode 100644 src/m2d/converters/diff-pins.test.js diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js new file mode 100644 index 0000000..976e496 --- /dev/null +++ b/src/m2d/actions/update-pins.js @@ -0,0 +1,25 @@ +// @ts-check + +const {sync, from, discord} = require("../../passthrough") + +/** @type {import("../converters/diff-pins")} */ +const diffPins = sync.require("../converters/diff-pins") + +/** + * @param {string[]} pins + * @param {string[]} prev + */ +async function updatePins(pins, prev) { + const diff = diffPins.diffPins(pins, prev) + for (const [event_id, added] of diff) { + const row = from("event_message").join("message_channel", "message_id").where({event_id}).select("channel_id", "message_id").get() + if (!row) continue + if (added) { + discord.snow.channel.addChannelPinnedMessage(row.channel_id, row.message_id, "Message pinned on Matrix") + } else { + discord.snow.channel.removeChannelPinnedMessage(row.channel_id, row.message_id, "Message unpinned on Matrix") + } + } +} + +module.exports.updatePins = updatePins diff --git a/src/m2d/converters/diff-pins.js b/src/m2d/converters/diff-pins.js new file mode 100644 index 0000000..e6e038b --- /dev/null +++ b/src/m2d/converters/diff-pins.js @@ -0,0 +1,17 @@ +// @ts-check + +/** + * @param {string[]} pins + * @param {string[]} prev + * @returns {[string, boolean][]} + */ +function diffPins(pins, prev) { + /** @type {[string, boolean][]} */ + const result = [] + return result.concat( + prev.filter(id => !pins.includes(id)).map(id => [id, false]), // removed + pins.filter(id => !prev.includes(id)).map(id => [id, true]) // added + ) +} + +module.exports.diffPins = diffPins diff --git a/src/m2d/converters/diff-pins.test.js b/src/m2d/converters/diff-pins.test.js new file mode 100644 index 0000000..edb9c47 --- /dev/null +++ b/src/m2d/converters/diff-pins.test.js @@ -0,0 +1,11 @@ +// @ts-check + +const {test} = require("supertape") +const diffPins = require("./diff-pins") + +test("diff pins: diff is as expected", t => { + t.deepEqual( + diffPins.diffPins(["same", "new"], ["same", "old"]), + [["old", false], ["new", true]] + ) +}) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 301dcc4..d5d6ed3 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -14,6 +14,8 @@ const sendEvent = sync.require("./actions/send-event") const addReaction = sync.require("./actions/add-reaction") /** @type {import("./actions/redact")} */ const redact = sync.require("./actions/redact") +/** @type {import("./actions/update-pins")}) */ +const updatePins = sync.require("./actions/update-pins") /** @type {import("../matrix/matrix-command-handler")} */ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") /** @type {import("./converters/utils")} */ @@ -159,6 +161,33 @@ async event => { db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id) })) +sync.addTemporaryListener(as, "type:m.room.pinned_events", guard("m.room.pinned_events", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + const pins = event.content.pinned + if (!Array.isArray(pins)) return + let prev = event.unsigned?.prev_content?.pinned + if (!Array.isArray(prev)) { + if (pins.length === 1) { + /* + In edge cases, prev_content isn't guaranteed to be provided by the server. + If prev_content is missing, we can't diff. Better safe than sorry: we'd like to ignore the change rather than wiping the whole channel's pins on Discord. + However, that would mean if the first ever pin came from Matrix-side, it would be ignored, because there would be no prev_content (it's the first pinned event!) + So to handle that edge case, we assume that if there's exactly 1 entry in `pinned`, this is the first ever pin and it should go through. + */ + prev = [] + } else { + return + } + } + + await updatePins.updatePins(pins, prev) +})) + sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", /** * @param {Ty.Event.StateOuter} event diff --git a/src/types.d.ts b/src/types.d.ts index 178a560..b99046b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -241,6 +241,10 @@ export namespace Event { name?: string } + export type M_Room_PinnedEvents = { + pinned: string[] + } + export type M_Power_Levels = { /** The level required to ban a user. Defaults to 50 if unspecified. */ ban?: number, diff --git a/test/test.js b/test/test.js index 089a0dd..591b64b 100644 --- a/test/test.js +++ b/test/test.js @@ -139,6 +139,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/converters/remove-reaction.test") require("../src/d2m/converters/thread-to-announcement.test") require("../src/d2m/converters/user-to-mxid.test") + require("../src/m2d/converters/diff-pins.test") require("../src/m2d/converters/event-to-message.test") require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test") From dcb7dda6f121bd733e7417d4597747a6f624691e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 7 Jan 2025 15:04:43 +1300 Subject: [PATCH 179/346] Setup now checks for privileged intents --- scripts/setup.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 1fbbcff..3491106 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -15,6 +15,7 @@ const fetch = require("node-fetch").default const {magenta, bold, cyan} = require("ansi-colors") const HeatSync = require("heatsync") const {SnowTransfer} = require("snowtransfer") +const DiscordTypes = require("discord-api-types/v10") const {createApp, defineEventHandler, toNodeListener} = require("h3") const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]}) @@ -166,9 +167,10 @@ function defineEchoHandler() { await server.close() console.log("What is your Discord bot token?") + console.log("Go to https://discord.com/developers, create or pick an app, go to the Bot section, and reset the token.") /** @type {SnowTransfer} */ // @ts-ignore let snow = null - /** @type {{id: string, redirect_uris: string[]}} */ // @ts-ignore + /** @type {{id: string, flags: number, redirect_uris: string[]}} */ // @ts-ignore let client = null /** @type {{discord_token: string}} */ const discordTokenResponse = await prompt({ @@ -187,8 +189,27 @@ function defineEchoHandler() { } }) + const mandatoryIntentFlags = DiscordTypes.ApplicationFlags.GatewayMessageContent | DiscordTypes.ApplicationFlags.GatewayMessageContentLimited + if (!(client.flags & mandatoryIntentFlags)) { + console.log(`On that same page, scroll down to Privileged Gateway Intents and enable all switches.`) + await prompt({ + type: "invisible", + name: "intents", + message: "Press Enter when you've enabled them", + validate: async token => { + process.stdout.write(magenta("checking, please wait...")) + client = await snow.requestHandler.request(`/applications/@me`, {}, "get") + if (client.flags & mandatoryIntentFlags) { + return true + } else { + return "Switches have not been enabled yet" + } + } + }) + } + console.log("What is your Discord client secret?") - console.log(`You can find it on the application page: https://discord.com/developers/applications/${client.id}/oauth2`) + console.log(`You can find it in the application's OAuth2 section: https://discord.com/developers/applications/${client.id}/oauth2`) /** @type {{discord_client_secret: string}} */ const clientSecretResponse = await prompt({ type: "input", @@ -198,7 +219,7 @@ function defineEchoHandler() { const expectedUri = `${bridgeOriginResponse.bridge_origin}/oauth` if (!client.redirect_uris.includes(expectedUri)) { - console.log(`On the same application page, go to the Redirects section, and add this URI: ${cyan(expectedUri)}`) + console.log(`On that same page, scroll down to Redirects and add this URI: ${cyan(expectedUri)}`) await prompt({ type: "invisible", name: "redirect_uri", From 2009e23689cddaa58aa9f556747ea109700d8f25 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 01:01:32 +1300 Subject: [PATCH 180/346] Docs: Why does the bridge have a website? --- docs/why-does-the-bridge-have-a-website.md | 103 +++++++++++++++++++++ readme.md | 1 + 2 files changed, 104 insertions(+) create mode 100644 docs/why-does-the-bridge-have-a-website.md diff --git a/docs/why-does-the-bridge-have-a-website.md b/docs/why-does-the-bridge-have-a-website.md new file mode 100644 index 0000000..c60b62c --- /dev/null +++ b/docs/why-does-the-bridge-have-a-website.md @@ -0,0 +1,103 @@ +# Why does the bridge have a website? + +## It's essential for making images work + +Matrix has a feature called [Authenticated Media](https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/), where uploaded media (like user avatars and uploaded files) is restricted to Matrix users only. This means Discord users wouldn't be able to see important parts of the conversation. + +To keep things working for Discord users, OOYE's web server can act as a proxy files that were uploaded on Matrix-side. This will automatically take effect when needed, so Discord users shouldn't notice any issues. + +## Why now? + +I knew a web interface had a lot of potential, but I was reluctant to add one because it would make the initial setup more complicated. However, when authenticated media forced my hand, I saw an opportunity to introduce new useful features. Hopefully you'll agree that it's worth it! + +# What else does it do? + +## Makes it easy to invite the bot + +The home page of the website has buttons to add the bridge bot to a Discord server. If you are primarly a Matrix user and you want somebody else (who may be less technical) to add the bot to their own Discord server, this should make it a lot more intuitive for them. + +## Makes it easy to invite yourself or others + +After your hypothetical less-technical friend adds the bot to their Discord server, you need to generate an invite for your Matrix account on Matrix-side. Without the website, you might need to guide them through running a /invite command with your user ID. With the website, they don't have to do anything extra. You can use your phone to scan the QR code on their screen, which lets you invite your user ID in your own time. + +You can also set the person's permissions when you invite them, so you can easily bootstrap the Matrix side with your trusted ones as moderators. + +## To link channels and rooms + +Without a website, to link Discord channels to existing Matrix rooms, you'd need to run a /link command with the internal IDs of each room. This is tedious and error-prone, especially if you want to set up a lot of channels. With the web interface, you can see a list of all the available rooms and click them to link them together. + +## To change settings + +Important settings, like whether the Matrix rooms should be private or publicly accessible, can be configured by clicking buttons rather than memorising commands. Changes take effect immediately. + +# Permissions + +## Bot invites + +Anybody who can access the home page can use the buttons to add your bot - but even without the website, they can already do this by manually constructing a URL. If you want to make it so _only you_ can add your bot to servers, you need to edit [your Discord application](https://discord.com/developers/applications), go to the Bot section, and turn off the switch that says Public Bot. + +## Server settings + +If you have either the Administrator or Manage Server permissions in a Discord server, you can use the website to manage that server, including linking channels and changing settings. + +# Initial setup + +The website is built in to OOYE and is always running as part of the bridge. For authenticated media proxy to work, you'll need to make the web server accessible on the public internet over HTTPS, presumably using a reverse proxy. + +When you use `npm run setup` as part of OOYE's initial setup, it will guide you through this process, and it will do a thorough self-test to make sure it's configured correctly. If you get stuck or want a configuration template, check the notes below. + +## Reverse proxy + +When OOYE is running, the web server runs on port 6693. (To use a different port or a UNIX socket, edit registration.yaml's `socket` setting and restart.) + +It doesn't have to have its own dedicated domain name, you can also use a sub-path on an existing domain, like the domain of your Matrix homeserver. See the end of this document for more information. + +You are likely already using a reverse proxy for running your homeserver, so this should just be a configuration change. + +## Example configuration for nginx, dedicated domain name + +Replace `bridge.cadence.moe` with the hostname you're using. + +```nix +server { + listen 80; + listen [::]:80; + server_name bridge.cadence.moe; + + return 301 https://bridge.cadence.moe$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name bridge.cadence.moe; + + # ssl parameters here... + client_max_body_size 5M; + + location / { + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + proxy_pass http://127.0.0.1:6693; + } +} +``` + +## Example configuration for nginx, sharing a domain name + +Same as above, but change the following: + +- `location / {` -> `location /ooye/ {` (any sub-path you want; you MUST use a trailing slash or it won't work) +- `proxy_pass http://127.0.0.1:6693;` -> `proxy_pass http://127.0.0.1:6693/;` (you MUST use a trailing slash on this too or it won't work) + +## Example configuration for Caddy, dedicated domain name + +```nix +bridge.cadence.moe { + log { + output file /var/log/caddy/access.log + format console + } + encode gzip + reverse_proxy 127.0.0.1:6693 +} +``` diff --git a/readme.md b/readme.md index 40161d6..3770f9c 100644 --- a/readme.md +++ b/readme.md @@ -72,6 +72,7 @@ You'll need: * Administrative access to a homeserver * Discord bot +* Domain name for the bridge's website ([more info](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/why-does-the-bridge-have-a-website.md) Follow these steps: From 3e5034cff585dd6f133dd25fc112d7f96b13a3af Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 01:07:46 +1300 Subject: [PATCH 181/346] Remove read receipts visibility client hint This is a failed experiment that is long past its time. It needs to go. --- src/d2m/actions/create-room.js | 3 --- test/data.js | 1 - 2 files changed, 4 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index ece8ef7..e99fc93 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -162,9 +162,6 @@ async function channelToKState(channel, guild, di) { }, users: {...spacePower, ...globalAdminPower} }, - "chat.schildi.hide_ui/read_receipts": { - hidden: true - }, [`uk.half-shot.bridge/moe.cadence.ooye://discord/${guild.id}/${channel.id}`]: { bridgebot: `@${reg.sender_localpart}:${reg.ooye.server_name}`, protocol: { diff --git a/test/data.js b/test/data.js index 3133d27..75a961c 100644 --- a/test/data.js +++ b/test/data.js @@ -48,7 +48,6 @@ module.exports = { room: 0 } }, - "chat.schildi.hide_ui/read_receipts": {hidden: true}, "uk.half-shot.bridge/moe.cadence.ooye://discord/112760669178241024/112760669178241024": { bridgebot: "@_ooye_bot:cadence.moe", protocol: { From 93cacba2833294d172615752968ee284227fd150 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 01:25:17 +1300 Subject: [PATCH 182/346] Make sure client hint change applies Will eventually remove it fully in v4. --- src/d2m/actions/create-room.js | 2 ++ test/data.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index e99fc93..8f8ac00 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -162,6 +162,8 @@ async function channelToKState(channel, guild, di) { }, users: {...spacePower, ...globalAdminPower} }, + "chat.schildi.hide_ui/read_receipts": { + }, [`uk.half-shot.bridge/moe.cadence.ooye://discord/${guild.id}/${channel.id}`]: { bridgebot: `@${reg.sender_localpart}:${reg.ooye.server_name}`, protocol: { diff --git a/test/data.js b/test/data.js index 75a961c..b92ae1b 100644 --- a/test/data.js +++ b/test/data.js @@ -48,6 +48,7 @@ module.exports = { room: 0 } }, + "chat.schildi.hide_ui/read_receipts": {}, "uk.half-shot.bridge/moe.cadence.ooye://discord/112760669178241024/112760669178241024": { bridgebot: "@_ooye_bot:cadence.moe", protocol: { From 7e6548eb90851795a3abd663a5faf346dde08eb7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 01:31:31 +1300 Subject: [PATCH 183/346] Ack bridged Matrix events May provide reassurance that the bridge is currently working. Half-Shot's bridge has always done this. --- src/m2d/event-dispatcher.js | 3 +++ src/matrix/api.js | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index d5d6ed3..c7c725e 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -104,6 +104,7 @@ async event => { // @ts-ignore await matrixCommandHandler.execute(event) } + await api.ackEvent(event) })) sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker", @@ -113,6 +114,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker", async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) + await api.ackEvent(event) })) sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction", @@ -137,6 +139,7 @@ sync.addTemporaryListener(as, "type:m.room.redaction", guard("m.room.redaction", async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return await redact.handle(event) + await api.ackEvent(event) })) sync.addTemporaryListener(as, "type:m.room.avatar", guard("m.room.avatar", diff --git a/src/matrix/api.js b/src/matrix/api.js index 532e326..abf264e 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -347,6 +347,26 @@ function getMedia(mxc, init = {}) { }) } +/** + * Updates the m.read receipt in roomID to point to eventID. + * This doesn't modify m.fully_read, which matches [the behaviour of matrix-bot-sdk.](https://github.com/element-hq/matrix-bot-sdk/blob/e72a4c498e00c6c339a791630c45d00a351f56a8/src/MatrixClient.ts#L1227) + * @param {string} roomID + * @param {string} eventID + * @param {string?} [mxid] + */ +async function sendReadReceipt(roomID, eventID, mxid) { + await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/receipt/m.read/${eventID}`, mxid), {}) +} + +/** + * Acknowledge an event as read by calling api.sendReadReceipt on it. + * @param {Ty.Event.Outer} event + * @param {string?} [mxid] + */ +async function ackEvent(event, mxid) { + await sendReadReceipt(event.room_id, event.event_id, mxid) +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -373,3 +393,5 @@ module.exports.setUserPower = setUserPower module.exports.setUserPowerCascade = setUserPowerCascade module.exports.ping = ping module.exports.getMedia = getMedia +module.exports.sendReadReceipt = sendReadReceipt +module.exports.ackEvent = ackEvent From fb18c0fe0b311baea311fb373a8921b14ed5b26a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 02:05:28 +1300 Subject: [PATCH 184/346] Ensure 1 pin = 1 pin even when message is split --- src/m2d/actions/update-pins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js index 976e496..46074c6 100644 --- a/src/m2d/actions/update-pins.js +++ b/src/m2d/actions/update-pins.js @@ -12,7 +12,7 @@ const diffPins = sync.require("../converters/diff-pins") async function updatePins(pins, prev) { const diff = diffPins.diffPins(pins, prev) for (const [event_id, added] of diff) { - const row = from("event_message").join("message_channel", "message_id").where({event_id}).select("channel_id", "message_id").get() + const row = from("event_message").join("message_channel", "message_id").where({event_id, part: 0}).select("channel_id", "message_id").get() if (!row) continue if (added) { discord.snow.channel.addChannelPinnedMessage(row.channel_id, row.message_id, "Message pinned on Matrix") From 0c1b4c5e8e10a69c9198344d9d44c158458dc1e5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 09:37:30 +1300 Subject: [PATCH 185/346] Remove unhelpful guard preventing d->m pin syncing --- src/d2m/actions/update-pins.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/d2m/actions/update-pins.js b/src/d2m/actions/update-pins.js index 5d98501..bce3f88 100644 --- a/src/d2m/actions/update-pins.js +++ b/src/d2m/actions/update-pins.js @@ -25,11 +25,9 @@ function convertTimestamp(timestamp) { async function updatePins(channelID, roomID, convertedTimestamp) { const pins = await discord.snow.channel.getChannelPinnedMessages(channelID) const eventIDs = pinsToList.pinsToList(pins) - if (pins.length === eventIDs.length || eventIDs.length) { - await api.sendState(roomID, "m.room.pinned_events", "", { - pinned: eventIDs - }) - } + await api.sendState(roomID, "m.room.pinned_events", "", { + pinned: eventIDs + }) db.prepare("UPDATE channel_room SET last_bridged_pin_timestamp = ? WHERE channel_id = ?").run(convertedTimestamp || 0, channelID) } From 6e55061760e2b297fef2cd10a7ff6fa106ef61d6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 11:31:43 +1300 Subject: [PATCH 186/346] Use kstate for d->m pins updates --- src/d2m/actions/create-room.js | 34 ++++++--------------------------- src/d2m/actions/create-space.js | 8 ++++---- src/d2m/actions/update-pins.js | 14 +++++++++----- src/m2d/event-dispatcher.js | 1 + src/matrix/api.js | 2 +- src/matrix/kstate.js | 26 +++++++++++++++++++++++++ src/matrix/power.js | 5 ++--- src/web/pug/guild.pug | 2 +- 8 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 8f8ac00..eeba7aa 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -39,26 +39,6 @@ const DEFAULT_PRIVACY_LEVEL = 0 /** @type {Map>} channel ID -> Promise */ const inflightRoomCreate = new Map() -/** - * Async because it gets all room state from the homeserver. - * @param {string} roomID - */ -async function roomToKState(roomID) { - const root = await api.getAllState(roomID) - return ks.stateToKState(root) -} - -/** - * @param {string} roomID - * @param {any} kstate - */ -async function applyKStateDiffToRoom(roomID, kstate) { - const events = await ks.kstateToState(kstate) - return Promise.all(events.map(({type, state_key, content}) => - api.sendState(roomID, type, state_key, content) - )) -} - /** * @param {{id: string, name: string, topic?: string?, type: number, parent_id?: string?}} channel * @param {{id: string}} guild @@ -253,9 +233,9 @@ async function postApplyPowerLevels(kstate, callback) { // Now *really* apply the power level overrides on top of what Synapse *really* set if (powerLevelContent) { - const newRoomKState = await roomToKState(roomID) + const newRoomKState = await ks.roomToKState(roomID) const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent}) - await applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff) + await ks.applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff) } return roomID @@ -384,7 +364,7 @@ async function _syncRoom(channelID, shouldActuallySync) { const {spaceID, channelKState} = await channelToKState(channel, guild, {api}) // calling this in both branches because we don't want to calculate this if not syncing // sync channel state to room - const roomKState = await roomToKState(roomID) + const roomKState = await ks.roomToKState(roomID) if (+roomKState["m.room.create/"].room_version <= 8) { // join_rule `restricted` is not available in room version < 8 and not working properly in version == 8 // read more: https://spec.matrix.org/v1.8/rooms/v9/ @@ -392,7 +372,7 @@ async function _syncRoom(channelID, shouldActuallySync) { channelKState["m.room.join_rules/"] = {join_rule: "public"} } const roomDiff = ks.diffKState(roomKState, channelKState) - const roomApply = applyKStateDiffToRoom(roomID, roomDiff) + const roomApply = ks.applyKStateDiffToRoom(roomID, roomDiff) db.prepare("UPDATE channel_room SET name = ? WHERE room_id = ?").run(channel.name, roomID) // sync room as space member @@ -462,7 +442,7 @@ async function unbridgeDeletedChannel(channel, guildID) { * @returns {Promise} */ async function _syncSpaceMember(channel, spaceID, roomID) { - const spaceKState = await roomToKState(spaceID) + const spaceKState = await ks.roomToKState(spaceID) let spaceEventContent = {} if ( channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join) @@ -475,7 +455,7 @@ async function _syncSpaceMember(channel, spaceID, roomID) { const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${roomID}`]: spaceEventContent }) - return applyKStateDiffToRoom(spaceID, spaceDiff) + return ks.applyKStateDiffToRoom(spaceID, spaceDiff) } async function createAllForGuild(guildID) { @@ -498,8 +478,6 @@ module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState -module.exports.roomToKState = roomToKState -module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index acd0d8f..edd3ba3 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -116,9 +116,9 @@ async function _syncSpace(guild, shouldActuallySync) { const guildKState = await guildToKState(guild, privacy_level) // calling this in both branches because we don't want to calculate this if not syncing // sync guild state to space - const spaceKState = await createRoom.roomToKState(spaceID) + const spaceKState = await ks.roomToKState(spaceID) const spaceDiff = ks.diffKState(spaceKState, guildKState) - await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) + await ks.applyKStateDiffToRoom(spaceID, spaceDiff) // guild icon was changed, so room avatars need to be updated as well as the space ones // doing it this way rather than calling syncRoom for great efficiency gains @@ -183,9 +183,9 @@ async function syncSpaceFully(guildID) { const guildKState = await guildToKState(guild, privacy_level) // sync guild state to space - const spaceKState = await createRoom.roomToKState(spaceID) + const spaceKState = await ks.roomToKState(spaceID) const spaceDiff = ks.diffKState(spaceKState, guildKState) - await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) + await ks.applyKStateDiffToRoom(spaceID, spaceDiff) const childRooms = await api.getFullHierarchy(spaceID) diff --git a/src/d2m/actions/update-pins.js b/src/d2m/actions/update-pins.js index bce3f88..692081a 100644 --- a/src/d2m/actions/update-pins.js +++ b/src/d2m/actions/update-pins.js @@ -6,6 +6,8 @@ const {discord, sync, db} = passthrough const pinsToList = sync.require("../converters/pins-to-list") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/kstate")} */ +const ks = sync.require("../../matrix/kstate") /** * @template {string | null | undefined} T @@ -23,11 +25,13 @@ function convertTimestamp(timestamp) { * @param {number?} convertedTimestamp */ async function updatePins(channelID, roomID, convertedTimestamp) { - const pins = await discord.snow.channel.getChannelPinnedMessages(channelID) - const eventIDs = pinsToList.pinsToList(pins) - await api.sendState(roomID, "m.room.pinned_events", "", { - pinned: eventIDs - }) + const discordPins = await discord.snow.channel.getChannelPinnedMessages(channelID) + const pinned = pinsToList.pinsToList(discordPins) + + const kstate = await ks.roomToKState(roomID) + const diff = ks.diffKState(kstate, {"m.room.pinned_events/": {pinned}}) + await ks.applyKStateDiffToRoom(roomID, diff) + db.prepare("UPDATE channel_room SET last_bridged_pin_timestamp = ? WHERE channel_id = ?").run(convertedTimestamp || 0, channelID) } diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index c7c725e..a270293 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -189,6 +189,7 @@ async event => { } await updatePins.updatePins(pins, prev) + await api.ackEvent(event) })) sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", diff --git a/src/matrix/api.js b/src/matrix/api.js index abf264e..f9d3e08 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -6,7 +6,7 @@ const assert = require("assert").strict const fetch = require("node-fetch").default const passthrough = require("../passthrough") -const { discord, sync, db } = passthrough +const {sync} = passthrough /** @type {import("./mreq")} */ const mreq = sync.require("./mreq") /** @type {import("./txnid")} */ diff --git a/src/matrix/kstate.js b/src/matrix/kstate.js index 67bb063..11155f5 100644 --- a/src/matrix/kstate.js +++ b/src/matrix/kstate.js @@ -8,6 +8,8 @@ const passthrough = require("../passthrough") const {sync} = passthrough /** @type {import("./file")} */ const file = sync.require("./file") +/** @type {import("./api")} */ +const api = sync.require("./api") /** Mutates the input. Not recursive - can only include or exclude entire state events. */ function kstateStripConditionals(kstate) { @@ -102,8 +104,32 @@ function diffKState(actual, target) { return diff } +/* c8 ignore start */ + +/** + * Async because it gets all room state from the homeserver. + * @param {string} roomID + */ +async function roomToKState(roomID) { + const root = await api.getAllState(roomID) + return stateToKState(root) +} + +/** + * @param {string} roomID + * @param {any} kstate + */ +async function applyKStateDiffToRoom(roomID, kstate) { + const events = await kstateToState(kstate) + return Promise.all(events.map(({type, state_key, content}) => + api.sendState(roomID, type, state_key, content) + )) +} + module.exports.kstateStripConditionals = kstateStripConditionals module.exports.kstateUploadMxc = kstateUploadMxc module.exports.kstateToState = kstateToState module.exports.stateToKState = stateToKState module.exports.diffKState = diffKState +module.exports.roomToKState = roomToKState +module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom diff --git a/src/matrix/power.js b/src/matrix/power.js index 3e613dd..d323d17 100644 --- a/src/matrix/power.js +++ b/src/matrix/power.js @@ -3,7 +3,6 @@ const {db, from} = require("../passthrough") const {reg} = require("./read-registration") const ks = require("./kstate") -const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") /** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */ function _getAffectedRooms() { @@ -23,9 +22,9 @@ async function applyPower() { const rows = _getAffectedRooms() for (const row of rows) { - const kstate = await roomToKState(row.room_id) + const kstate = await ks.roomToKState(row.room_id) const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}}) - await applyKStateDiffToRoom(row.room_id, diff) + await ks.applyKStateDiffToRoom(row.room_id, diff) // There is a listener on m.room.power_levels to do this same update, // but we update it here anyway since the homeserver does not always deliver the event round-trip. db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 536ea4c..ab61430 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -11,7 +11,7 @@ mixin badge-private | Private mixin discord(channel, radio=false) - - let permissions = dUtils.getPermissions([], discord.guilds.get(channel.guild_id).roles, null, channel.permission_overwrites) + - let permissions = dUtils.getPermissions([], guild.roles, null, channel.permission_overwrites) .s-user-card.s-user-card__small if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) != icons.Icons.IconLock From ad1aa2c0f663f0936bb95e123cf5ccb0369d808a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 13:56:59 +1300 Subject: [PATCH 187/346] Resolve Matrix room aliases to Discord channels --- src/m2d/converters/event-to-message.js | 14 ++- src/m2d/converters/event-to-message.test.js | 127 ++++++++++++++++++++ src/matrix/api.js | 11 ++ src/types.d.ts | 5 + 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index ebbe9e1..b52717d 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -330,9 +330,9 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, */ async function handleRoomOrMessageLinks(input, di) { let offset = 0 - for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/(![^"/, ?)]+)(?:\/(\$[^"/ ?)]+))?(?:\?[^",:!? )]*?)?)(">|[,<\n )]|$)/g)]) { + for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/((?:#|%23|!)[^"/, ?)]+)(?:\/(\$[^"/ ?)]+))?(?:\?[^",:!? )]*?)?)(">|[,<\n )]|$)/g)]) { assert(typeof match.index === "number") - const [_, attributeValue, roomID, eventID, endMarker] = match + let [_, attributeValue, roomID, eventID, endMarker] = match let result const resultType = endMarker === '">' ? "html" : "plain" @@ -350,6 +350,16 @@ async function handleRoomOrMessageLinks(input, di) { // Don't process links that are part of the reply fallback, they'll be removed entirely by turndown if (input.slice(match.index + match[0].length + offset).startsWith("In reply to")) continue + // Resolve room alias to room ID if needed + roomID = decodeURIComponent(roomID) + if (roomID[0] === "#") { + try { + roomID = await di.api.getAlias(roomID) + } catch (e) { + continue // Room alias is unresolvable, so it can't be converted + } + } + const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get() if (!channelID) continue if (!eventID) { diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index a97fd26..145e9ec 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -2957,6 +2957,133 @@ test("event2message: mentioning bridged rooms works (plaintext body)", async t = ) }) +test("event2message: mentioning bridged rooms by alias works", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `I'm just worm-farm testing channel mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }, {}, { + api: { + async getAlias(alias) { + called++ + t.equal(alias, "#worm-farm:cadence.moe") + return "!BnKuBPCvyfOkhcUjEu:cadence.moe" + } + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "I'm just <#1100319550446252084> testing channel mentions", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) + t.equal(called, 1) +}) + +test("event2message: mentioning bridged rooms by alias works (plaintext body)", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: `I'm just https://matrix.to/#/#worm-farm:cadence.moe?via=cadence.moe testing channel mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }, {}, { + api: { + async getAlias(alias) { + called++ + t.equal(alias, "#worm-farm:cadence.moe") + return "!BnKuBPCvyfOkhcUjEu:cadence.moe" + } + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "I'm just <#1100319550446252084> testing channel mentions", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) + t.equal(called, 1) +}) + +test("event2message: mentioning bridged rooms by alias skips the link when alias is unresolvable", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: `I'm just https://matrix.to/#/#worm-farm:cadence.moe?via=cadence.moe and https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe?via=cadence.moe testing channel mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }, {}, { + api: { + async getAlias(alias) { + called++ + throw new MatrixServerError("Alias doesn't exist or something") + } + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "I'm just and <#1100319550446252084> testing channel mentions", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) + t.equal(called, 1) +}) + test("event2message: mentioning known bridged events works (plaintext body)", async t => { t.deepEqual( await eventToMessage({ diff --git a/src/matrix/api.js b/src/matrix/api.js index f9d3e08..c2b2384 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -367,6 +367,16 @@ async function ackEvent(event, mxid) { await sendReadReceipt(event.room_id, event.event_id, mxid) } +/** + * Resolve a room alias to a room ID. + * @param {string} alias + */ +async function getAlias(alias) { + /** @type {Ty.R.ResolvedRoom} */ + const root = await mreq.mreq("GET", `/client/v3/directory/room/${encodeURIComponent(alias)}`) + return root.room_id +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -395,3 +405,4 @@ module.exports.ping = ping module.exports.getMedia = getMedia module.exports.sendReadReceipt = sendReadReceipt module.exports.ackEvent = ackEvent +module.exports.getAlias = getAlias diff --git a/src/types.d.ts b/src/types.d.ts index b99046b..3298c40 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -336,6 +336,11 @@ export namespace R { room_id: string room_type?: string } + + export type ResolvedRoom = { + room_id: string + servers: string[] + } } export type Pagination = { From 551dbd0c42b5dad0b3835fad57aedfde967bbb51 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 8 Jan 2025 14:51:32 +1300 Subject: [PATCH 188/346] Add dependency justification --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 3770f9c..6f2f477 100644 --- a/readme.md +++ b/readme.md @@ -161,7 +161,7 @@ To get into the rooms on your Matrix account, use the `/invite [your mxid here]` ## Dependency justification -Total transitive production dependencies: 147 +Total transitive production dependencies: 148 ### 🦕 @@ -188,6 +188,7 @@ Total transitive production dependencies: 147 * (0) domino: DOM implementation that's already pulled in by turndown. * (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. +* (0) get-relative-path: Looks fine. No dependencies. * (0) get-stream: Only needed if content_length_workaround is true. * (1) heatsync: Module hot-reloader that I trust. * (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON. From c6708d4dbda2137a1c8358dee2a2eadecc43efb5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 12:50:32 +1300 Subject: [PATCH 189/346] Fix channel linking form URL --- src/web/pug/guild.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index ab61430..34178bf 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -133,7 +133,7 @@ block body p.s-description.m0 Publicly listed in directory, like Discord server discovery h3.mt32.fs-category Manually link channels - form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this") + form.d-flex.g16.ai-start(hx-post="/api/link" hx-trigger="submit" hx-disabled-elt="this") .fl-grow2.s-btn-group.fd-column.w40 each channel in unlinkedChannels input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) From a3e94a215a76e646e906400c8d62a82b8a99851f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 13:05:16 +1300 Subject: [PATCH 190/346] Hide error if sendTyping request fails --- src/d2m/actions/send-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 6d100d4..be785bb 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -48,7 +48,7 @@ async function sendMessage(message, channel, guild, row) { const eventIDs = [] if (events.length) { db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) - if (senderMxid) api.sendTyping(roomID, false, senderMxid) + if (senderMxid) api.sendTyping(roomID, false, senderMxid).catch(() => {}) } for (const event of events) { const part = event === events[0] ? 0 : 1 From 85269ea1530a0cc8af0e0ccd0c9507ff6ff31b7c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 13:11:51 +1300 Subject: [PATCH 191/346] Hopefully prevent checkMissed errors from crashing --- src/d2m/actions/create-space.js | 2 +- src/d2m/discord-packets.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index edd3ba3..ce86789 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -232,7 +232,7 @@ async function syncSpaceExpressions(data, checkBeforeSync) { } if (isDeepStrictEqual(existing, content)) return } - api.sendState(spaceID, "im.ponies.room_emotes", eventKey, content) + await api.sendState(spaceID, "im.ponies.room_emotes", eventKey, content) } await update(spaceID, "emojis", "moe.cadence.ooye.pack.emojis", expression.emojisToState) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 5956ac5..2e97671 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -51,9 +51,14 @@ const utils = { } if (listen === "full") { - eventDispatcher.checkMissedExpressions(message.d) - eventDispatcher.checkMissedPins(client, message.d) - eventDispatcher.checkMissedMessages(client, message.d) + try { + await eventDispatcher.checkMissedExpressions(message.d) + await eventDispatcher.checkMissedPins(client, message.d) + await eventDispatcher.checkMissedMessages(client, message.d) + } catch (e) { + console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") + console.error(e) + } } } else if (message.t === "GUILD_UPDATE") { From de57bdaf3c65e21ea97d4225107eb65968977912 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 13:32:39 +1300 Subject: [PATCH 192/346] Await syncRoom after linking --- src/web/routes/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 90cfd53..e63c01b 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -57,7 +57,7 @@ as.router.post("/api/link", defineEventHandler(async event => { db.prepare("INSERT INTO channel_room (channel_id, room_id, name, guild_id) VALUES (?, ?, ?, ?)").run(parsedBody.discord, parsedBody.matrix, channel.name, guildID) // Sync room data and space child - createRoom.syncRoom(parsedBody.discord) + await createRoom.syncRoom(parsedBody.discord) return null // 204 })) From f3b0d01400caaf5dcda28bcf75463fa79e8d53a9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 13:51:57 +1300 Subject: [PATCH 193/346] Fix fish reaction --- src/m2d/converters/emoji.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/m2d/converters/emoji.js b/src/m2d/converters/emoji.js index bb6a3b0..66a690c 100644 --- a/src/m2d/converters/emoji.js +++ b/src/m2d/converters/emoji.js @@ -42,6 +42,7 @@ function encodeEmoji(input, shortcode) { "%E2%9D%93", // ❓ "%F0%9F%8F%86", // 🏆️ "%F0%9F%93%9A", // 📚️ + "%F0%9F%90%9F", // 🐟️ ] discordPreferredEncoding = From 1e4952f1b871adaa7ba722b383bf1576d4794b76 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 12 Jan 2025 14:31:32 +1300 Subject: [PATCH 194/346] Add anti-timeout system to reactions interaction --- src/discord/interactions/reactions.js | 45 +++++++++++++--------- src/discord/interactions/reactions.test.js | 34 +++++++++++----- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 1a5a9ca..13dbd69 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -2,6 +2,8 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, select, from} = require("../../passthrough") +const {id: botID} = require("../../../addbot") +const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") @@ -11,21 +13,28 @@ const utils = sync.require("../../m2d/converters/utils") /** * @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction * @param {{api: typeof api}} di - * @returns {Promise} + * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ -async function _interact({data}, {api}) { +async function* _interact({data}, {api}) { const row = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") .select("event_id", "room_id").where({message_id: data.target_id}).get() if (!row) { - return { + return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { content: "This message hasn't been bridged to Matrix.", flags: DiscordTypes.MessageFlags.Ephemeral } - } + }} } + yield {createInteractionResponse: { + type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: DiscordTypes.MessageFlags.Ephemeral + } + }} + const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") /** @type {Map} */ @@ -40,29 +49,27 @@ async function _interact({data}, {api}) { } if (inverted.size === 0) { - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: "Nobody from Matrix reacted to this message.", - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: "Nobody from Matrix reacted to this message.", + }} } - return { - type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, - data: { - content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), - flags: DiscordTypes.MessageFlags.Ephemeral - } - } + return yield {editOriginalInteractionResponse: { + content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), + }} } /* c8 ignore start */ /** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */ async function interact(interaction) { - await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api})) + for await (const response of _interact(interaction, {api})) { + if (response.createInteractionResponse) { + await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse) + } else if (response.editOriginalInteractionResponse) { + await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse) + } + } } module.exports.interact = interact diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js index 6e29f57..50ddeca 100644 --- a/src/discord/interactions/reactions.test.js +++ b/src/discord/interactions/reactions.test.js @@ -1,17 +1,31 @@ const {test} = require("supertape") const {_interact} = require("./reactions") +/** + * @template T + * @param {AsyncIterable} ai + * @returns {Promise} + */ +async function fromAsync(ai) { + const result = [] + for await (const value of ai) { + result.push(value) + } + return result +} + test("reactions: checks if message is bridged", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "0" } - }, {}) - t.equal(msg.data.content, "This message hasn't been bridged to Matrix.") + }, {})) + t.equal(msgs.length, 1) + t.equal(msgs[0].createInteractionResponse.data.content, "This message hasn't been bridged to Matrix.") }) test("reactions: different response if nobody reacted", async t => { - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "1126786462646550579" } @@ -23,13 +37,14 @@ test("reactions: different response if nobody reacted", async t => { return [] } } - }) - t.equal(msg.data.content, "Nobody from Matrix reacted to this message.") + })) + t.equal(msgs.length, 2) + t.equal(msgs[1].editOriginalInteractionResponse.content, "Nobody from Matrix reacted to this message.") }) test("reactions: shows reactions if there are some, ignoring discord users", async t => { let called = 1 - const msg = await _interact({ + const msgs = await fromAsync(_interact({ data: { target_id: "1126786462646550579" } @@ -73,9 +88,10 @@ test("reactions: shows reactions if there are some, ignoring discord users", asy }] } } - }) + })) + t.equal(msgs.length, 2) t.equal( - msg.data.content, + msgs[1].editOriginalInteractionResponse.content, "🐈 ⮞ cadence [they] ⬩ @rnl:cadence.moe" + "\n🐈‍⬛ ⮞ cadence [they]" ) From 6bb31deeaf55fa01a74eb0bef44cbd17892d6677 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 16 Jan 2025 08:40:26 +1300 Subject: [PATCH 195/346] Ignore missed messages if channel was just added --- src/d2m/event-dispatcher.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 943e40e..a80c451 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -103,13 +103,19 @@ module.exports = { async checkMissedMessages(client, guild) { if (guild.unavailable) return const bridgedChannels = select("channel_room", "channel_id").pluck().all() - const prepared = select("event_message", "event_id", {}, "WHERE message_id = ?").pluck() + const preparedExists = db.prepare("SELECT channel_id FROM message_channel WHERE channel_id = ? LIMIT 1") + const preparedGet = select("event_message", "event_id", {}, "WHERE message_id = ?").pluck() for (const channel of guild.channels.concat(guild.threads)) { if (!bridgedChannels.includes(channel.id)) continue if (!("last_message_id" in channel) || !channel.last_message_id) continue - const latestWasBridged = prepared.get(channel.last_message_id) + + // Skip if channel is already up-to-date + const latestWasBridged = preparedGet.get(channel.last_message_id) if (latestWasBridged) continue + // Skip if channel was just added to the bridge (there's no place to resume from if it's brand new) + if (!preparedExists.get(channel.id)) continue + // Permissions check const member = guild.members.find(m => m.user?.id === client.user.id) if (!member) return @@ -131,7 +137,7 @@ module.exports = { } } let latestBridgedMessageIndex = messages.findIndex(m => { - return prepared.get(m.id) + return preparedGet.get(m.id) }) // console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`) if (latestBridgedMessageIndex === -1) latestBridgedMessageIndex = 1 // rather than crawling the ENTIRE channel history, let's just bridge the most recent 1 message to make it up to date. From 931cacea6ac7d0277f428d417c17ffb9f6bcb608 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 16 Jan 2025 08:44:13 +1300 Subject: [PATCH 196/346] Don't add channels/threads to the public directory --- src/d2m/actions/create-room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index eeba7aa..42d5714 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -26,7 +26,7 @@ const createSpace = sync.require("./create-space") */ const PRIVACY_ENUMS = { PRESET: ["private_chat", "public_chat", "public_chat"], - VISIBILITY: ["private", "private", "public"], + VISIBILITY: ["private", "private", "private"], SPACE_HISTORY_VISIBILITY: ["invited", "world_readable", "world_readable"], // copying from element client ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after are visible, but for world_readable anybody can read without even joining GUEST_ACCESS: ["can_join", "forbidden", "forbidden"], // whether guests can join space if other conditions are met From 8ad299b04c3a926c6372ff82f77a6cbb9c1837c3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 17 Jan 2025 11:33:29 +1300 Subject: [PATCH 197/346] Add foreign keys to database --- docs/foreign-keys.md | 98 ++++++++++++ src/d2m/actions/create-room.js | 7 + src/d2m/actions/delete-message.js | 2 - src/d2m/actions/edit-message.js | 2 +- src/d2m/actions/send-message.js | 2 +- src/db/migrate.js | 5 +- src/db/migrations/0016-foreign-keys.sql | 164 ++++++++++++++++++++ src/m2d/actions/redact.js | 1 - src/m2d/actions/send-event.js | 3 +- src/m2d/converters/event-to-message.js | 8 +- src/m2d/converters/event-to-message.test.js | 90 ++++++++--- src/m2d/event-dispatcher.js | 29 ++-- src/web/routes/oauth.js | 3 +- test/ooye-test-data.sql | 49 +++--- 14 files changed, 398 insertions(+), 65 deletions(-) create mode 100644 docs/foreign-keys.md create mode 100644 src/db/migrations/0016-foreign-keys.sql diff --git a/docs/foreign-keys.md b/docs/foreign-keys.md new file mode 100644 index 0000000..9940ed0 --- /dev/null +++ b/docs/foreign-keys.md @@ -0,0 +1,98 @@ +# Foreign keys in the Out Of Your Element database + +Historically, Out Of Your Element did not use foreign keys in the database, but since I found a need for them, I have decided to add them. Referential integrity is probably valuable as well. + +The need is that unlinking a channel and room using the web interface should clear up all related entries from `message_channel`, `event_message`, `reaction`, etc. Without foreign keys, this requires multiple DELETEs with tricky queries. With foreign keys and ON DELETE CASCADE, this just works. + +## Quirks + +* **REPLACE INTO** internally causes a DELETE followed by an INSERT, and the DELETE part **will trigger any ON DELETE CASCADE** foreign key conditions on the table, even when the primary key being replaced is the same. + * ```sql + CREATE TABLE discord_channel (channel_id TEXT NOT NULL, name TEXT NOT NULL, PRIMARY KEY (channel_id)); + CREATE TABLE discord_message (message_id TEXT NOT NULL, channel_id TEXT NOT NULL, PRIMARY KEY (message_id), + FOREIGN KEY (channel_id) REFERENCES discord_channel (channel_id) ON DELETE CASCADE); + INSERT INTO discord_channel (channel_id, name) VALUES ("c_1", "place"); + INSERT INTO discord_message (message_id, channel_id) VALUES ("m_2", "c_1"); -- i love my message + REPLACE INTO discord_channel (channel_id, name) VALUES ("c_1", "new place"); -- replace into time + -- i love my message + SELECT * FROM discord_message; -- where is my message + ``` +* In SQLite, `pragma foreign_keys = on` must be set **for each connection** after it's established. I've added this at the start of `migrate.js`, which is called by all database connections. + * Pragma? Pragma keys +* Whenever a child row is inserted, SQLite will look up a row from the parent table to ensure referential integrity. This means **the parent table should be sufficiently keyed or indexed on columns referenced by foreign keys**, or SQLite won't let you do it, with a cryptic error message later on during DML. Due to normal forms, foreign keys naturally tend to reference the parent table's primary key, which is indexed, so that's okay. But still keep this in mind, since many of OOYE's tables effectively have two primary keys, for the Discord and Matrix IDs. A composite primary key doesn't count, even when it's the first column. A unique index counts. + +## Where keys + +Here are some tables that could potentially have foreign keys added between them, and my thought process of whether foreign keys would be a good idea: + +* `guild_active` <--(PK guild_id FK)-- `channel_room` ✅ + * Could be good for referential integrity. + * Linking to guild_space would be pretty scary in case the guild was being relinked to a different space - since rooms aren't tied to a space, this wouldn't actually disturb anything. So I pick guild_active instead. +* `channel_room` <--(PK channel_id FK)-- `message_channel` ✅ + * Seems useful as we want message records to be deleted when a channel is unlinked. +* `message_channel` <--(PK message_id PK)-- `event_message` ✅ + * Seems useful as we want event information to be deleted when a channel is unlinked. +* `guild_active` <--(PK guild_id PK)-- `guild_space` ✅ + * All bridged guilds should have a corresponding guild_active entry, so referential integrity would be useful here to make sure we haven't got any weird states. +* `channel_room` <--(**C** room_id PK)-- `member_cache` ✅ + * Seems useful as we want to clear the member cache when a channel is unlinked. + * There is no index on `channel_room.room_id` right now. It would be good to create this index. Will just make it UNIQUE in the table definition. +* `message_channel` <--(PK message_id FK)-- `reaction` ✅ + * Seems useful as we want to clear the reactions cache when a channel is unlinked. +* `sim` <--(**C** mxid FK)-- `sim_member` + * OOYE inner joins on this. + * Sims are never deleted so if this was added it would only be used for enforcing referential integrity. + * The storage cost of the additional index on `sim` would not be worth the benefits. +* `channel_room` <--(**C** room_id PK)-- `sim_member` + * If a room is being permanently unlinked, it may be useful to see a populated member list. If it's about to be relinked to another channel, we want to keep the sims in the room for more speed and to avoid spamming state events into the timeline. + * Either way, the sims should remain in the room even after it's been unlinked. So no referential integrity is desirable here. +* `sim` <--(PK user_id PK)-- `sim_proxy` + * OOYE left joins on this. In normal operation, this relationship might not exist. +* `channel_room` <--(PK channel_id PK)-- `webhook` ✅ + * Seems useful. Webhooks should be deleted from Discord just before the channel is unlinked. That should be mirrored in the database too. + +## Occurrences of REPLACE INTO/DELETE FROM + +* `edit-message.js` — `REPLACE INTO message_channel` + * Scary! Changed to INSERT OR IGNORE +* `send-message.js` — `REPLACE INTO message_channel` + * Changed to INSERT OR IGNORE +* `add-reaction.js` — `REPLACE INTO reaction` +* `channel-webhook.js` — `REPLACE INTO webhook` +* `send-event.js` — `REPLACE INTO message_channel` + * Seems incorrect? Maybe?? Originally added in fcbb045. Changed to INSERT +* `event-to-message.js` — `REPLACE INTO member_cache` +* `oauth.js` — `REPLACE INTO guild_active` + * Very scary!! Changed to INSERT .. ON CONFLICT DO UPDATE +* `create-room.js` — `DELETE FROM channel_room` + * Please cascade +* `delete-message.js` + * Removed redundant DELETEs +* `edit-message.js` — `DELETE FROM event_message` +* `register-pk-user.js` — `DELETE FROM sim` + * It's a failsafe during creation +* `register-user.js` — `DELETE FROM sim` + * It's a failsafe during creation +* `remove-reaction.js` — `DELETE FROM reaction` +* `event-dispatcher.js` — `DELETE FROM member_cache` +* `redact.js` — `DELETE FROM event_message` + * Removed this redundant DELETE +* `send-event.js` — `DELETE FROM event_message` + * Removed this redundant DELETE + +## How keys + +SQLite does not have a complete ALTER TABLE command, so I have to DROP and CREATE. According to [the docs](https://www.sqlite.org/lang_altertable.html), the correct strategy is: + +1. (Not applicable) *If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF.* +2. Start a transaction. +3. (Not applicable) *Remember the format of all indexes, triggers, and views associated with table X. This information will be needed in step 8 below. One way to do this is to run a query like the following: SELECT type, sql FROM sqlite_schema WHERE tbl_name='X'.* +4. Use CREATE TABLE to construct a new table "new_X" that is in the desired revised format of table X. Make sure that the name "new_X" does not collide with any existing table name, of course. +5. Transfer content from X into new_X using a statement like: INSERT INTO new_X SELECT ... FROM X. +6. Drop the old table X: DROP TABLE X. +7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X. +8. (Not applicable) *Use CREATE INDEX, CREATE TRIGGER, and CREATE VIEW to reconstruct indexes, triggers, and views associated with table X. Perhaps use the old format of the triggers, indexes, and views saved from step 3 above as a guide, making changes as appropriate for the alteration.* +9. (Not applicable) *If any views refer to table X in a way that is affected by the schema change, then drop those views using DROP VIEW and recreate them with whatever changes are necessary to accommodate the schema change using CREATE VIEW.* +10. If foreign key constraints were originally enabled then run PRAGMA foreign_key_check to verify that the schema change did not break any foreign key constraints. +11. Commit the transaction started in step 2. +12. (Not applicable) *If foreign keys constraints were originally enabled, reenable them now.* diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 42d5714..64e2b68 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -430,6 +430,13 @@ async function unbridgeDeletedChannel(channel, guildID) { // leave room await api.leaveRoom(roomID) + // delete webhook on discord + const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get() + if (webhook) { + await discord.snow.webhook.deleteWebhook(webhook.webhook_id, webhook.webhook_token) + db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(channel.id) + } + // delete room from database db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) } diff --git a/src/d2m/actions/delete-message.js b/src/d2m/actions/delete-message.js index bc8adfb..e9e0b08 100644 --- a/src/d2m/actions/delete-message.js +++ b/src/d2m/actions/delete-message.js @@ -16,7 +16,6 @@ async function deleteMessage(data) { const eventsToRedact = select("event_message", "event_id", {message_id: data.id}).pluck().all() db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(data.id) - db.prepare("DELETE FROM event_message WHERE message_id = ?").run(data.id) for (const eventID of eventsToRedact) { // Unfortunately, we can't specify a sender to do the redaction as, unless we find out that info via the audit logs await api.redactEvent(row.room_id, eventID) @@ -35,7 +34,6 @@ async function deleteMessageBulk(data) { const sids = JSON.stringify(data.ids) const eventsToRedact = from("event_message").pluck("event_id").and("WHERE message_id IN (SELECT value FROM json_each(?))").all(sids) db.prepare("DELETE FROM message_channel WHERE message_id IN (SELECT value FROM json_each(?))").run(sids) - db.prepare("DELETE FROM event_message WHERE message_id IN (SELECT value FROM json_each(?))").run(sids) for (const eventID of eventsToRedact) { // Awaiting will make it go slower, but since this could be a long-running operation either way, we want to leave rate limit capacity for other operations await api.redactEvent(roomID, eventID) diff --git a/src/d2m/actions/edit-message.js b/src/d2m/actions/edit-message.js index d85f925..85b1a14 100644 --- a/src/d2m/actions/edit-message.js +++ b/src/d2m/actions/edit-message.js @@ -61,7 +61,7 @@ async function editMessage(message, guild, row) { // 4. Send all the things. if (eventsToSend.length) { - db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) + db.prepare("INSERT OR IGNORE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) } for (const content of eventsToSend) { const eventType = content.$type diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index be785bb..743c15a 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -47,7 +47,7 @@ async function sendMessage(message, channel, guild, row) { const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) const eventIDs = [] if (events.length) { - db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) + db.prepare("INSERT OR IGNORE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) if (senderMxid) api.sendTyping(roomID, false, senderMxid).catch(() => {}) } for (const event of events) { diff --git a/src/db/migrate.js b/src/db/migrate.js index 7c1faf9..46d0c14 100644 --- a/src/db/migrate.js +++ b/src/db/migrate.js @@ -6,7 +6,8 @@ const {join} = require("path") async function migrate(db) { let files = fs.readdirSync(join(__dirname, "migrations")) files = files.sort() - db.prepare("CREATE TABLE IF NOT EXISTS migration (filename TEXT NOT NULL)").run() + db.prepare("CREATE TABLE IF NOT EXISTS migration (filename TEXT NOT NULL, PRIMARY KEY (filename)) WITHOUT ROWID").run() + /** @type {string} */ let progress = db.prepare("SELECT * FROM migration").pluck().get() if (!progress) { progress = "" @@ -37,6 +38,8 @@ async function migrate(db) { if (migrationRan) { console.log("Database migrations all done.") } + + db.pragma("foreign_keys = on") } module.exports.migrate = migrate diff --git a/src/db/migrations/0016-foreign-keys.sql b/src/db/migrations/0016-foreign-keys.sql new file mode 100644 index 0000000..c41f329 --- /dev/null +++ b/src/db/migrations/0016-foreign-keys.sql @@ -0,0 +1,164 @@ +-- /docs/foreign-keys.md + +-- 2 +BEGIN TRANSACTION; + +-- *** channel_room *** + +-- 4 +-- adding UNIQUE to room_id here will auto-generate the usable index we wanted +CREATE TABLE "new_channel_room" ( + "channel_id" TEXT NOT NULL, + "room_id" TEXT NOT NULL UNIQUE, + "name" TEXT NOT NULL, + "nick" TEXT, + "thread_parent" TEXT, + "custom_avatar" TEXT, + "last_bridged_pin_timestamp" INTEGER, + "speedbump_id" TEXT, + "speedbump_checked" INTEGER, + "speedbump_webhook_id" TEXT, + "guild_id" TEXT, + PRIMARY KEY("channel_id"), + FOREIGN KEY("guild_id") REFERENCES "guild_active"("guild_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +INSERT INTO new_channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id) SELECT channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id FROM channel_room; +-- 6 +DROP TABLE channel_room; +-- 7 +ALTER TABLE new_channel_room RENAME TO channel_room; + +-- *** message_channel *** + +-- 4 +CREATE TABLE "new_message_channel" ( + "message_id" TEXT NOT NULL, + "channel_id" TEXT NOT NULL, + PRIMARY KEY("message_id"), + FOREIGN KEY("channel_id") REFERENCES "channel_room"("channel_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +-- don't copy any orphaned messages +INSERT INTO new_message_channel (message_id, channel_id) SELECT message_id, channel_id FROM message_channel WHERE channel_id IN (SELECT channel_id FROM channel_room); +-- 6 +DROP TABLE message_channel; +-- 7 +ALTER TABLE new_message_channel RENAME TO message_channel; + +-- *** event_message *** + +-- clean up any orphaned events +DELETE FROM event_message WHERE message_id NOT IN (SELECT message_id FROM message_channel); +-- 4 +CREATE TABLE "new_event_message" ( + "event_id" TEXT NOT NULL, + "event_type" TEXT, + "event_subtype" TEXT, + "message_id" TEXT NOT NULL, + "part" INTEGER NOT NULL, + "reaction_part" INTEGER NOT NULL, + "source" INTEGER NOT NULL, + PRIMARY KEY("message_id","event_id"), + FOREIGN KEY("message_id") REFERENCES "message_channel"("message_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +INSERT INTO new_event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) SELECT event_id, event_type, event_subtype, message_id, part, reaction_part, source FROM event_message; +-- 6 +DROP TABLE event_message; +-- 7 +ALTER TABLE new_event_message RENAME TO event_message; + +-- *** guild_space *** + +-- 4 +CREATE TABLE "new_guild_space" ( + "guild_id" TEXT NOT NULL, + "space_id" TEXT NOT NULL, + "privacy_level" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("guild_id"), + FOREIGN KEY("guild_id") REFERENCES "guild_active"("guild_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +INSERT INTO new_guild_space (guild_id, space_id, privacy_level) SELECT guild_id, space_id, privacy_level FROM guild_space; +-- 6 +DROP TABLE guild_space; +-- 7 +ALTER TABLE new_guild_space RENAME TO guild_space; + +-- *** member_cache *** + +-- 4 +CREATE TABLE "new_member_cache" ( + "room_id" TEXT NOT NULL, + "mxid" TEXT NOT NULL, + "displayname" TEXT, + "avatar_url" TEXT, power_level INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("room_id","mxid"), + FOREIGN KEY("room_id") REFERENCES "channel_room"("room_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +INSERT INTO new_member_cache (room_id, mxid, displayname, avatar_url) SELECT room_id, mxid, displayname, avatar_url FROM member_cache WHERE room_id IN (SELECT room_id FROM channel_room); +-- 6 +DROP TABLE member_cache; +-- 7 +ALTER TABLE new_member_cache RENAME TO member_cache; + +-- *** reaction *** + +-- 4 +CREATE TABLE "new_reaction" ( + "hashed_event_id" INTEGER NOT NULL, + "message_id" TEXT NOT NULL, + "encoded_emoji" TEXT NOT NULL, + PRIMARY KEY("hashed_event_id"), + FOREIGN KEY("message_id") REFERENCES "message_channel"("message_id") ON DELETE CASCADE +) WITHOUT ROWID; +-- 5 +INSERT INTO new_reaction (hashed_event_id, message_id, encoded_emoji) SELECT hashed_event_id, message_id, encoded_emoji FROM reaction WHERE message_id IN (SELECT message_id FROM message_channel); +-- 6 +DROP TABLE reaction; +-- 7 +ALTER TABLE new_reaction RENAME TO reaction; + +-- *** webhook *** + +-- 4 +-- using RESTRICT instead of CASCADE as a reminder that the webhooks also need to be deleted using the Discord API, it can't just be entirely automatic +CREATE TABLE "new_webhook" ( + "channel_id" TEXT NOT NULL, + "webhook_id" TEXT NOT NULL, + "webhook_token" TEXT NOT NULL, + PRIMARY KEY("channel_id"), + FOREIGN KEY("channel_id") REFERENCES "channel_room"("channel_id") ON DELETE RESTRICT +) WITHOUT ROWID; +-- 5 +INSERT INTO new_webhook (channel_id, webhook_id, webhook_token) SELECT channel_id, webhook_id, webhook_token FROM webhook WHERE channel_id IN (SELECT channel_id FROM channel_room); +-- 6 +DROP TABLE webhook; +-- 7 +ALTER TABLE new_webhook RENAME TO webhook; + +-- *** sim *** + +-- 4 +-- while we're at it, rebuild this table to give it WITHOUT ROWID, remove UNIQUE, and drop the localpart column. no foreign keys needed +CREATE TABLE "new_sim" ( + "user_id" TEXT NOT NULL, + "sim_name" TEXT NOT NULL, + "mxid" TEXT NOT NULL, + PRIMARY KEY("user_id") +) WITHOUT ROWID; +-- 5 +INSERT INTO new_sim (user_id, sim_name, mxid) SELECT user_id, sim_name, mxid FROM sim; +-- 6 +DROP TABLE sim; +-- 7 +ALTER TABLE new_sim RENAME TO sim; + +-- *** end *** + +-- 10 +PRAGMA foreign_key_check; +-- 11 +COMMIT; diff --git a/src/m2d/actions/redact.js b/src/m2d/actions/redact.js index 5a12d5a..1f6cef8 100644 --- a/src/m2d/actions/redact.js +++ b/src/m2d/actions/redact.js @@ -13,7 +13,6 @@ const utils = sync.require("../converters/utils") */ async function deleteMessage(event) { const rows = from("event_message").join("message_channel", "message_id").select("channel_id", "message_id").where({event_id: event.redacts}).all() - db.prepare("DELETE FROM event_message WHERE event_id = ?").run(event.redacts) for (const row of rows) { db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(row.message_id) await discord.snow.channel.deleteMessage(row.channel_id, row.message_id, event.content.reason) diff --git a/src/m2d/actions/send-event.js b/src/m2d/actions/send-event.js index 0a270a0..35fcfda 100644 --- a/src/m2d/actions/send-event.js +++ b/src/m2d/actions/send-event.js @@ -102,14 +102,13 @@ async function sendEvent(event) { for (const id of messagesToDelete) { db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(id) - db.prepare("DELETE FROM event_message WHERE message_id = ?").run(id) await channelWebhook.deleteMessageWithWebhook(channelID, id, threadID) } for (const message of messagesToSend) { const reactionPart = messagesToEdit.length === 0 && message === messagesToSend[messagesToSend.length - 1] ? 0 : 1 const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) - db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, threadID || channelID) + db.prepare("INSERT INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, threadID || channelID) db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content["msgtype"] || null, messageResponse.id, eventPart, reactionPart) // source 0 = matrix eventPart = 1 diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index b52717d..c498f1e 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -258,7 +258,13 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) { const row = select("member_cache", ["displayname", "avatar_url"], {room_id: roomID, mxid}).get() if (row) return row return api.getStateEvent(roomID, "m.room.member", mxid).then(event => { - db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(roomID, mxid, event?.displayname || null, event?.avatar_url || null) + const displayname = event?.displayname || null + const avatar_url = event?.avatar_url || null + db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?").run( + roomID, mxid, + displayname, avatar_url, + displayname, avatar_url + ) return event }).catch(() => { return {displayname: null, avatar_url: null} diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 145e9ec..0dc9110 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -559,7 +559,7 @@ test("event2message: lists are bridged correctly", async t => { "transaction_id": "m1692967313951.441" }, "event_id": "$l-xQPY5vNJo3SNxU9d8aOWNVD1glMslMyrp4M_JEF70", - "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" + "room_id": "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -662,7 +662,7 @@ test("event2message: code block contents are formatted correctly and not escaped formatted_body: "
    input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n
    \n

    input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,

    \n" }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -692,7 +692,7 @@ test("event2message: code blocks use double backtick as delimiter when necessary formatted_body: "backtick in ` the middle, backtick at the edge`" }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -722,7 +722,7 @@ test("event2message: inline code is converted to code block if it contains both formatted_body: "` one two ``" }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -752,7 +752,7 @@ test("event2message: code blocks are uploaded as attachments instead if they con formatted_body: 'So if you run code like this
    System.out.println("```");
    it should print a markdown formatted code block' }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -784,7 +784,7 @@ test("event2message: code blocks are uploaded as attachments instead if they con formatted_body: 'So if you run code like this
    System.out.println("```");
    it should print a markdown formatted code block' }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -821,7 +821,7 @@ test("event2message: characters are encoded properly in code blocks", async t => + '\n
    ' }, event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!BpMdOUkWWhFxmTrENV:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }), { ensureJoined: [], @@ -902,7 +902,7 @@ test("event2message: lists have appropriate line breaks", async t => { 'm.mentions': {}, msgtype: 'm.text' }, - room_id: '!cBxtVRxDlZvSVhJXVK:cadence.moe', + room_id: '!TqlyQmifxGUggEmdBN:cadence.moe', sender: '@Milan:tchncs.de', type: 'm.room.message', }), @@ -943,7 +943,7 @@ test("event2message: ordered list start attribute works", async t => { 'm.mentions': {}, msgtype: 'm.text' }, - room_id: '!cBxtVRxDlZvSVhJXVK:cadence.moe', + room_id: '!TqlyQmifxGUggEmdBN:cadence.moe', sender: '@Milan:tchncs.de', type: 'm.room.message', }), @@ -1088,7 +1088,7 @@ test("event2message: rich reply to a rich reply to a multi-line message should c content: { body: "> <@cadence:cadence.moe> I just checked in a fix that will probably work, can you try reproducing this on the latest `main` branch and see if I fixed it?\n\nwill try later (tomorrow if I don't forgor)", format: "org.matrix.custom.html", - formatted_body: "
    In reply to @cadence:cadence.moe
    I just checked in a fix that will probably work, can you try reproducing this on the latest main branch and see if I fixed it?
    will try later (tomorrow if I don't forgor)", + formatted_body: "
    In reply to @cadence:cadence.moe
    I just checked in a fix that will probably work, can you try reproducing this on the latest main branch and see if I fixed it?
    will try later (tomorrow if I don't forgor)", "m.relates_to": { "m.in_reply_to": { event_id: "$A0Rj559NKOh2VndCZSTJXcvgi42gZWVfVQt73wA2Hn0" @@ -1111,7 +1111,7 @@ test("event2message: rich reply to a rich reply to a multi-line message should c "msgtype": "m.text", "body": "> <@solonovamax:matrix.org> multipart messages will be deleted if the message is edited to require less space\n> \n> \n> steps to reproduce:\n> \n> 1. send a message that is longer than 2000 characters (discord character limit)\n> - bot will split message into two messages on discord\n> 2. edit message to be under 2000 characters (discord character limit)\n> - bot will delete one of the messages on discord, and then edit the other one to include the edited content\n> - the bot will *then* delete the message on matrix (presumably) because one of the messages on discord was deleted (by \n\nI just checked in a fix that will probably work, can you try reproducing this on the latest `main` branch and see if I fixed it?", "format": "org.matrix.custom.html", - "formatted_body": "
    In reply to @solonovamax:matrix.org

    multipart messages will be deleted if the message is edited to require less space

    \n

    steps to reproduce:

    \n
      \n
    1. send a message that is longer than 2000 characters (discord character limit)
    2. \n
    \n
      \n
    • bot will split message into two messages on discord
    • \n
    \n
      \n
    1. edit message to be under 2000 characters (discord character limit)
    2. \n
    \n
      \n
    • bot will delete one of the messages on discord, and then edit the other one to include the edited content
    • \n
    • the bot will then delete the message on matrix (presumably) because one of the messages on discord was deleted (by
    • \n
    \n
    I just checked in a fix that will probably work, can you try reproducing this on the latest main branch and see if I fixed it?", + "formatted_body": "
    In reply to @solonovamax:matrix.org

    multipart messages will be deleted if the message is edited to require less space

    \n

    steps to reproduce:

    \n
      \n
    1. send a message that is longer than 2000 characters (discord character limit)
    2. \n
    \n
      \n
    • bot will split message into two messages on discord
    • \n
    \n
      \n
    1. edit message to be under 2000 characters (discord character limit)
    2. \n
    \n
      \n
    • bot will delete one of the messages on discord, and then edit the other one to include the edited content
    • \n
    • the bot will then delete the message on matrix (presumably) because one of the messages on discord was deleted (by
    • \n
    \n
    I just checked in a fix that will probably work, can you try reproducing this on the latest main branch and see if I fixed it?", "m.relates_to": { "m.in_reply_to": { "event_id": "$u4OD19vd2GETkOyhgFVla92oDKI4ojwBf2-JeVCG7EI" @@ -1123,7 +1123,7 @@ test("event2message: rich reply to a rich reply to a multi-line message should c "age": 19069564 }, "event_id": "$A0Rj559NKOh2VndCZSTJXcvgi42gZWVfVQt73wA2Hn0", - "room_id": "!cBxtVRxDlZvSVhJXVK:cadence.moe" + "room_id": "!TqlyQmifxGUggEmdBN:cadence.moe" }) }, snow: { @@ -3476,6 +3476,56 @@ test("event2message: colon after mentions is stripped", async t => { }) test("event2message: caches the member if the member is not known", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + content: { + body: "testing the member state cache", + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe", + sender: "@should_be_newly_cached:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }, {}, { + api: { + getStateEvent: async (roomID, type, stateKey) => { + called++ + t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe") + t.equal(type, "m.room.member") + t.equal(stateKey, "@should_be_newly_cached:cadence.moe") + return { + avatar_url: "mxc://cadence.moe/this_is_the_avatar" + } + } + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "should_be_newly_cached", + content: "testing the member state cache", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) + + t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe"}).all(), [ + {avatar_url: "mxc://cadence.moe/this_is_the_avatar", displayname: null, mxid: "@should_be_newly_cached:cadence.moe"} + ]) + t.equal(called, 1, "getStateEvent should be called once") +}) + +test("event2message: does not cache the member if the room is not known", async t => { let called = 0 t.deepEqual( await eventToMessage({ @@ -3511,7 +3561,7 @@ test("event2message: caches the member if the member is not known", async t => { messagesToSend: [{ username: "should_be_newly_cached", content: "testing the member state cache", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3519,9 +3569,7 @@ test("event2message: caches the member if the member is not known", async t => { } ) - t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!should_be_newly_cached:cadence.moe"}).all(), [ - {avatar_url: "mxc://cadence.moe/this_is_the_avatar", displayname: null, mxid: "@should_be_newly_cached:cadence.moe"} - ]) + t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!should_be_newly_cached:cadence.moe"}).all(), []) t.equal(called, 1, "getStateEvent should be called once") }) @@ -3580,7 +3628,7 @@ test("event2message: overly long usernames are shifted into the message content" }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", origin_server_ts: 1688301929913, - room_id: "!should_be_newly_cached_2:cadence.moe", + room_id: "!cqeGDbPiMFAhLsqqqq:cadence.moe", sender: "@should_be_newly_cached_2:cadence.moe", type: "m.room.message", unsigned: { @@ -3590,7 +3638,7 @@ test("event2message: overly long usernames are shifted into the message content" api: { getStateEvent: async (roomID, type, stateKey) => { called++ - t.equal(roomID, "!should_be_newly_cached_2:cadence.moe") + t.equal(roomID, "!cqeGDbPiMFAhLsqqqq:cadence.moe") t.equal(type, "m.room.member") t.equal(stateKey, "@should_be_newly_cached_2:cadence.moe") return { @@ -3613,7 +3661,7 @@ test("event2message: overly long usernames are shifted into the message content" }] } ) - t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!should_be_newly_cached_2:cadence.moe"}).all(), [ + t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!cqeGDbPiMFAhLsqqqq:cadence.moe"}).all(), [ {avatar_url: null, displayname: "I am BLACK I am WHITE I am SHORT I am LONG I am EVERYTHING YOU THINK IS IMPORTANT and I DON'T MATTER", mxid: "@should_be_newly_cached_2:cadence.moe"} ]) t.equal(called, 1, "getStateEvent should be called once") @@ -3628,7 +3676,7 @@ test("event2message: overly long usernames are not treated specially when the ms }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", origin_server_ts: 1688301929913, - room_id: "!should_be_newly_cached_2:cadence.moe", + room_id: "!cqeGDbPiMFAhLsqqqq:cadence.moe", sender: "@should_be_newly_cached_2:cadence.moe", type: "m.room.message", unsigned: { @@ -4477,7 +4525,7 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet formatted_body: "testing \":chess_good_move:\"\":chess_incorrect:\"\":chess_blund:\"\":chess_brilliant_move:\"\":chess_blundest:\"\":chess_draw_black:\"\":chess_good_move:\"\":chess_incorrect:\"\":chess_blund:\"\":chess_brilliant_move:\"\":chess_blundest:\"\":chess_draw_black:\"" }, event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4", - room_id: "!maggESguZBqGBZtSnr:cadence.moe" + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, {mxcDownloader: mockGetAndConvertEmoji}) const testResult = { content: messages.messagesToSend[0].content, diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index a270293..6cbd6c6 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -199,18 +199,29 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", async event => { if (event.state_key[0] !== "@") return if (utils.eventSenderIsFromDiscord(event.state_key)) return + if (event.content.membership === "leave" || event.content.membership === "ban") { // Member is gone - db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) - } else { - // Member is here - db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?") - .run( - event.room_id, event.state_key, - event.content.displayname || null, event.content.avatar_url || null, - event.content.displayname || null, event.content.avatar_url || null - ) + return db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) } + + const room = select("channel_room", "room_id", {room_id: event.room_id}) + if (!room) return // don't cache members in unbridged rooms + + // Member is here + let powerLevel = 0 + try { + /** @type {Ty.Event.M_Power_Levels} */ + const powerLevelsEvent = await api.getStateEvent(event.room_id, "m.room.power_levels", "") + powerLevel = powerLevelsEvent.users?.[event.state_key] ?? powerLevelsEvent.users_default ?? 0 + } catch (e) {} + const displayname = event.content.displayname || null + const avatar_url = event.content.avatar_url + db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?").run( + event.room_id, event.state_key, + displayname, avatar_url, powerLevel, + displayname, avatar_url, powerLevel + ) })) sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_levels", diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js index 12c991d..1a3753f 100644 --- a/src/web/routes/oauth.js +++ b/src/web/routes/oauth.js @@ -87,7 +87,8 @@ as.router.get("/oauth", defineEventHandler(async event => { // Set auto-create for the guild // @ts-ignore if (managedGuilds.includes(parsedQuery.data.guild_id)) { - db.prepare("REPLACE INTO guild_active (guild_id, autocreate) VALUES (?, ?)").run(parsedQuery.data.guild_id, +!session.data.selfService) + const autocreateInteger = +!session.data.selfService + db.prepare("INSERT INTO guild_active (guild_id, autocreate) VALUES (?, ?) ON CONFLICT DO UPDATE SET autocreate = ?").run(parsedQuery.data.guild_id, autocreateInteger, autocreateInteger) } if (parsedQuery.data.guild_id) { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 38fed25..757ef9b 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -1,13 +1,14 @@ BEGIN TRANSACTION; -INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES -('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0); - INSERT INTO guild_active (guild_id, autocreate) VALUES ('112760669178241024', 1); +INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES +('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0); + INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES ('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL), +('687028734322147344', '!fGgIymcYWOqjbSRUdV:cadence.moe', 'slow-news-day', NULL, NULL, NULL), ('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL), ('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL), ('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL), @@ -18,25 +19,25 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom ('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL), ('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL); -INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES -('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), -('820865262526005258', 'crunch_god', '_ooye_crunch_god', '@_ooye_crunch_god:cadence.moe'), -('771520384671416320', 'bojack_horseman', '_ooye_bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), -('112890272819507200', '.wing.', '_ooye_.wing.', '@_ooye_.wing.:cadence.moe'), -('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), -('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), -('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'), -('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'), -('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe'), -('772659086046658620', 'cadence', '_ooye_cadence', '@_ooye_cadence:cadence.moe'); - -INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES -('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); +INSERT INTO sim (user_id, sim_name, mxid) VALUES +('0', 'bot', '@_ooye_bot:cadence.moe'), +('820865262526005258', 'crunch_god', '@_ooye_crunch_god:cadence.moe'), +('771520384671416320', 'bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), +('112890272819507200', '.wing.', '@_ooye_.wing.:cadence.moe'), +('114147806469554185', 'extremity', '@_ooye_extremity:cadence.moe'), +('111604486476181504', 'kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), +('1109360903096369153', 'amanda', '@_ooye_amanda:cadence.moe'), +('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'), +('320067006521147393', 'papiophidian', '@_ooye_papiophidian:cadence.moe'), +('772659086046658620', 'cadence', '@_ooye_cadence:cadence.moe'); INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL), ('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); +INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES +('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); + INSERT INTO message_channel (message_id, channel_id) VALUES ('1106366167788044450', '122155380120748034'), ('1106366167788044451', '122155380120748034'), @@ -65,7 +66,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1273743950028607530', '1100319550446252084'), ('1278002262400176128', '1100319550446252084'), ('1278001833876525057', '1100319550446252084'), -('1191567971970191490', '176333891320283136'); +('1191567971970191490', '176333891320283136'), +('1144874214311067708', '687028734322147344'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), @@ -140,19 +142,16 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES ('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), -('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc', 0), +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL, 0), ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), -('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), +('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL, 0), ('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0), -('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0), +('!TqlyQmifxGUggEmdBN:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0), -('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0), -('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL, 0), -('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0), -('!BpMdOUkWWhFxmTrENV:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 100); +('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0); INSERT INTO reaction (hashed_event_id, message_id, encoded_emoji) VALUES (5162930312280790092, '1141501302736695317', '%F0%9F%90%88'); From 14574b4e2c31f451af069e029ce0db69219451b1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 17 Jan 2025 11:40:34 +1300 Subject: [PATCH 198/346] Support alternate Discord hosts --- src/d2m/discord-client.js | 10 +++++++--- src/types.d.ts | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index bffb904..37b3eac 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -1,10 +1,14 @@ // @ts-check -const { SnowTransfer } = require("snowtransfer") -const { Client: CloudStorm } = require("cloudstorm") +const {Endpoints, SnowTransfer} = require("snowtransfer") +const {reg} = require("../matrix/read-registration") +const {Client: CloudStorm} = require("cloudstorm") + +// @ts-ignore +Endpoints.BASE_HOST = reg.ooye.discord_origin || "https://discord.com"; Endpoints.CDN_URL = reg.ooye.discord_cdn_origin || "https://cdn.discordapp.com" const passthrough = require("../passthrough") -const { sync } = passthrough +const {sync} = passthrough /** @type {import("./discord-packets")} */ const discordPackets = sync.require("./discord-packets") diff --git a/src/types.d.ts b/src/types.d.ts index 3298c40..84aad44 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -28,6 +28,8 @@ export type AppServiceRegistrationConfig = { content_length_workaround: boolean include_user_id_in_mxid: boolean invite: string[] + discord_origin?: string + discord_cdn_origin?: string } old_bridge?: { as_token: string From f42eb6495fba503401f84575751258d9ff9cafa0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 17 Jan 2025 18:05:34 +1300 Subject: [PATCH 199/346] New unicode emoji processor --- scripts/emoji-surrogates-statistics.js | 77 + src/d2m/actions/remove-reaction.js | 2 +- src/m2d/actions/add-reaction.js | 2 +- src/m2d/converters/emoji.js | 128 +- src/m2d/converters/emoji.test.js | 52 + src/m2d/converters/emojis.txt | 3799 ++++++++++++++++++++++++ test/test.js | 1 + 7 files changed, 4015 insertions(+), 46 deletions(-) create mode 100644 scripts/emoji-surrogates-statistics.js create mode 100644 src/m2d/converters/emoji.test.js create mode 100644 src/m2d/converters/emojis.txt diff --git a/scripts/emoji-surrogates-statistics.js b/scripts/emoji-surrogates-statistics.js new file mode 100644 index 0000000..29abce3 --- /dev/null +++ b/scripts/emoji-surrogates-statistics.js @@ -0,0 +1,77 @@ +// @ts-check + +const fs = require("fs") +const {join} = require("path") +const s = fs.readFileSync(join(__dirname, "..", "src", "m2d", "converters", "emojis.txt"), "utf8").split("\n").map(x => encodeURIComponent(x)) +const searchPattern = "%EF%B8%8F" + +/** + * adapted from es.map.group-by.js in core-js + * @template K,V + * @param {V[]} items + * @param {(item: V) => K} fn + * @returns {Map} + */ +function groupBy(items, fn) { + var map = new Map(); + for (const value of items) { + var key = fn(value); + if (!map.has(key)) map.set(key, [value]); + else map.get(key).push(value); + } + return map; +} + +/** + * @param {number[]} items + * @param {number} width + */ +function xhistogram(items, width) { + const chars = " ▏▎▍▌▋▊▉" + const max = items.reduce((a, c) => c > a ? c : a, 0) + return items.map(v => { + const p = v / max * (width-1) + return ( + Array(Math.floor(p)).fill("█").join("") /* whole part */ + + chars[Math.ceil((p % 1) * (chars.length-1))] /* decimal part */ + ).padEnd(width) + }) +} + +/** + * @param {number[]} items + * @param {[number, number]} xrange + */ +function yhistogram(items, xrange, printHeader = false) { + const chars = "░▁_▂▃▄▅▆▇█" + const ones = "₀₁₂₃₄₅₆₇₈₉" + const tens = "0123456789" + const xy = [] + let max = 0 + /** value (x) -> frequency (y) */ + const grouped = groupBy(items, x => x) + for (let i = xrange[0]; i <= xrange[1]; i++) { + if (printHeader) { + if (i === -1) process.stdout.write("-") + else if (i.toString().at(-1) === "0") process.stdout.write(tens[i/10]) + else process.stdout.write(ones[i%10]) + } + const y = grouped.get(i)?.length ?? 0 + if (y > max) max = y + xy.push(y) + } + if (printHeader) console.log() + return xy.map(y => chars[Math.ceil(y / max * (chars.length-1))]).join("") +} + +const grouped = groupBy(s, x => x.length) +const sortedGroups = [...grouped.entries()].sort((a, b) => b[0] - a[0]) +let length = 0 +const lengthHistogram = xhistogram(sortedGroups.map(v => v[1].length), 10) +for (let i = 0; i < sortedGroups.length; i++) { + const [k, v] = sortedGroups[i] + const l = lengthHistogram[i] + const h = yhistogram(v.map(x => x.indexOf(searchPattern)), [-1, k - searchPattern.length], i === 0) + if (i === 0) length = h.length + 1 + console.log(`${h.padEnd(length, i % 2 === 0 ? "⸱" : " ")}length ${k.toString().padEnd(3)} ${l} ${v.length}`) +} diff --git a/src/d2m/actions/remove-reaction.js b/src/d2m/actions/remove-reaction.js index d991f08..6f922cb 100644 --- a/src/d2m/actions/remove-reaction.js +++ b/src/d2m/actions/remove-reaction.js @@ -53,7 +53,7 @@ async function removeReaction(data, reactions) { */ async function removeEmojiReaction(data, reactions) { const key = await emojiToKey.emojiToKey(data.emoji) - const discordPreferredEncoding = emoji.encodeEmoji(key, undefined) + const discordPreferredEncoding = await emoji.encodeEmoji(key, undefined) db.prepare("DELETE FROM reaction WHERE message_id = ? AND encoded_emoji = ?").run(data.message_id, discordPreferredEncoding) return converter.removeEmojiReaction(data, reactions, key) diff --git a/src/m2d/actions/add-reaction.js b/src/m2d/actions/add-reaction.js index cfd471b..d54bf77 100644 --- a/src/m2d/actions/add-reaction.js +++ b/src/m2d/actions/add-reaction.js @@ -20,7 +20,7 @@ async function addReaction(event) { if (!messageID) return // Nothing can be done if the parent message was never bridged. const key = event.content["m.relates_to"].key - const discordPreferredEncoding = emoji.encodeEmoji(key, event.content.shortcode) + const discordPreferredEncoding = await emoji.encodeEmoji(key, event.content.shortcode) if (!discordPreferredEncoding) return await discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding) // acting as the discord bot itself diff --git a/src/m2d/converters/emoji.js b/src/m2d/converters/emoji.js index 66a690c..63e53a9 100644 --- a/src/m2d/converters/emoji.js +++ b/src/m2d/converters/emoji.js @@ -1,58 +1,98 @@ // @ts-check -const assert = require("assert").strict -const Ty = require("../../types") +const fsp = require("fs").promises +const {join} = require("path") +const emojisp = fsp.readFile(join(__dirname, "emojis.txt"), "utf8").then(content => content.split("\n")) const passthrough = require("../../passthrough") -const {sync, select} = passthrough +const {select} = passthrough + /** * @param {string} input * @param {string | null | undefined} shortcode * @returns {string?} */ -function encodeEmoji(input, shortcode) { - let discordPreferredEncoding - if (input.startsWith("mxc://")) { - // Custom emoji - let row = select("emoji", ["emoji_id", "name"], {mxc_url: input}).get() - if (!row && shortcode) { - // Use the name to try to find a known emoji with the same name. - const name = shortcode.replace(/^:|:$/g, "") - row = select("emoji", ["emoji_id", "name"], {name: name}).get() - } - if (!row) { - // We don't have this emoji and there's no realistic way to just-in-time upload a new emoji somewhere. - // Sucks! - return null - } - // Cool, we got an exact or a candidate emoji. - discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.emoji_id}`) - } else { - // Default emoji - // https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ???????????? - const encoded = encodeURIComponent(input) - const encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") - - const forceTrimmedList = [ - "%F0%9F%91%8D", // 👍 - "%F0%9F%91%8E", // 👎️ - "%E2%AD%90", // ⭐ - "%F0%9F%90%88", // 🐈 - "%E2%9D%93", // ❓ - "%F0%9F%8F%86", // 🏆️ - "%F0%9F%93%9A", // 📚️ - "%F0%9F%90%9F", // 🐟️ - ] - - discordPreferredEncoding = - ( forceTrimmedList.includes(encodedTrimmed) ? encodedTrimmed - : encodedTrimmed !== encoded && [...input].length === 2 ? encoded - : encodedTrimmed) - - console.log("add reaction from matrix:", input, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding) +function encodeCustomEmoji(input, shortcode) { + // Custom emoji + let row = select("emoji", ["emoji_id", "name"], {mxc_url: input}).get() + if (!row && shortcode) { + // Use the name to try to find a known emoji with the same name. + const name = shortcode.replace(/^:|:$/g, "") + row = select("emoji", ["emoji_id", "name"], {name: name}).get() + } + if (!row) { + // We don't have this emoji and there's no realistic way to just-in-time upload a new emoji somewhere. Sucks! + return null + } + return encodeURIComponent(`${row.name}:${row.emoji_id}`) +} + +/** + * @param {string} input + * @returns {Promise} URL encoded! + */ +async function encodeDefaultEmoji(input) { + // Default emoji + + // Shortcut: If there are ASCII letters then it's not an emoji, it's a freeform Matrix text reaction. + // (Regional indicator letters are not ASCII. ASCII digits might be part of an emoji.) + if (input.match(/[A-Za-z]/)) return null + + // Check against the dataset + const emojis = await emojisp + const encoded = encodeURIComponent(input) + + // Best case scenario: they reacted with an exact replica of a valid emoji. + if (emojis.includes(input)) return encoded + + // Maybe it has some extraneous \ufe0f or \ufe0e (at the end or in the middle), and it'll be valid if they're removed. + const trimmed = input.replace(/\ufe0e|\ufe0f/g, "") + const trimmedEncoded = encodeURIComponent(trimmed) + if (trimmed !== input) { + if (emojis.includes(trimmed)) return trimmedEncoded + } + + // Okay, well, maybe it was already missing one and it actually needs an extra \ufe0f, and it'll be valid if that's added. + else { + const appended = input + "\ufe0f" + const appendedEncoded = encodeURIComponent(appended) + if (emojis.includes(appended)) return appendedEncoded + } + + // Hmm, so adding or removing that from the end didn't help, but maybe there needs to be one in the middle? We can try some heuristics. + // These heuristics come from executing scripts/emoji-surrogates-statistics.js. + if (trimmedEncoded.length <= 21 && trimmed.match(/^[*#0-9]/)) { // ->19: Keycap digit? 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ *️⃣ #️⃣ + const keycap = trimmed[0] + "\ufe0f" + trimmed.slice(1) + if (emojis.includes(keycap)) return encodeURIComponent(keycap) + } else if (trimmedEncoded.length === 27 && trimmed[0] === "⛹") { // ->45: ⛹️‍♀️ ⛹️‍♂️ + const balling = trimmed[0] + "\ufe0f" + trimmed.slice(1) + "\ufe0f" + if (emojis.includes(balling)) return encodeURIComponent(balling) + } else if (trimmedEncoded.length === 30) { // ->39: ⛓️‍💥 ❤️‍🩹 ❤️‍🔥 or ->48: 🏳️‍⚧️ 🏌️‍♀️ 🕵️‍♀️ 🏋️‍♀️ and gender variants + const thriving = trimmed[0] + "\ufe0f" + trimmed.slice(1) + if (emojis.includes(thriving)) return encodeURIComponent(thriving) + const powerful = trimmed.slice(0, 2) + "\ufe0f" + trimmed.slice(2) + "\ufe0f" + if (emojis.includes(powerful)) return encodeURIComponent(powerful) + } else if (trimmedEncoded.length === 51 && trimmed[3] === "❤") { // ->60: 👩‍❤️‍👨 👩‍❤️‍👩 👨‍❤️‍👨 + const yellowRomance = trimmed.slice(0, 3) + "❤\ufe0f" + trimmed.slice(4) + if (emojis.includes(yellowRomance)) return encodeURIComponent(yellowRomance) + } + + // there are a few more longer ones but I got bored + return null +} + +/** + * @param {string} input + * @param {string | null | undefined} shortcode + * @returns {Promise} + */ +async function encodeEmoji(input, shortcode) { + if (input.startsWith("mxc://")) { + return encodeCustomEmoji(input, shortcode) + } else { + return encodeDefaultEmoji(input) } - return discordPreferredEncoding } module.exports.encodeEmoji = encodeEmoji diff --git a/src/m2d/converters/emoji.test.js b/src/m2d/converters/emoji.test.js new file mode 100644 index 0000000..ad9846b --- /dev/null +++ b/src/m2d/converters/emoji.test.js @@ -0,0 +1,52 @@ +// @ts-check + +const {test} = require("supertape") +const {encodeEmoji} = require("./emoji") + +test("emoji: valid", async t => { + t.equal(await encodeEmoji("🦄", null), "%F0%9F%A6%84") +}) + +test("emoji: freeform text", async t => { + t.equal(await encodeEmoji("ha", null), null) +}) + +test("emoji: suspicious unicode", async t => { + t.equal(await encodeEmoji("Ⓐ", null), null) +}) + +test("emoji: needs u+fe0f added", async t => { + t.equal(await encodeEmoji("☺", null), "%E2%98%BA%EF%B8%8F") +}) + +test("emoji: needs u+fe0f removed", async t => { + t.equal(await encodeEmoji("⭐️", null), "%E2%AD%90") +}) + +test("emoji: number key needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("3⃣", null), "3%EF%B8%8F%E2%83%A3") +}) + +test("emoji: hash key needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("#⃣", null), "%23%EF%B8%8F%E2%83%A3") +}) + +test("emoji: broken chains needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("⛓‍💥", null), "%E2%9B%93%EF%B8%8F%E2%80%8D%F0%9F%92%A5") +}) + +test("emoji: balling needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("⛹‍♀", null), "%E2%9B%B9%EF%B8%8F%E2%80%8D%E2%99%80%EF%B8%8F") +}) + +test("emoji: trans flag needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("🏳‍⚧", null), "%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%E2%9A%A7%EF%B8%8F") +}) + +test("emoji: spy needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("🕵‍♀", null), "%F0%9F%95%B5%EF%B8%8F%E2%80%8D%E2%99%80%EF%B8%8F") +}) + +test("emoji: couple needs u+fe0f in the middle", async t => { + t.equal(await encodeEmoji("👩‍❤‍👩", null), "%F0%9F%91%A9%E2%80%8D%E2%9D%A4%EF%B8%8F%E2%80%8D%F0%9F%91%A9") +}) diff --git a/src/m2d/converters/emojis.txt b/src/m2d/converters/emojis.txt new file mode 100644 index 0000000..32c3f15 --- /dev/null +++ b/src/m2d/converters/emojis.txt @@ -0,0 +1,3799 @@ +😀 +😃 +😄 +😁 +😆 +🥹 +😅 +😂 +🤣 +🥲 +☺️ +😊 +😇 +🙂 +🙃 +😉 +😌 +😍 +🥰 +😘 +😗 +😙 +😚 +😋 +😛 +😝 +😜 +🤪 +🤨 +🧐 +🤓 +😎 +🥸 +🤩 +🥳 +😏 +😒 +😞 +😔 +😟 +😕 +🙁 +☹️ +😣 +😖 +😫 +😩 +🥺 +😢 +😭 +😤 +😠 +😡 +🤬 +🤯 +😳 +🥵 +🥶 +😶‍🌫️ +😱 +😨 +😰 +😥 +😓 +🤗 +🤔 +🫣 +🤭 +🫢 +🫡 +🤫 +🫠 +🤥 +😶 +🫥 +😐 +🫤 +😑 +🫨 +🙂‍↔️ +🙂‍↕️ +😬 +🙄 +😯 +😦 +😧 +😮 +😲 +🥱 +😴 +🤤 +😪 +😮‍💨 +😵 +😵‍💫 +🤐 +🥴 +🤢 +🤮 +🤧 +😷 +🤒 +🤕 +🤑 +🤠 +😈 +👿 +👹 +👺 +🤡 +💩 +👻 +💀 +☠️ +👽 +👾 +🤖 +🎃 +😺 +😸 +😹 +😻 +😼 +😽 +🙀 +😿 +😾 +🤝🏻 +🫱🏻‍🫲🏼 +🫱🏻‍🫲🏽 +🫱🏻‍🫲🏾 +🫱🏻‍🫲🏿 +🫱🏼‍🫲🏻 +🤝🏼 +🫱🏼‍🫲🏽 +🫱🏼‍🫲🏾 +🫱🏼‍🫲🏿 +🫱🏽‍🫲🏻 +🫱🏽‍🫲🏼 +🤝🏽 +🫱🏽‍🫲🏾 +🫱🏽‍🫲🏿 +🫱🏾‍🫲🏻 +🫱🏾‍🫲🏼 +🫱🏾‍🫲🏽 +🤝🏾 +🫱🏾‍🫲🏿 +🫱🏿‍🫲🏻 +🫱🏿‍🫲🏼 +🫱🏿‍🫲🏽 +🫱🏿‍🫲🏾 +🤝🏿 +🤝 +🫶🏻 +🫶🏼 +🫶🏽 +🫶🏾 +🫶🏿 +🫶 +🤲🏻 +🤲🏼 +🤲🏽 +🤲🏾 +🤲🏿 +🤲 +👐🏻 +👐🏼 +👐🏽 +👐🏾 +👐🏿 +👐 +🙌🏻 +🙌🏼 +🙌🏽 +🙌🏾 +🙌🏿 +🙌 +👏🏻 +👏🏼 +👏🏽 +👏🏾 +👏🏿 +👏 +👍🏻 +👍🏼 +👍🏽 +👍🏾 +👍🏿 +👍 +👎🏻 +👎🏼 +👎🏽 +👎🏾 +👎🏿 +👎 +👊🏻 +👊🏼 +👊🏽 +👊🏾 +👊🏿 +👊 +✊🏻 +✊🏼 +✊🏽 +✊🏾 +✊🏿 +✊ +🤛🏻 +🤛🏼 +🤛🏽 +🤛🏾 +🤛🏿 +🤛 +🤜🏻 +🤜🏼 +🤜🏽 +🤜🏾 +🤜🏿 +🤜 +🫷🏻 +🫷🏼 +🫷🏽 +🫷🏾 +🫷🏿 +🫷 +🫸🏻 +🫸🏼 +🫸🏽 +🫸🏾 +🫸🏿 +🫸 +🤞🏻 +🤞🏼 +🤞🏽 +🤞🏾 +🤞🏿 +🤞 +✌🏻 +✌🏼 +✌🏽 +✌🏾 +✌🏿 +✌️ +🫰🏻 +🫰🏼 +🫰🏽 +🫰🏾 +🫰🏿 +🫰 +🤟🏻 +🤟🏼 +🤟🏽 +🤟🏾 +🤟🏿 +🤟 +🤘🏻 +🤘🏼 +🤘🏽 +🤘🏾 +🤘🏿 +🤘 +👌🏻 +👌🏼 +👌🏽 +👌🏾 +👌🏿 +👌 +🤌🏼 +🤌🏻 +🤌🏽 +🤌🏾 +🤌🏿 +🤌 +🤏🏻 +🤏🏼 +🤏🏽 +🤏🏾 +🤏🏿 +🤏 +🫳🏻 +🫳🏼 +🫳🏽 +🫳🏾 +🫳🏿 +🫳 +🫴🏻 +🫴🏼 +🫴🏽 +🫴🏾 +🫴🏿 +🫴 +👈🏻 +👈🏼 +👈🏽 +👈🏾 +👈🏿 +👈 +👉🏻 +👉🏼 +👉🏽 +👉🏾 +👉🏿 +👉 +👆🏻 +👆🏼 +👆🏽 +👆🏾 +👆🏿 +👆 +👇🏻 +👇🏼 +👇🏽 +👇🏾 +👇🏿 +👇 +☝🏻 +☝🏼 +☝🏽 +☝🏾 +☝🏿 +☝️ +✋🏻 +✋🏼 +✋🏽 +✋🏾 +✋🏿 +✋ +🤚🏻 +🤚🏼 +🤚🏽 +🤚🏾 +🤚🏿 +🤚 +🖐🏻 +🖐🏼 +🖐🏽 +🖐🏾 +🖐🏿 +🖐️ +🖖🏻 +🖖🏼 +🖖🏽 +🖖🏾 +🖖🏿 +🖖 +👋🏻 +👋🏼 +👋🏽 +👋🏾 +👋🏿 +👋 +🤙🏻 +🤙🏼 +🤙🏽 +🤙🏾 +🤙🏿 +🤙 +🫲🏻 +🫲🏼 +🫲🏽 +🫲🏾 +🫲🏿 +🫲 +🫱🏻 +🫱🏼 +🫱🏽 +🫱🏾 +🫱🏿 +🫱 +💪🏻 +💪🏼 +💪🏽 +💪🏾 +💪🏿 +💪 +🦾 +🖕🏻 +🖕🏼 +🖕🏽 +🖕🏾 +🖕🏿 +🖕 +✍🏻 +✍🏼 +✍🏽 +✍🏾 +✍🏿 +✍️ +🙏🏻 +🙏🏼 +🙏🏽 +🙏🏾 +🙏🏿 +🙏 +🫵🏻 +🫵🏼 +🫵🏽 +🫵🏾 +🫵🏿 +🫵 +🦶🏻 +🦶🏼 +🦶🏽 +🦶🏾 +🦶🏿 +🦶 +🦵🏻 +🦵🏼 +🦵🏽 +🦵🏾 +🦵🏿 +🦵 +🦿 +💄 +💋 +👄 +🫦 +🦷 +👅 +👂🏻 +👂🏼 +👂🏽 +👂🏾 +👂🏿 +👂 +🦻🏻 +🦻🏼 +🦻🏽 +🦻🏾 +🦻🏿 +🦻 +👃🏻 +👃🏼 +👃🏽 +👃🏾 +👃🏿 +👃 +👣 +👁️ +👀 +🫀 +🫁 +🧠 +🗣️ +👤 +👥 +🫂 +👶🏻 +👶🏼 +👶🏽 +👶🏾 +👶🏿 +👶 +🧒🏻 +🧒🏼 +🧒🏽 +🧒🏾 +🧒🏿 +🧒 +👧🏻 +👧🏼 +👧🏽 +👧🏾 +👧🏿 +👧 +👦🏻 +👦🏼 +👦🏽 +👦🏾 +👦🏿 +👦 +🧑🏻 +🧑🏼 +🧑🏽 +🧑🏾 +🧑🏿 +🧑 +👩🏻 +👩🏼 +👩🏽 +👩🏾 +👩🏿 +👩 +👨🏻 +👨🏼 +👨🏽 +👨🏾 +👨🏿 +👨 +🧑🏻‍🦱 +🧑🏼‍🦱 +🧑🏽‍🦱 +🧑🏾‍🦱 +🧑🏿‍🦱 +🧑‍🦱 +👩🏻‍🦱 +👩🏼‍🦱 +👩🏽‍🦱 +👩🏾‍🦱 +👩🏿‍🦱 +👩‍🦱 +👨🏻‍🦱 +👨🏼‍🦱 +👨🏽‍🦱 +👨🏾‍🦱 +👨🏿‍🦱 +👨‍🦱 +🧑🏻‍🦰 +🧑🏼‍🦰 +🧑🏽‍🦰 +🧑🏾‍🦰 +🧑🏿‍🦰 +🧑‍🦰 +👩🏻‍🦰 +👩🏼‍🦰 +👩🏽‍🦰 +👩🏾‍🦰 +👩🏿‍🦰 +👩‍🦰 +👨🏻‍🦰 +👨🏼‍🦰 +👨🏽‍🦰 +👨🏾‍🦰 +👨🏿‍🦰 +👨‍🦰 +👱🏻 +👱🏼 +👱🏽 +👱🏾 +👱🏿 +👱 +👱🏻‍♀️ +👱🏼‍♀️ +👱🏽‍♀️ +👱🏾‍♀️ +👱🏿‍♀️ +👱‍♀️ +👱🏻‍♂️ +👱🏼‍♂️ +👱🏽‍♂️ +👱🏾‍♂️ +👱🏿‍♂️ +👱‍♂️ +🧑🏻‍🦳 +🧑🏼‍🦳 +🧑🏽‍🦳 +🧑🏾‍🦳 +🧑🏿‍🦳 +🧑‍🦳 +👩🏻‍🦳 +👩🏼‍🦳 +👩🏽‍🦳 +👩🏾‍🦳 +👩🏿‍🦳 +👩‍🦳 +👨🏻‍🦳 +👨🏼‍🦳 +👨🏽‍🦳 +👨🏾‍🦳 +👨🏿‍🦳 +👨‍🦳 +🧑🏻‍🦲 +🧑🏼‍🦲 +🧑🏽‍🦲 +🧑🏾‍🦲 +🧑🏿‍🦲 +🧑‍🦲 +👩🏻‍🦲 +👩🏼‍🦲 +👩🏽‍🦲 +👩🏾‍🦲 +👩🏿‍🦲 +👩‍🦲 +👨🏻‍🦲 +👨🏼‍🦲 +👨🏽‍🦲 +👨🏾‍🦲 +👨🏿‍🦲 +👨‍🦲 +🧔🏻 +🧔🏼 +🧔🏽 +🧔🏾 +🧔🏿 +🧔 +🧔🏻‍♀️ +🧔🏼‍♀️ +🧔🏽‍♀️ +🧔🏾‍♀️ +🧔🏿‍♀️ +🧔‍♀️ +🧔🏻‍♂️ +🧔🏼‍♂️ +🧔🏽‍♂️ +🧔🏾‍♂️ +🧔🏿‍♂️ +🧔‍♂️ +🧓🏻 +🧓🏼 +🧓🏽 +🧓🏾 +🧓🏿 +🧓 +👵🏻 +👵🏼 +👵🏽 +👵🏾 +👵🏿 +👵 +👴🏻 +👴🏼 +👴🏽 +👴🏾 +👴🏿 +👴 +👲🏻 +👲🏼 +👲🏽 +👲🏾 +👲🏿 +👲 +👳🏻 +👳🏼 +👳🏽 +👳🏾 +👳🏿 +👳 +👳🏻‍♀️ +👳🏼‍♀️ +👳🏽‍♀️ +👳🏾‍♀️ +👳🏿‍♀️ +👳‍♀️ +👳🏻‍♂️ +👳🏼‍♂️ +👳🏽‍♂️ +👳🏾‍♂️ +👳🏿‍♂️ +👳‍♂️ +🧕🏻 +🧕🏼 +🧕🏽 +🧕🏾 +🧕🏿 +🧕 +👮🏻 +👮🏼 +👮🏽 +👮🏾 +👮🏿 +👮 +👮🏻‍♀️ +👮🏼‍♀️ +👮🏽‍♀️ +👮🏾‍♀️ +👮🏿‍♀️ +👮‍♀️ +👮🏻‍♂️ +👮🏼‍♂️ +👮🏽‍♂️ +👮🏾‍♂️ +👮🏿‍♂️ +👮‍♂️ +👷🏻 +👷🏼 +👷🏽 +👷🏾 +👷🏿 +👷 +👷🏻‍♀️ +👷🏼‍♀️ +👷🏽‍♀️ +👷🏾‍♀️ +👷🏿‍♀️ +👷‍♀️ +👷🏻‍♂️ +👷🏼‍♂️ +👷🏽‍♂️ +👷🏾‍♂️ +👷🏿‍♂️ +👷‍♂️ +💂🏻 +💂🏼 +💂🏽 +💂🏾 +💂🏿 +💂 +💂🏻‍♀️ +💂🏼‍♀️ +💂🏽‍♀️ +💂🏾‍♀️ +💂🏿‍♀️ +💂‍♀️ +💂🏻‍♂️ +💂🏼‍♂️ +💂🏽‍♂️ +💂🏾‍♂️ +💂🏿‍♂️ +💂‍♂️ +🕵🏻 +🕵🏼 +🕵🏽 +🕵🏾 +🕵🏿 +🕵️ +🕵🏻‍♀️ +🕵🏼‍♀️ +🕵🏽‍♀️ +🕵🏾‍♀️ +🕵🏿‍♀️ +🕵️‍♀️ +🕵🏻‍♂️ +🕵🏼‍♂️ +🕵🏽‍♂️ +🕵🏾‍♂️ +🕵🏿‍♂️ +🕵️‍♂️ +🧑🏻‍⚕️ +🧑🏼‍⚕️ +🧑🏽‍⚕️ +🧑🏾‍⚕️ +🧑🏿‍⚕️ +🧑‍⚕️ +👩🏻‍⚕️ +👩🏼‍⚕️ +👩🏽‍⚕️ +👩🏾‍⚕️ +👩🏿‍⚕️ +👩‍⚕️ +👨🏻‍⚕️ +👨🏼‍⚕️ +👨🏽‍⚕️ +👨🏾‍⚕️ +👨🏿‍⚕️ +👨‍⚕️ +🧑🏻‍🌾 +🧑🏼‍🌾 +🧑🏽‍🌾 +🧑🏾‍🌾 +🧑🏿‍🌾 +🧑‍🌾 +👩🏻‍🌾 +👩🏼‍🌾 +👩🏽‍🌾 +👩🏾‍🌾 +👩🏿‍🌾 +👩‍🌾 +👨🏻‍🌾 +👨🏼‍🌾 +👨🏽‍🌾 +👨🏾‍🌾 +👨🏿‍🌾 +👨‍🌾 +🧑🏻‍🍳 +🧑🏼‍🍳 +🧑🏽‍🍳 +🧑🏾‍🍳 +🧑🏿‍🍳 +🧑‍🍳 +👩🏻‍🍳 +👩🏼‍🍳 +👩🏽‍🍳 +👩🏾‍🍳 +👩🏿‍🍳 +👩‍🍳 +👨🏻‍🍳 +👨🏼‍🍳 +👨🏽‍🍳 +👨🏾‍🍳 +👨🏿‍🍳 +👨‍🍳 +🧑🏻‍🎓 +🧑🏼‍🎓 +🧑🏽‍🎓 +🧑🏾‍🎓 +🧑🏿‍🎓 +🧑‍🎓 +👩🏻‍🎓 +👩🏼‍🎓 +👩🏽‍🎓 +👩🏾‍🎓 +👩🏿‍🎓 +👩‍🎓 +👨🏻‍🎓 +👨🏼‍🎓 +👨🏽‍🎓 +👨🏾‍🎓 +👨🏿‍🎓 +👨‍🎓 +🧑🏻‍🎤 +🧑🏼‍🎤 +🧑🏽‍🎤 +🧑🏾‍🎤 +🧑🏿‍🎤 +🧑‍🎤 +👩🏻‍🎤 +👩🏼‍🎤 +👩🏽‍🎤 +👩🏾‍🎤 +👩🏿‍🎤 +👩‍🎤 +👨🏻‍🎤 +👨🏼‍🎤 +👨🏽‍🎤 +👨🏾‍🎤 +👨🏿‍🎤 +👨‍🎤 +🧑🏻‍🏫 +🧑🏼‍🏫 +🧑🏽‍🏫 +🧑🏾‍🏫 +🧑🏿‍🏫 +🧑‍🏫 +👩🏻‍🏫 +👩🏼‍🏫 +👩🏽‍🏫 +👩🏾‍🏫 +👩🏿‍🏫 +👩‍🏫 +👨🏻‍🏫 +👨🏼‍🏫 +👨🏽‍🏫 +👨🏾‍🏫 +👨🏿‍🏫 +👨‍🏫 +🧑🏻‍🏭 +🧑🏼‍🏭 +🧑🏽‍🏭 +🧑🏾‍🏭 +🧑🏿‍🏭 +🧑‍🏭 +👩🏻‍🏭 +👩🏼‍🏭 +👩🏽‍🏭 +👩🏾‍🏭 +👩🏿‍🏭 +👩‍🏭 +👨🏻‍🏭 +👨🏼‍🏭 +👨🏽‍🏭 +👨🏾‍🏭 +👨🏿‍🏭 +👨‍🏭 +🧑🏻‍💻 +🧑🏼‍💻 +🧑🏽‍💻 +🧑🏾‍💻 +🧑🏿‍💻 +🧑‍💻 +👩🏻‍💻 +👩🏼‍💻 +👩🏽‍💻 +👩🏾‍💻 +👩🏿‍💻 +👩‍💻 +👨🏻‍💻 +👨🏼‍💻 +👨🏽‍💻 +👨🏾‍💻 +👨🏿‍💻 +👨‍💻 +🧑🏻‍💼 +🧑🏼‍💼 +🧑🏽‍💼 +🧑🏾‍💼 +🧑🏿‍💼 +🧑‍💼 +👩🏻‍💼 +👩🏼‍💼 +👩🏽‍💼 +👩🏾‍💼 +👩🏿‍💼 +👩‍💼 +👨🏻‍💼 +👨🏼‍💼 +👨🏽‍💼 +👨🏾‍💼 +👨🏿‍💼 +👨‍💼 +🧑🏻‍🔧 +🧑🏼‍🔧 +🧑🏽‍🔧 +🧑🏾‍🔧 +🧑🏿‍🔧 +🧑‍🔧 +👩🏻‍🔧 +👩🏼‍🔧 +👩🏽‍🔧 +👩🏾‍🔧 +👩🏿‍🔧 +👩‍🔧 +👨🏻‍🔧 +👨🏼‍🔧 +👨🏽‍🔧 +👨🏾‍🔧 +👨🏿‍🔧 +👨‍🔧 +🧑🏻‍🔬 +🧑🏼‍🔬 +🧑🏽‍🔬 +🧑🏾‍🔬 +🧑🏿‍🔬 +🧑‍🔬 +👩🏻‍🔬 +👩🏼‍🔬 +👩🏽‍🔬 +👩🏾‍🔬 +👩🏿‍🔬 +👩‍🔬 +👨🏻‍🔬 +👨🏼‍🔬 +👨🏽‍🔬 +👨🏾‍🔬 +👨🏿‍🔬 +👨‍🔬 +🧑🏻‍🎨 +🧑🏼‍🎨 +🧑🏽‍🎨 +🧑🏾‍🎨 +🧑🏿‍🎨 +🧑‍🎨 +👩🏻‍🎨 +👩🏼‍🎨 +👩🏽‍🎨 +👩🏾‍🎨 +👩🏿‍🎨 +👩‍🎨 +👨🏻‍🎨 +👨🏼‍🎨 +👨🏽‍🎨 +👨🏾‍🎨 +👨🏿‍🎨 +👨‍🎨 +🧑🏻‍🚒 +🧑🏼‍🚒 +🧑🏽‍🚒 +🧑🏾‍🚒 +🧑🏿‍🚒 +🧑‍🚒 +👩🏻‍🚒 +👩🏼‍🚒 +👩🏽‍🚒 +👩🏾‍🚒 +👩🏿‍🚒 +👩‍🚒 +👨🏻‍🚒 +👨🏼‍🚒 +👨🏽‍🚒 +👨🏾‍🚒 +👨🏿‍🚒 +👨‍🚒 +🧑🏻‍✈️ +🧑🏼‍✈️ +🧑🏽‍✈️ +🧑🏾‍✈️ +🧑🏿‍✈️ +🧑‍✈️ +👩🏻‍✈️ +👩🏼‍✈️ +👩🏽‍✈️ +👩🏾‍✈️ +👩🏿‍✈️ +👩‍✈️ +👨🏻‍✈️ +👨🏼‍✈️ +👨🏽‍✈️ +👨🏾‍✈️ +👨🏿‍✈️ +👨‍✈️ +🧑🏻‍🚀 +🧑🏼‍🚀 +🧑🏽‍🚀 +🧑🏾‍🚀 +🧑🏿‍🚀 +🧑‍🚀 +👩🏻‍🚀 +👩🏼‍🚀 +👩🏽‍🚀 +👩🏾‍🚀 +👩🏿‍🚀 +👩‍🚀 +👨🏻‍🚀 +👨🏼‍🚀 +👨🏽‍🚀 +👨🏾‍🚀 +👨🏿‍🚀 +👨‍🚀 +🧑🏻‍⚖️ +🧑🏼‍⚖️ +🧑🏽‍⚖️ +🧑🏾‍⚖️ +🧑🏿‍⚖️ +🧑‍⚖️ +👩🏻‍⚖️ +👩🏼‍⚖️ +👩🏽‍⚖️ +👩🏾‍⚖️ +👩🏿‍⚖️ +👩‍⚖️ +👨🏻‍⚖️ +👨🏼‍⚖️ +👨🏽‍⚖️ +👨🏾‍⚖️ +👨🏿‍⚖️ +👨‍⚖️ +👰🏻 +👰🏼 +👰🏽 +👰🏾 +👰🏿 +👰 +👰🏻‍♀️ +👰🏼‍♀️ +👰🏽‍♀️ +👰🏾‍♀️ +👰🏿‍♀️ +👰‍♀️ +👰🏻‍♂️ +👰🏼‍♂️ +👰🏽‍♂️ +👰🏾‍♂️ +👰🏿‍♂️ +👰‍♂️ +🤵🏻 +🤵🏼 +🤵🏽 +🤵🏾 +🤵🏿 +🤵 +🤵🏻‍♀️ +🤵🏼‍♀️ +🤵🏽‍♀️ +🤵🏾‍♀️ +🤵🏿‍♀️ +🤵‍♀️ +🤵🏻‍♂️ +🤵🏼‍♂️ +🤵🏽‍♂️ +🤵🏾‍♂️ +🤵🏿‍♂️ +🤵‍♂️ +🫅🏻 +🫅🏼 +🫅🏽 +🫅🏾 +🫅🏿 +🫅 +👸🏻 +👸🏼 +👸🏽 +👸🏾 +👸🏿 +👸 +🤴🏻 +🤴🏼 +🤴🏽 +🤴🏾 +🤴🏿 +🤴 +🦸🏻 +🦸🏼 +🦸🏽 +🦸🏾 +🦸🏿 +🦸 +🦸🏻‍♀️ +🦸🏼‍♀️ +🦸🏽‍♀️ +🦸🏾‍♀️ +🦸🏿‍♀️ +🦸‍♀️ +🦸🏻‍♂️ +🦸🏼‍♂️ +🦸🏽‍♂️ +🦸🏾‍♂️ +🦸🏿‍♂️ +🦸‍♂️ +🦹🏻 +🦹🏼 +🦹🏽 +🦹🏾 +🦹🏿 +🦹 +🦹🏻‍♀️ +🦹🏼‍♀️ +🦹🏽‍♀️ +🦹🏾‍♀️ +🦹🏿‍♀️ +🦹‍♀️ +🦹🏻‍♂️ +🦹🏼‍♂️ +🦹🏽‍♂️ +🦹🏾‍♂️ +🦹🏿‍♂️ +🦹‍♂️ +🥷🏻 +🥷🏼 +🥷🏽 +🥷🏾 +🥷🏿 +🥷 +🧑🏻‍🎄 +🧑🏼‍🎄 +🧑🏽‍🎄 +🧑🏾‍🎄 +🧑🏿‍🎄 +🧑‍🎄 +🤶🏻 +🤶🏼 +🤶🏽 +🤶🏾 +🤶🏿 +🤶 +🎅🏻 +🎅🏼 +🎅🏽 +🎅🏾 +🎅🏿 +🎅 +🧙🏻 +🧙🏼 +🧙🏽 +🧙🏾 +🧙🏿 +🧙 +🧙🏻‍♀️ +🧙🏼‍♀️ +🧙🏽‍♀️ +🧙🏾‍♀️ +🧙🏿‍♀️ +🧙‍♀️ +🧙🏻‍♂️ +🧙🏼‍♂️ +🧙🏽‍♂️ +🧙🏾‍♂️ +🧙🏿‍♂️ +🧙‍♂️ +🧝🏻 +🧝🏼 +🧝🏽 +🧝🏾 +🧝🏿 +🧝 +🧝🏻‍♀️ +🧝🏼‍♀️ +🧝🏽‍♀️ +🧝🏾‍♀️ +🧝🏿‍♀️ +🧝‍♀️ +🧝🏻‍♂️ +🧝🏼‍♂️ +🧝🏽‍♂️ +🧝🏾‍♂️ +🧝🏿‍♂️ +🧝‍♂️ +🧌 +🧛🏻 +🧛🏼 +🧛🏽 +🧛🏾 +🧛🏿 +🧛 +🧛🏻‍♀️ +🧛🏼‍♀️ +🧛🏽‍♀️ +🧛🏾‍♀️ +🧛🏿‍♀️ +🧛‍♀️ +🧛🏻‍♂️ +🧛🏼‍♂️ +🧛🏽‍♂️ +🧛🏾‍♂️ +🧛🏿‍♂️ +🧛‍♂️ +🧟 +🧟‍♀️ +🧟‍♂️ +🧞 +🧞‍♀️ +🧞‍♂️ +🧜🏻 +🧜🏼 +🧜🏽 +🧜🏾 +🧜🏿 +🧜 +🧜🏻‍♀️ +🧜🏼‍♀️ +🧜🏽‍♀️ +🧜🏾‍♀️ +🧜🏿‍♀️ +🧜‍♀️ +🧜🏻‍♂️ +🧜🏼‍♂️ +🧜🏽‍♂️ +🧜🏾‍♂️ +🧜🏿‍♂️ +🧜‍♂️ +🧚🏻 +🧚🏼 +🧚🏽 +🧚🏾 +🧚🏿 +🧚 +🧚🏻‍♀️ +🧚🏼‍♀️ +🧚🏽‍♀️ +🧚🏾‍♀️ +🧚🏿‍♀️ +🧚‍♀️ +🧚🏻‍♂️ +🧚🏼‍♂️ +🧚🏽‍♂️ +🧚🏾‍♂️ +🧚🏿‍♂️ +🧚‍♂️ +👼🏻 +👼🏼 +👼🏽 +👼🏾 +👼🏿 +👼 +🫄🏻 +🫄🏼 +🫄🏽 +🫄🏾 +🫄🏿 +🫄 +🤰🏻 +🤰🏼 +🤰🏽 +🤰🏾 +🤰🏿 +🤰 +🫃🏻 +🫃🏼 +🫃🏽 +🫃🏾 +🫃🏿 +🫃 +🤱🏻 +🤱🏼 +🤱🏽 +🤱🏾 +🤱🏿 +🤱 +🧑🏻‍🍼 +🧑🏼‍🍼 +🧑🏽‍🍼 +🧑🏾‍🍼 +🧑🏿‍🍼 +🧑‍🍼 +👩🏻‍🍼 +👩🏼‍🍼 +👩🏽‍🍼 +👩🏾‍🍼 +👩🏿‍🍼 +👩‍🍼 +👨🏻‍🍼 +👨🏼‍🍼 +👨🏽‍🍼 +👨🏾‍🍼 +👨🏿‍🍼 +👨‍🍼 +🙇🏻 +🙇🏼 +🙇🏽 +🙇🏾 +🙇🏿 +🙇 +🙇🏻‍♀️ +🙇🏼‍♀️ +🙇🏽‍♀️ +🙇🏾‍♀️ +🙇🏿‍♀️ +🙇‍♀️ +🙇🏻‍♂️ +🙇🏼‍♂️ +🙇🏽‍♂️ +🙇🏾‍♂️ +🙇🏿‍♂️ +🙇‍♂️ +💁🏻 +💁🏼 +💁🏽 +💁🏾 +💁🏿 +💁 +💁🏻‍♀️ +💁🏼‍♀️ +💁🏽‍♀️ +💁🏾‍♀️ +💁🏿‍♀️ +💁‍♀️ +💁🏻‍♂️ +💁🏼‍♂️ +💁🏽‍♂️ +💁🏾‍♂️ +💁🏿‍♂️ +💁‍♂️ +🙅🏻 +🙅🏼 +🙅🏽 +🙅🏾 +🙅🏿 +🙅 +🙅🏻‍♀️ +🙅🏼‍♀️ +🙅🏽‍♀️ +🙅🏾‍♀️ +🙅🏿‍♀️ +🙅‍♀️ +🙅🏻‍♂️ +🙅🏼‍♂️ +🙅🏽‍♂️ +🙅🏾‍♂️ +🙅🏿‍♂️ +🙅‍♂️ +🙆🏻 +🙆🏼 +🙆🏽 +🙆🏾 +🙆🏿 +🙆 +🙆🏻‍♀️ +🙆🏼‍♀️ +🙆🏽‍♀️ +🙆🏾‍♀️ +🙆🏿‍♀️ +🙆‍♀️ +🙆🏻‍♂️ +🙆🏼‍♂️ +🙆🏽‍♂️ +🙆🏾‍♂️ +🙆🏿‍♂️ +🙆‍♂️ +🙋🏻 +🙋🏼 +🙋🏽 +🙋🏾 +🙋🏿 +🙋 +🙋🏻‍♀️ +🙋🏼‍♀️ +🙋🏽‍♀️ +🙋🏾‍♀️ +🙋🏿‍♀️ +🙋‍♀️ +🙋🏻‍♂️ +🙋🏼‍♂️ +🙋🏽‍♂️ +🙋🏾‍♂️ +🙋🏿‍♂️ +🙋‍♂️ +🧏🏻 +🧏🏼 +🧏🏽 +🧏🏾 +🧏🏿 +🧏 +🧏🏻‍♀️ +🧏🏼‍♀️ +🧏🏽‍♀️ +🧏🏾‍♀️ +🧏🏿‍♀️ +🧏‍♀️ +🧏🏻‍♂️ +🧏🏼‍♂️ +🧏🏽‍♂️ +🧏🏾‍♂️ +🧏🏿‍♂️ +🧏‍♂️ +🤦🏻 +🤦🏼 +🤦🏽 +🤦🏾 +🤦🏿 +🤦 +🤦🏻‍♀️ +🤦🏼‍♀️ +🤦🏽‍♀️ +🤦🏾‍♀️ +🤦🏿‍♀️ +🤦‍♀️ +🤦🏻‍♂️ +🤦🏼‍♂️ +🤦🏽‍♂️ +🤦🏾‍♂️ +🤦🏿‍♂️ +🤦‍♂️ +🤷🏻 +🤷🏼 +🤷🏽 +🤷🏾 +🤷🏿 +🤷 +🤷🏻‍♀️ +🤷🏼‍♀️ +🤷🏽‍♀️ +🤷🏾‍♀️ +🤷🏿‍♀️ +🤷‍♀️ +🤷🏻‍♂️ +🤷🏼‍♂️ +🤷🏽‍♂️ +🤷🏾‍♂️ +🤷🏿‍♂️ +🤷‍♂️ +🙎🏻 +🙎🏼 +🙎🏽 +🙎🏾 +🙎🏿 +🙎 +🙎🏻‍♀️ +🙎🏼‍♀️ +🙎🏽‍♀️ +🙎🏾‍♀️ +🙎🏿‍♀️ +🙎‍♀️ +🙎🏻‍♂️ +🙎🏼‍♂️ +🙎🏽‍♂️ +🙎🏾‍♂️ +🙎🏿‍♂️ +🙎‍♂️ +🙍🏻 +🙍🏼 +🙍🏽 +🙍🏾 +🙍🏿 +🙍 +🙍🏻‍♀️ +🙍🏼‍♀️ +🙍🏽‍♀️ +🙍🏾‍♀️ +🙍🏿‍♀️ +🙍‍♀️ +🙍🏻‍♂️ +🙍🏼‍♂️ +🙍🏽‍♂️ +🙍🏾‍♂️ +🙍🏿‍♂️ +🙍‍♂️ +💇🏻 +💇🏼 +💇🏽 +💇🏾 +💇🏿 +💇 +💇🏻‍♀️ +💇🏼‍♀️ +💇🏽‍♀️ +💇🏾‍♀️ +💇🏿‍♀️ +💇‍♀️ +💇🏻‍♂️ +💇🏼‍♂️ +💇🏽‍♂️ +💇🏾‍♂️ +💇🏿‍♂️ +💇‍♂️ +💆🏻 +💆🏼 +💆🏽 +💆🏾 +💆🏿 +💆 +💆🏻‍♀️ +💆🏼‍♀️ +💆🏽‍♀️ +💆🏾‍♀️ +💆🏿‍♀️ +💆‍♀️ +💆🏻‍♂️ +💆🏼‍♂️ +💆🏽‍♂️ +💆🏾‍♂️ +💆🏿‍♂️ +💆‍♂️ +🧖🏻 +🧖🏼 +🧖🏽 +🧖🏾 +🧖🏿 +🧖 +🧖🏻‍♀️ +🧖🏼‍♀️ +🧖🏽‍♀️ +🧖🏾‍♀️ +🧖🏿‍♀️ +🧖‍♀️ +🧖🏻‍♂️ +🧖🏼‍♂️ +🧖🏽‍♂️ +🧖🏾‍♂️ +🧖🏿‍♂️ +🧖‍♂️ +💅🏻 +💅🏼 +💅🏽 +💅🏾 +💅🏿 +💅 +🤳🏻 +🤳🏼 +🤳🏽 +🤳🏾 +🤳🏿 +🤳 +💃🏻 +💃🏼 +💃🏽 +💃🏾 +💃🏿 +💃 +🕺🏻 +🕺🏼 +🕺🏽 +🕺🏿 +🕺🏾 +🕺 +👯 +👯‍♀️ +👯‍♂️ +🕴🏻 +🕴🏼 +🕴🏽 +🕴🏾 +🕴🏿 +🕴️ +🧑🏻‍🦽 +🧑🏼‍🦽 +🧑🏽‍🦽 +🧑🏾‍🦽 +🧑🏿‍🦽 +🧑‍🦽 +👩🏻‍🦽 +👩🏼‍🦽 +👩🏽‍🦽 +👩🏾‍🦽 +👩🏿‍🦽 +👩‍🦽 +👨🏻‍🦽 +👨🏼‍🦽 +👨🏽‍🦽 +👨🏾‍🦽 +👨🏿‍🦽 +👨‍🦽 +🧑🏻‍🦽‍➡️ +🧑🏼‍🦽‍➡️ +🧑🏽‍🦽‍➡️ +🧑🏾‍🦽‍➡️ +🧑🏿‍🦽‍➡️ +🧑‍🦽‍➡️ +👨🏼‍🦽‍➡️ +👨🏻‍🦽‍➡️ +👨🏽‍🦽‍➡️ +👨🏾‍🦽‍➡️ +👨🏿‍🦽‍➡️ +👨‍🦽‍➡️ +👩🏻‍🦽‍➡️ +👩🏼‍🦽‍➡️ +👩🏽‍🦽‍➡️ +👩🏾‍🦽‍➡️ +👩🏿‍🦽‍➡️ +👩‍🦽‍➡️ +🧑🏻‍🦼 +🧑🏼‍🦼 +🧑🏽‍🦼 +🧑🏾‍🦼 +🧑🏿‍🦼 +🧑‍🦼 +👩🏻‍🦼 +👩🏼‍🦼 +👩🏽‍🦼 +👩🏾‍🦼 +👩🏿‍🦼 +👩‍🦼 +👨🏻‍🦼 +👨🏼‍🦼 +👨🏽‍🦼 +👨🏾‍🦼 +👨🏿‍🦼 +👨‍🦼 +🧑🏻‍🦼‍➡️ +🧑🏼‍🦼‍➡️ +🧑🏽‍🦼‍➡️ +🧑🏾‍🦼‍➡️ +🧑🏿‍🦼‍➡️ +🧑‍🦼‍➡️ +👨🏻‍🦼‍➡️ +👨🏼‍🦼‍➡️ +👨🏽‍🦼‍➡️ +👨🏾‍🦼‍➡️ +👨🏿‍🦼‍➡️ +👨‍🦼‍➡️ +👩🏻‍🦼‍➡️ +👩🏼‍🦼‍➡️ +👩🏽‍🦼‍➡️ +👩🏾‍🦼‍➡️ +👩🏿‍🦼‍➡️ +👩‍🦼‍➡️ +🚶🏻 +🚶🏼 +🚶🏽 +🚶🏾 +🚶🏿 +🚶 +🚶🏻‍♀️ +🚶🏼‍♀️ +🚶🏽‍♀️ +🚶🏾‍♀️ +🚶🏿‍♀️ +🚶‍♀️ +🚶🏻‍♂️ +🚶🏼‍♂️ +🚶🏽‍♂️ +🚶🏾‍♂️ +🚶🏿‍♂️ +🚶‍♂️ +🚶🏻‍➡️ +🚶🏼‍➡️ +🚶🏽‍➡️ +🚶🏾‍➡️ +🚶🏿‍➡️ +🚶‍➡️ +🚶🏻‍♀️‍➡️ +🚶🏼‍♀️‍➡️ +🚶🏽‍♀️‍➡️ +🚶🏾‍♀️‍➡️ +🚶🏿‍♀️‍➡️ +🚶‍♀️‍➡️ +🚶🏻‍♂️‍➡️ +🚶🏼‍♂️‍➡️ +🚶🏽‍♂️‍➡️ +🚶🏾‍♂️‍➡️ +🚶🏿‍♂️‍➡️ +🚶‍♂️‍➡️ +🧑🏻‍🦯 +🧑🏼‍🦯 +🧑🏽‍🦯 +🧑🏾‍🦯 +🧑🏿‍🦯 +🧑‍🦯 +👩🏻‍🦯 +👩🏼‍🦯 +👩🏽‍🦯 +👩🏾‍🦯 +👩🏿‍🦯 +👩‍🦯 +👨🏻‍🦯 +👨🏼‍🦯 +👨🏽‍🦯 +👨🏾‍🦯 +👨🏿‍🦯 +👨‍🦯 +🧑🏻‍🦯‍➡️ +🧑🏼‍🦯‍➡️ +🧑🏽‍🦯‍➡️ +🧑🏾‍🦯‍➡️ +🧑🏿‍🦯‍➡️ +🧑‍🦯‍➡️ +👨🏻‍🦯‍➡️ +👨🏼‍🦯‍➡️ +👨🏽‍🦯‍➡️ +👨🏾‍🦯‍➡️ +👨🏿‍🦯‍➡️ +👨‍🦯‍➡️ +👩🏻‍🦯‍➡️ +👩🏼‍🦯‍➡️ +👩🏽‍🦯‍➡️ +👩🏾‍🦯‍➡️ +👩🏿‍🦯‍➡️ +👩‍🦯‍➡️ +🧎🏻 +🧎🏼 +🧎🏽 +🧎🏾 +🧎🏿 +🧎 +🧎🏻‍♀️ +🧎🏼‍♀️ +🧎🏽‍♀️ +🧎🏾‍♀️ +🧎🏿‍♀️ +🧎‍♀️ +🧎🏻‍♂️ +🧎🏼‍♂️ +🧎🏽‍♂️ +🧎🏾‍♂️ +🧎🏿‍♂️ +🧎‍♂️ +🧎🏻‍➡️ +🧎🏼‍➡️ +🧎🏽‍➡️ +🧎🏾‍➡️ +🧎🏿‍➡️ +🧎‍➡️ +🧎🏻‍♀️‍➡️ +🧎🏼‍♀️‍➡️ +🧎🏽‍♀️‍➡️ +🧎🏾‍♀️‍➡️ +🧎🏿‍♀️‍➡️ +🧎‍♀️‍➡️ +🧎🏻‍♂️‍➡️ +🧎🏼‍♂️‍➡️ +🧎🏽‍♂️‍➡️ +🧎🏾‍♂️‍➡️ +🧎🏿‍♂️‍➡️ +🧎‍♂️‍➡️ +🏃🏻 +🏃🏼 +🏃🏽 +🏃🏾 +🏃🏿 +🏃 +🏃🏻‍♀️ +🏃🏼‍♀️ +🏃🏽‍♀️ +🏃🏾‍♀️ +🏃🏿‍♀️ +🏃‍♀️ +🏃🏻‍♂️ +🏃🏼‍♂️ +🏃🏽‍♂️ +🏃🏾‍♂️ +🏃🏿‍♂️ +🏃‍♂️ +🏃🏻‍➡️ +🏃🏼‍➡️ +🏃🏽‍➡️ +🏃🏾‍➡️ +🏃🏿‍➡️ +🏃‍➡️ +🏃🏻‍♀️‍➡️ +🏃🏼‍♀️‍➡️ +🏃🏽‍♀️‍➡️ +🏃🏾‍♀️‍➡️ +🏃🏿‍♀️‍➡️ +🏃‍♀️‍➡️ +🏃🏻‍♂️‍➡️ +🏃🏼‍♂️‍➡️ +🏃🏽‍♂️‍➡️ +🏃🏾‍♂️‍➡️ +🏃🏿‍♂️‍➡️ +🏃‍♂️‍➡️ +🧍🏻 +🧍🏼 +🧍🏽 +🧍🏾 +🧍🏿 +🧍 +🧍🏻‍♀️ +🧍🏼‍♀️ +🧍🏽‍♀️ +🧍🏾‍♀️ +🧍🏿‍♀️ +🧍‍♀️ +🧍🏻‍♂️ +🧍🏼‍♂️ +🧍🏽‍♂️ +🧍🏾‍♂️ +🧍🏿‍♂️ +🧍‍♂️ +🧑🏻‍🤝‍🧑🏻 +🧑🏻‍🤝‍🧑🏼 +🧑🏻‍🤝‍🧑🏽 +🧑🏻‍🤝‍🧑🏾 +🧑🏻‍🤝‍🧑🏿 +🧑🏼‍🤝‍🧑🏻 +🧑🏼‍🤝‍🧑🏼 +🧑🏼‍🤝‍🧑🏽 +🧑🏼‍🤝‍🧑🏾 +🧑🏼‍🤝‍🧑🏿 +🧑🏽‍🤝‍🧑🏻 +🧑🏽‍🤝‍🧑🏼 +🧑🏽‍🤝‍🧑🏽 +🧑🏽‍🤝‍🧑🏾 +🧑🏽‍🤝‍🧑🏿 +🧑🏾‍🤝‍🧑🏻 +🧑🏾‍🤝‍🧑🏼 +🧑🏾‍🤝‍🧑🏽 +🧑🏾‍🤝‍🧑🏾 +🧑🏾‍🤝‍🧑🏿 +🧑🏿‍🤝‍🧑🏻 +🧑🏿‍🤝‍🧑🏼 +🧑🏿‍🤝‍🧑🏽 +🧑🏿‍🤝‍🧑🏾 +🧑🏿‍🤝‍🧑🏿 +🧑‍🤝‍🧑 +👫🏻 +👩🏻‍🤝‍👨🏼 +👩🏻‍🤝‍👨🏽 +👩🏻‍🤝‍👨🏾 +👩🏻‍🤝‍👨🏿 +👩🏼‍🤝‍👨🏻 +👫🏼 +👩🏼‍🤝‍👨🏽 +👩🏼‍🤝‍👨🏾 +👩🏼‍🤝‍👨🏿 +👩🏽‍🤝‍👨🏻 +👩🏽‍🤝‍👨🏼 +👫🏽 +👩🏽‍🤝‍👨🏾 +👩🏽‍🤝‍👨🏿 +👩🏾‍🤝‍👨🏻 +👩🏾‍🤝‍👨🏼 +👩🏾‍🤝‍👨🏽 +👫🏾 +👩🏾‍🤝‍👨🏿 +👩🏿‍🤝‍👨🏻 +👩🏿‍🤝‍👨🏼 +👩🏿‍🤝‍👨🏽 +👩🏿‍🤝‍👨🏾 +👫🏿 +👫 +👭🏻 +👩🏻‍🤝‍👩🏼 +👩🏻‍🤝‍👩🏽 +👩🏻‍🤝‍👩🏾 +👩🏻‍🤝‍👩🏿 +👩🏼‍🤝‍👩🏻 +👭🏼 +👩🏼‍🤝‍👩🏽 +👩🏼‍🤝‍👩🏾 +👩🏼‍🤝‍👩🏿 +👩🏽‍🤝‍👩🏻 +👩🏽‍🤝‍👩🏼 +👭🏽 +👩🏽‍🤝‍👩🏾 +👩🏽‍🤝‍👩🏿 +👩🏾‍🤝‍👩🏻 +👩🏾‍🤝‍👩🏼 +👩🏾‍🤝‍👩🏽 +👭🏾 +👩🏾‍🤝‍👩🏿 +👩🏿‍🤝‍👩🏻 +👩🏿‍🤝‍👩🏼 +👩🏿‍🤝‍👩🏽 +👩🏿‍🤝‍👩🏾 +👭🏿 +👭 +👬🏻 +👨🏻‍🤝‍👨🏼 +👨🏻‍🤝‍👨🏽 +👨🏻‍🤝‍👨🏾 +👨🏻‍🤝‍👨🏿 +👨🏼‍🤝‍👨🏻 +👬🏼 +👨🏼‍🤝‍👨🏽 +👨🏼‍🤝‍👨🏾 +👨🏼‍🤝‍👨🏿 +👨🏽‍🤝‍👨🏻 +👨🏽‍🤝‍👨🏼 +👬🏽 +👨🏽‍🤝‍👨🏾 +👨🏽‍🤝‍👨🏿 +👨🏾‍🤝‍👨🏻 +👨🏾‍🤝‍👨🏼 +👨🏾‍🤝‍👨🏽 +👬🏾 +👨🏾‍🤝‍👨🏿 +👨🏿‍🤝‍👨🏻 +👨🏿‍🤝‍👨🏼 +👨🏿‍🤝‍👨🏽 +👨🏿‍🤝‍👨🏾 +👬🏿 +👬 +💑🏻 +🧑🏻‍❤️‍🧑🏼 +🧑🏻‍❤️‍🧑🏽 +🧑🏻‍❤️‍🧑🏾 +🧑🏻‍❤️‍🧑🏿 +🧑🏼‍❤️‍🧑🏻 +💑🏼 +🧑🏼‍❤️‍🧑🏽 +🧑🏼‍❤️‍🧑🏾 +🧑🏼‍❤️‍🧑🏿 +🧑🏽‍❤️‍🧑🏻 +🧑🏽‍❤️‍🧑🏼 +💑🏽 +🧑🏽‍❤️‍🧑🏾 +🧑🏽‍❤️‍🧑🏿 +🧑🏾‍❤️‍🧑🏻 +🧑🏾‍❤️‍🧑🏼 +🧑🏾‍❤️‍🧑🏽 +💑🏾 +🧑🏾‍❤️‍🧑🏿 +🧑🏿‍❤️‍🧑🏻 +🧑🏿‍❤️‍🧑🏼 +🧑🏿‍❤️‍🧑🏽 +🧑🏿‍❤️‍🧑🏾 +💑🏿 +💑 +👩🏻‍❤️‍👨🏻 +👩🏻‍❤️‍👨🏼 +👩🏻‍❤️‍👨🏽 +👩🏻‍❤️‍👨🏾 +👩🏻‍❤️‍👨🏿 +👩🏼‍❤️‍👨🏻 +👩🏼‍❤️‍👨🏼 +👩🏼‍❤️‍👨🏽 +👩🏼‍❤️‍👨🏾 +👩🏼‍❤️‍👨🏿 +👩🏽‍❤️‍👨🏻 +👩🏽‍❤️‍👨🏼 +👩🏽‍❤️‍👨🏽 +👩🏽‍❤️‍👨🏾 +👩🏽‍❤️‍👨🏿 +👩🏾‍❤️‍👨🏻 +👩🏾‍❤️‍👨🏼 +👩🏾‍❤️‍👨🏽 +👩🏾‍❤️‍👨🏾 +👩🏾‍❤️‍👨🏿 +👩🏿‍❤️‍👨🏻 +👩🏿‍❤️‍👨🏼 +👩🏿‍❤️‍👨🏽 +👩🏿‍❤️‍👨🏾 +👩🏿‍❤️‍👨🏿 +👩‍❤️‍👨 +👩🏻‍❤️‍👩🏻 +👩🏻‍❤️‍👩🏼 +👩🏻‍❤️‍👩🏽 +👩🏻‍❤️‍👩🏾 +👩🏻‍❤️‍👩🏿 +👩🏼‍❤️‍👩🏻 +👩🏼‍❤️‍👩🏼 +👩🏼‍❤️‍👩🏽 +👩🏼‍❤️‍👩🏾 +👩🏼‍❤️‍👩🏿 +👩🏽‍❤️‍👩🏻 +👩🏽‍❤️‍👩🏼 +👩🏽‍❤️‍👩🏽 +👩🏽‍❤️‍👩🏾 +👩🏽‍❤️‍👩🏿 +👩🏾‍❤️‍👩🏻 +👩🏾‍❤️‍👩🏼 +👩🏾‍❤️‍👩🏽 +👩🏾‍❤️‍👩🏾 +👩🏾‍❤️‍👩🏿 +👩🏿‍❤️‍👩🏻 +👩🏿‍❤️‍👩🏼 +👩🏿‍❤️‍👩🏽 +👩🏿‍❤️‍👩🏾 +👩🏿‍❤️‍👩🏿 +👩‍❤️‍👩 +👨🏻‍❤️‍👨🏻 +👨🏻‍❤️‍👨🏼 +👨🏻‍❤️‍👨🏽 +👨🏻‍❤️‍👨🏾 +👨🏻‍❤️‍👨🏿 +👨🏼‍❤️‍👨🏻 +👨🏼‍❤️‍👨🏼 +👨🏼‍❤️‍👨🏽 +👨🏼‍❤️‍👨🏾 +👨🏼‍❤️‍👨🏿 +👨🏽‍❤️‍👨🏻 +👨🏽‍❤️‍👨🏼 +👨🏽‍❤️‍👨🏽 +👨🏽‍❤️‍👨🏾 +👨🏽‍❤️‍👨🏿 +👨🏾‍❤️‍👨🏻 +👨🏾‍❤️‍👨🏼 +👨🏾‍❤️‍👨🏽 +👨🏾‍❤️‍👨🏾 +👨🏾‍❤️‍👨🏿 +👨🏿‍❤️‍👨🏻 +👨🏿‍❤️‍👨🏼 +👨🏿‍❤️‍👨🏽 +👨🏿‍❤️‍👨🏾 +👨🏿‍❤️‍👨🏿 +👨‍❤️‍👨 +💏🏻 +🧑🏻‍❤️‍💋‍🧑🏼 +🧑🏻‍❤️‍💋‍🧑🏽 +🧑🏻‍❤️‍💋‍🧑🏾 +🧑🏻‍❤️‍💋‍🧑🏿 +🧑🏼‍❤️‍💋‍🧑🏻 +💏🏼 +🧑🏼‍❤️‍💋‍🧑🏽 +🧑🏼‍❤️‍💋‍🧑🏾 +🧑🏼‍❤️‍💋‍🧑🏿 +🧑🏽‍❤️‍💋‍🧑🏻 +🧑🏽‍❤️‍💋‍🧑🏼 +💏🏽 +🧑🏽‍❤️‍💋‍🧑🏾 +🧑🏽‍❤️‍💋‍🧑🏿 +🧑🏾‍❤️‍💋‍🧑🏻 +🧑🏾‍❤️‍💋‍🧑🏼 +🧑🏾‍❤️‍💋‍🧑🏽 +💏🏾 +🧑🏾‍❤️‍💋‍🧑🏿 +🧑🏿‍❤️‍💋‍🧑🏻 +🧑🏿‍❤️‍💋‍🧑🏼 +🧑🏿‍❤️‍💋‍🧑🏽 +🧑🏿‍❤️‍💋‍🧑🏾 +💏🏿 +💏 +👩🏻‍❤️‍💋‍👨🏻 +👩🏻‍❤️‍💋‍👨🏼 +👩🏻‍❤️‍💋‍👨🏽 +👩🏻‍❤️‍💋‍👨🏾 +👩🏻‍❤️‍💋‍👨🏿 +👩🏼‍❤️‍💋‍👨🏻 +👩🏼‍❤️‍💋‍👨🏼 +👩🏼‍❤️‍💋‍👨🏽 +👩🏼‍❤️‍💋‍👨🏾 +👩🏼‍❤️‍💋‍👨🏿 +👩🏽‍❤️‍💋‍👨🏻 +👩🏽‍❤️‍💋‍👨🏼 +👩🏽‍❤️‍💋‍👨🏽 +👩🏽‍❤️‍💋‍👨🏾 +👩🏽‍❤️‍💋‍👨🏿 +👩🏾‍❤️‍💋‍👨🏻 +👩🏾‍❤️‍💋‍👨🏼 +👩🏾‍❤️‍💋‍👨🏽 +👩🏾‍❤️‍💋‍👨🏾 +👩🏾‍❤️‍💋‍👨🏿 +👩🏿‍❤️‍💋‍👨🏻 +👩🏿‍❤️‍💋‍👨🏼 +👩🏿‍❤️‍💋‍👨🏽 +👩🏿‍❤️‍💋‍👨🏾 +👩🏿‍❤️‍💋‍👨🏿 +👩‍❤️‍💋‍👨 +👩🏻‍❤️‍💋‍👩🏻 +👩🏻‍❤️‍💋‍👩🏼 +👩🏻‍❤️‍💋‍👩🏽 +👩🏻‍❤️‍💋‍👩🏾 +👩🏻‍❤️‍💋‍👩🏿 +👩🏼‍❤️‍💋‍👩🏻 +👩🏼‍❤️‍💋‍👩🏼 +👩🏼‍❤️‍💋‍👩🏽 +👩🏼‍❤️‍💋‍👩🏾 +👩🏼‍❤️‍💋‍👩🏿 +👩🏽‍❤️‍💋‍👩🏻 +👩🏽‍❤️‍💋‍👩🏼 +👩🏽‍❤️‍💋‍👩🏽 +👩🏽‍❤️‍💋‍👩🏾 +👩🏽‍❤️‍💋‍👩🏿 +👩🏾‍❤️‍💋‍👩🏻 +👩🏾‍❤️‍💋‍👩🏼 +👩🏾‍❤️‍💋‍👩🏽 +👩🏾‍❤️‍💋‍👩🏾 +👩🏾‍❤️‍💋‍👩🏿 +👩🏿‍❤️‍💋‍👩🏻 +👩🏿‍❤️‍💋‍👩🏼 +👩🏿‍❤️‍💋‍👩🏽 +👩🏿‍❤️‍💋‍👩🏾 +👩🏿‍❤️‍💋‍👩🏿 +👩‍❤️‍💋‍👩 +👨🏻‍❤️‍💋‍👨🏻 +👨🏻‍❤️‍💋‍👨🏼 +👨🏻‍❤️‍💋‍👨🏽 +👨🏻‍❤️‍💋‍👨🏾 +👨🏻‍❤️‍💋‍👨🏿 +👨🏼‍❤️‍💋‍👨🏻 +👨🏼‍❤️‍💋‍👨🏼 +👨🏼‍❤️‍💋‍👨🏽 +👨🏼‍❤️‍💋‍👨🏾 +👨🏼‍❤️‍💋‍👨🏿 +👨🏽‍❤️‍💋‍👨🏻 +👨🏽‍❤️‍💋‍👨🏼 +👨🏽‍❤️‍💋‍👨🏽 +👨🏽‍❤️‍💋‍👨🏾 +👨🏽‍❤️‍💋‍👨🏿 +👨🏾‍❤️‍💋‍👨🏻 +👨🏾‍❤️‍💋‍👨🏼 +👨🏾‍❤️‍💋‍👨🏽 +👨🏾‍❤️‍💋‍👨🏾 +👨🏾‍❤️‍💋‍👨🏿 +👨🏿‍❤️‍💋‍👨🏻 +👨🏿‍❤️‍💋‍👨🏼 +👨🏿‍❤️‍💋‍👨🏽 +👨🏿‍❤️‍💋‍👨🏾 +👨🏿‍❤️‍💋‍👨🏿 +👨‍❤️‍💋‍👨 +🧑‍🧑‍🧒‍🧒 +🧑‍🧑‍🧒 +🧑‍🧒‍🧒 +🧑‍🧒 +👪 +👨‍👩‍👦 +👨‍👩‍👧 +👨‍👩‍👧‍👦 +👨‍👩‍👦‍👦 +👨‍👩‍👧‍👧 +👩‍👩‍👦 +👩‍👩‍👧 +👩‍👩‍👧‍👦 +👩‍👩‍👦‍👦 +👩‍👩‍👧‍👧 +👨‍👨‍👦 +👨‍👨‍👧 +👨‍👨‍👧‍👦 +👨‍👨‍👦‍👦 +👨‍👨‍👧‍👧 +👩‍👦 +👩‍👧 +👩‍👧‍👦 +👩‍👦‍👦 +👩‍👧‍👧 +👨‍👦 +👨‍👧 +👨‍👧‍👦 +👨‍👦‍👦 +👨‍👧‍👧 +🪢 +🧶 +🧵 +🪡 +🧥 +🥼 +🦺 +👚 +👕 +👖 +🩲 +🩳 +👔 +👗 +👙 +🩱 +👘 +🥻 +🩴 +🥿 +👠 +👡 +👢 +👞 +👟 +🥾 +🧦 +🧤 +🧣 +🎩 +🧢 +👒 +🎓 +⛑️ +🪖 +👑 +💍 +👝 +👛 +👜 +💼 +🎒 +🧳 +👓 +🕶️ +🥽 +🌂 +🐶 +🐱 +🐭 +🐹 +🐰 +🦊 +🐻 +🐼 +🐻‍❄️ +🐨 +🐯 +🦁 +🐮 +🐷 +🐽 +🐸 +🐵 +🙈 +🙉 +🙊 +🐒 +🐔 +🐧 +🐦 +🐤 +🐣 +🐥 +🪿 +🦆 +🐦‍⬛ +🦅 +🦉 +🦇 +🐺 +🐗 +🐴 +🦄 +🫎 +🐝 +🪱 +🐛 +🦋 +🐌 +🐞 +🐜 +🪰 +🪲 +🪳 +🦟 +🦗 +🕷️ +🕸️ +🦂 +🐢 +🐍 +🦎 +🦖 +🦕 +🐙 +🦑 +🪼 +🦐 +🦞 +🦀 +🐡 +🐠 +🐟 +🐬 +🐳 +🐋 +🦈 +🦭 +🐊 +🐅 +🐆 +🦓 +🦍 +🦧 +🦣 +🐘 +🦛 +🦏 +🐪 +🐫 +🦒 +🦘 +🦬 +🐃 +🐂 +🐄 +🫏 +🐎 +🐖 +🐏 +🐑 +🦙 +🐐 +🦌 +🐕 +🐩 +🦮 +🐕‍🦺 +🐈 +🐈‍⬛ +🪶 +🪽 +🐓 +🦃 +🦤 +🦚 +🦜 +🦢 +🦩 +🕊️ +🐇 +🦝 +🦨 +🦡 +🦫 +🦦 +🦥 +🐁 +🐀 +🐿️ +🦔 +🐾 +🐉 +🐲 +🐦‍🔥 +🌵 +🎄 +🌲 +🌳 +🌴 +🪵 +🌱 +🌿 +☘️ +🍀 +🎍 +🪴 +🎋 +🍃 +🍂 +🍁 +🪺 +🪹 +🍄 +🍄‍🟫 +🐚 +🪸 +🪨 +🌾 +💐 +🌷 +🌹 +🥀 +🪻 +🪷 +🌺 +🌸 +🌼 +🌻 +🌞 +🌝 +🌛 +🌜 +🌚 +🌕 +🌖 +🌗 +🌘 +🌑 +🌒 +🌓 +🌔 +🌙 +🌎 +🌍 +🌏 +🪐 +💫 +⭐ +🌟 +✨ +⚡ +☄️ +💥 +🔥 +🌪️ +🌈 +☀️ +🌤️ +⛅ +🌥️ +☁️ +🌦️ +🌧️ +⛈️ +🌩️ +🌨️ +❄️ +☃️ +⛄ +🌬️ +💨 +💧 +💦 +🫧 +☔ +☂️ +🌊 +🌫️ +🍏 +🍎 +🍐 +🍊 +🍋 +🍋‍🟩 +🍌 +🍉 +🍇 +🍓 +🫐 +🍈 +🍒 +🍑 +🥭 +🍍 +🥥 +🥝 +🍅 +🍆 +🥑 +🫛 +🥦 +🥬 +🥒 +🌶️ +🫑 +🌽 +🥕 +🫒 +🧄 +🧅 +🥔 +🍠 +🫚 +🥐 +🥯 +🍞 +🥖 +🥨 +🧀 +🥚 +🍳 +🧈 +🥞 +🧇 +🥓 +🥩 +🍗 +🍖 +🦴 +🌭 +🍔 +🍟 +🍕 +🫓 +🥪 +🥙 +🧆 +🌮 +🌯 +🫔 +🥗 +🥘 +🫕 +🥫 +🫙 +🍝 +🍜 +🍲 +🍛 +🍣 +🍱 +🥟 +🦪 +🍤 +🍙 +🍚 +🍘 +🍥 +🥠 +🥮 +🍢 +🍡 +🍧 +🍨 +🍦 +🥧 +🧁 +🍰 +🎂 +🍮 +🍭 +🍬 +🍫 +🍿 +🍩 +🍪 +🌰 +🥜 +🫘 +🍯 +🥛 +🫗 +🍼 +🫖 +☕ +🍵 +🧉 +🧃 +🥤 +🧋 +🍶 +🍺 +🍻 +🥂 +🍷 +🥃 +🍸 +🍹 +🍾 +🧊 +🥄 +🍴 +🍽️ +🥣 +🥡 +🥢 +🧂 +⚽ +🏀 +🏈 +⚾ +🥎 +🎾 +🏐 +🏉 +🥏 +🎱 +🪀 +🏓 +🏸 +🏒 +🏑 +🥍 +🏏 +🪃 +🥅 +⛳ +🪁 +🛝 +🏹 +🎣 +🤿 +🥊 +🥋 +🎽 +🛹 +🛼 +🛷 +⛸️ +🥌 +🎿 +⛷️ +🏂🏻 +🏂🏼 +🏂🏽 +🏂🏾 +🏂🏿 +🏂 +🪂 +🏋🏻 +🏋🏼 +🏋🏽 +🏋🏾 +🏋🏿 +🏋️ +🏋🏻‍♀️ +🏋🏼‍♀️ +🏋🏽‍♀️ +🏋🏾‍♀️ +🏋🏿‍♀️ +🏋️‍♀️ +🏋🏻‍♂️ +🏋🏼‍♂️ +🏋🏽‍♂️ +🏋🏾‍♂️ +🏋🏿‍♂️ +🏋️‍♂️ +🤼 +🤼‍♀️ +🤼‍♂️ +🤸🏻 +🤸🏼 +🤸🏽 +🤸🏾 +🤸🏿 +🤸 +🤸🏻‍♀️ +🤸🏼‍♀️ +🤸🏽‍♀️ +🤸🏾‍♀️ +🤸🏿‍♀️ +🤸‍♀️ +🤸🏻‍♂️ +🤸🏼‍♂️ +🤸🏽‍♂️ +🤸🏾‍♂️ +🤸🏿‍♂️ +🤸‍♂️ +⛹🏻 +⛹🏼 +⛹🏽 +⛹🏾 +⛹🏿 +⛹️ +⛹🏻‍♀️ +⛹🏼‍♀️ +⛹🏽‍♀️ +⛹🏾‍♀️ +⛹🏿‍♀️ +⛹️‍♀️ +⛹🏻‍♂️ +⛹🏼‍♂️ +⛹🏽‍♂️ +⛹🏾‍♂️ +⛹🏿‍♂️ +⛹️‍♂️ +🤺 +🤾🏻 +🤾🏼 +🤾🏽 +🤾🏾 +🤾🏿 +🤾 +🤾🏻‍♀️ +🤾🏼‍♀️ +🤾🏽‍♀️ +🤾🏾‍♀️ +🤾🏿‍♀️ +🤾‍♀️ +🤾🏻‍♂️ +🤾🏼‍♂️ +🤾🏽‍♂️ +🤾🏾‍♂️ +🤾🏿‍♂️ +🤾‍♂️ +🏌🏻 +🏌🏼 +🏌🏽 +🏌🏾 +🏌🏿 +🏌️ +🏌🏻‍♀️ +🏌🏼‍♀️ +🏌🏽‍♀️ +🏌🏾‍♀️ +🏌🏿‍♀️ +🏌️‍♀️ +🏌🏻‍♂️ +🏌🏼‍♂️ +🏌🏽‍♂️ +🏌🏾‍♂️ +🏌🏿‍♂️ +🏌️‍♂️ +🏇🏻 +🏇🏼 +🏇🏽 +🏇🏾 +🏇🏿 +🏇 +🧘🏻 +🧘🏼 +🧘🏽 +🧘🏾 +🧘🏿 +🧘 +🧘🏻‍♀️ +🧘🏼‍♀️ +🧘🏽‍♀️ +🧘🏾‍♀️ +🧘🏿‍♀️ +🧘‍♀️ +🧘🏻‍♂️ +🧘🏼‍♂️ +🧘🏽‍♂️ +🧘🏾‍♂️ +🧘🏿‍♂️ +🧘‍♂️ +🏄🏻 +🏄🏼 +🏄🏽 +🏄🏾 +🏄🏿 +🏄 +🏄🏻‍♀️ +🏄🏼‍♀️ +🏄🏽‍♀️ +🏄🏾‍♀️ +🏄🏿‍♀️ +🏄‍♀️ +🏄🏻‍♂️ +🏄🏼‍♂️ +🏄🏽‍♂️ +🏄🏾‍♂️ +🏄🏿‍♂️ +🏄‍♂️ +🏊🏻 +🏊🏼 +🏊🏽 +🏊🏾 +🏊🏿 +🏊 +🏊🏻‍♀️ +🏊🏼‍♀️ +🏊🏽‍♀️ +🏊🏾‍♀️ +🏊🏿‍♀️ +🏊‍♀️ +🏊🏻‍♂️ +🏊🏼‍♂️ +🏊🏽‍♂️ +🏊🏾‍♂️ +🏊🏿‍♂️ +🏊‍♂️ +🤽🏻 +🤽🏼 +🤽🏽 +🤽🏾 +🤽🏿 +🤽 +🤽🏻‍♀️ +🤽🏼‍♀️ +🤽🏽‍♀️ +🤽🏾‍♀️ +🤽🏿‍♀️ +🤽‍♀️ +🤽🏻‍♂️ +🤽🏼‍♂️ +🤽🏽‍♂️ +🤽🏾‍♂️ +🤽🏿‍♂️ +🤽‍♂️ +🚣🏻 +🚣🏼 +🚣🏽 +🚣🏾 +🚣🏿 +🚣 +🚣🏻‍♀️ +🚣🏼‍♀️ +🚣🏽‍♀️ +🚣🏾‍♀️ +🚣🏿‍♀️ +🚣‍♀️ +🚣🏻‍♂️ +🚣🏼‍♂️ +🚣🏽‍♂️ +🚣🏾‍♂️ +🚣🏿‍♂️ +🚣‍♂️ +🧗🏻 +🧗🏼 +🧗🏽 +🧗🏾 +🧗🏿 +🧗 +🧗🏻‍♀️ +🧗🏼‍♀️ +🧗🏽‍♀️ +🧗🏾‍♀️ +🧗🏿‍♀️ +🧗‍♀️ +🧗🏻‍♂️ +🧗🏼‍♂️ +🧗🏽‍♂️ +🧗🏾‍♂️ +🧗🏿‍♂️ +🧗‍♂️ +🚵🏻 +🚵🏼 +🚵🏽 +🚵🏾 +🚵🏿 +🚵 +🚵🏻‍♀️ +🚵🏼‍♀️ +🚵🏽‍♀️ +🚵🏾‍♀️ +🚵🏿‍♀️ +🚵‍♀️ +🚵🏻‍♂️ +🚵🏼‍♂️ +🚵🏽‍♂️ +🚵🏾‍♂️ +🚵🏿‍♂️ +🚵‍♂️ +🚴🏻 +🚴🏼 +🚴🏽 +🚴🏾 +🚴🏿 +🚴 +🚴🏻‍♀️ +🚴🏼‍♀️ +🚴🏽‍♀️ +🚴🏾‍♀️ +🚴🏿‍♀️ +🚴‍♀️ +🚴🏻‍♂️ +🚴🏼‍♂️ +🚴🏽‍♂️ +🚴🏾‍♂️ +🚴🏿‍♂️ +🚴‍♂️ +🏆 +🥇 +🥈 +🥉 +🏅 +🎖️ +🏵️ +🎗️ +🎫 +🎟️ +🎪 +🤹🏻 +🤹🏼 +🤹🏽 +🤹🏾 +🤹🏿 +🤹 +🤹🏻‍♀️ +🤹🏼‍♀️ +🤹🏽‍♀️ +🤹🏾‍♀️ +🤹🏿‍♀️ +🤹‍♀️ +🤹🏻‍♂️ +🤹🏼‍♂️ +🤹🏽‍♂️ +🤹🏾‍♂️ +🤹🏿‍♂️ +🤹‍♂️ +🎭 +🩰 +🎨 +🎬 +🎤 +🎧 +🎼 +🎹 +🪇 +🥁 +🪘 +🎷 +🎺 +🪗 +🎸 +🪕 +🎻 +🪈 +🎲 +♟️ +🎯 +🎳 +🎮 +🎰 +🧩 +🚗 +🚕 +🚙 +🛻 +🚐 +🚌 +🚎 +🏎️ +🚓 +🚑 +🚒 +🚚 +🚛 +🚜 +🦯 +🦽 +🦼 +🩼 +🛴 +🚲 +🛵 +🏍️ +🛺 +🛞 +🚨 +🚔 +🚍 +🚘 +🚖 +🚡 +🚠 +🚟 +🚃 +🚋 +🚞 +🚝 +🚄 +🚅 +🚈 +🚂 +🚆 +🚇 +🚊 +🚉 +✈️ +🛫 +🛬 +🛩️ +💺 +🛰️ +🚀 +🛸 +🚁 +🛶 +⛵ +🚤 +🛥️ +🛳️ +⛴️ +🚢 +🛟 +⚓ +🪝 +⛽ +🚧 +🚦 +🚥 +🚏 +🗺️ +🗿 +🗽 +🗼 +🏰 +🏯 +🏟️ +🎡 +🎢 +🎠 +⛲ +⛱️ +🏖️ +🏝️ +🏜️ +🌋 +⛰️ +🏔️ +🗻 +🏕️ +⛺ +🏠 +🏡 +🏘️ +🏚️ +🛖 +🏗️ +🏭 +🏢 +🏬 +🏣 +🏤 +🏥 +🏦 +🏨 +🏪 +🏫 +🏩 +💒 +🏛️ +⛪ +🕌 +🕍 +🛕 +🕋 +⛩️ +🛤️ +🛣️ +🗾 +🎑 +🏞️ +🌅 +🌄 +🌠 +🎇 +🎆 +🌇 +🌆 +🏙️ +🌃 +🌌 +🌉 +🌁 +⌚ +📱 +📲 +💻 +⌨️ +🖥️ +🖨️ +🖱️ +🖲️ +🕹️ +🗜️ +💽 +💾 +💿 +📀 +📼 +📷 +📸 +📹 +🎥 +📽️ +🎞️ +📞 +☎️ +📟 +📠 +📺 +📻 +🎙️ +🎚️ +🎛️ +🧭 +⏱️ +⏲️ +⏰ +🕰️ +⌛ +⏳ +📡 +🔋 +🪫 +🔌 +💡 +🔦 +🕯️ +🪔 +🧯 +🛢️ +💸 +💵 +💴 +💶 +💷 +🪙 +💰 +💳 +🪪 +💎 +⚖️ +🪜 +🧰 +🪛 +🔧 +🔨 +⚒️ +🛠️ +⛏️ +🪚 +🔩 +⚙️ +🪤 +🧱 +⛓️ +🔗 +⛓️‍💥 +🧲 +🔫 +💣 +🧨 +🪓 +🔪 +🗡️ +⚔️ +🛡️ +🚬 +⚰️ +🪦 +⚱️ +🏺 +🔮 +📿 +🧿 +🪬 +💈 +⚗️ +🔭 +🔬 +🕳️ +🩻 +🩹 +🩺 +💊 +💉 +🩸 +🧬 +🦠 +🧫 +🧪 +🌡️ +🧹 +🪠 +🧺 +🧻 +🚽 +🚰 +🚿 +🛁 +🛀🏻 +🛀🏼 +🛀🏽 +🛀🏾 +🛀🏿 +🛀 +🧼 +🪥 +🪒 +🪮 +🧽 +🪣 +🧴 +🛎️ +🔑 +🗝️ +🚪 +🪑 +🛋️ +🛏️ +🛌🏻 +🛌🏼 +🛌🏽 +🛌🏾 +🛌🏿 +🛌 +🧸 +🪆 +🖼️ +🪞 +🪟 +🛍️ +🛒 +🎁 +🎈 +🎏 +🎀 +🪄 +🪅 +🎊 +🎉 +🎎 +🪭 +🏮 +🎐 +🪩 +🧧 +✉️ +📩 +📨 +📧 +💌 +📥 +📤 +📦 +🏷️ +🪧 +📪 +📫 +📬 +📭 +📮 +📯 +📜 +📃 +📄 +📑 +🧾 +📊 +📈 +📉 +🗒️ +🗓️ +📆 +📅 +🗑️ +📇 +🗃️ +🗳️ +🗄️ +📋 +📁 +📂 +🗂️ +🗞️ +📰 +📓 +📔 +📒 +📕 +📗 +📘 +📙 +📚 +📖 +🔖 +🧷 +📎 +🖇️ +📐 +📏 +🧮 +📌 +📍 +✂️ +🖊️ +🖋️ +✒️ +🖌️ +🖍️ +📝 +✏️ +🔍 +🔎 +🔏 +🔐 +🔒 +🔓 +🩷 +❤️ +🧡 +💛 +💚 +🩵 +💙 +💜 +🖤 +🩶 +🤍 +🤎 +💔 +❣️ +💕 +💞 +💓 +💗 +💖 +💘 +💝 +❤️‍🩹 +❤️‍🔥 +💟 +☮️ +✝️ +☪️ +🕉️ +☸️ +🪯 +✡️ +🔯 +🕎 +☯️ +☦️ +🛐 +⛎ +♈ +♉ +♊ +♋ +♌ +♍ +♎ +♏ +♐ +♑ +♒ +♓ +🆔 +⚛️ +🉑 +☢️ +☣️ +📴 +📳 +🈶 +🈚 +🈸 +🈺 +🈷️ +✴️ +🆚 +💮 +🉐 +㊙️ +㊗️ +🈴 +🈵 +🈹 +🈲 +🅰️ +🅱️ +🆎 +🆑 +🅾️ +🆘 +❌ +⭕ +🛑 +⛔ +📛 +🚫 +💯 +💢 +♨️ +🚷 +🚯 +🚳 +🚱 +🔞 +📵 +🚭 +❗ +❕ +❓ +❔ +‼️ +⁉️ +🔅 +🔆 +〽️ +⚠️ +🚸 +🔱 +⚜️ +🔰 +♻️ +✅ +🈯 +💹 +❇️ +✳️ +❎ +🌐 +💠 +Ⓜ️ +🌀 +💤 +🏧 +🚾 +♿ +🅿️ +🛗 +🈳 +🈂️ +🛂 +🛃 +🛄 +🛅 +🛜 +🚹 +🚺 +🚼 +🚻 +🚮 +🎦 +📶 +🈁 +🔣 +ℹ️ +🔤 +🔡 +🔠 +🆖 +🆗 +🆙 +🆒 +🆕 +🆓 +0️⃣ +1️⃣ +2️⃣ +3️⃣ +4️⃣ +5️⃣ +6️⃣ +7️⃣ +8️⃣ +9️⃣ +🔟 +🔢 +#️⃣ +*️⃣ +⏏️ +▶️ +⏸️ +⏯️ +⏹️ +⏺️ +⏭️ +⏮️ +⏩ +⏪ +⏫ +⏬ +◀️ +🔼 +🔽 +➡️ +⬅️ +⬆️ +⬇️ +↗️ +↘️ +↙️ +↖️ +↕️ +↔️ +↪️ +↩️ +⤴️ +⤵️ +🔀 +🔁 +🔂 +🔄 +🔃 +🎵 +🎶 +➕ +➖ +➗ +✖️ +🟰 +♾️ +💲 +💱 +™️ +©️ +®️ +〰️ +➰ +➿ +🔚 +🔙 +🔛 +🔝 +🔜 +✔️ +☑️ +🔘 +⚪ +⚫ +🔴 +🔵 +🟤 +🟣 +🟢 +🟡 +🟠 +🔺 +🔻 +🔸 +🔹 +🔶 +🔷 +🔳 +🔲 +▪️ +▫️ +◾ +◽ +◼️ +◻️ +⬛ +⬜ +🟧 +🟦 +🟥 +🟫 +🟪 +🟩 +🟨 +🔈 +🔇 +🔉 +🔊 +🔔 +🔕 +📣 +📢 +🗨️ +👁‍🗨 +💬 +💭 +🗯️ +♠️ +♣️ +♥️ +♦️ +🃏 +🎴 +🀄 +🕐 +🕑 +🕒 +🕓 +🕔 +🕕 +🕖 +🕗 +🕘 +🕙 +🕚 +🕛 +🕜 +🕝 +🕞 +🕟 +🕠 +🕡 +🕢 +🕣 +🕤 +🕥 +🕦 +🕧 +♀️ +♂️ +⚧ +⚕️ +🇿 +🇾 +🇽 +🇼 +🇻 +🇺 +🇹 +🇸 +🇷 +🇶 +🇵 +🇴 +🇳 +🇲 +🇱 +🇰 +🇯 +🇮 +🇭 +🇬 +🇫 +🇪 +🇩 +🇨 +🇧 +🇦 +🏳️ +🏴 +🏴‍☠️ +🏁 +🚩 +🏳️‍🌈 +🏳️‍⚧️ +🇺🇳 +🇦🇫 +🇦🇽 +🇦🇱 +🇩🇿 +🇦🇸 +🇦🇩 +🇦🇴 +🇦🇮 +🇦🇶 +🇦🇬 +🇦🇷 +🇦🇲 +🇦🇼 +🇦🇺 +🇦🇹 +🇦🇿 +🇧🇸 +🇧🇭 +🇧🇩 +🇧🇧 +🇧🇾 +🇧🇪 +🇧🇿 +🇧🇯 +🇧🇲 +🇧🇹 +🇧🇴 +🇧🇦 +🇧🇼 +🇧🇷 +🇮🇴 +🇻🇬 +🇧🇳 +🇧🇬 +🇧🇫 +🇧🇮 +🇰🇭 +🇨🇲 +🇨🇦 +🇮🇨 +🇨🇻 +🇧🇶 +🇰🇾 +🇨🇫 +🇹🇩 +🇨🇱 +🇨🇳 +🇨🇽 +🇨🇨 +🇨🇴 +🇰🇲 +🇨🇬 +🇨🇩 +🇨🇰 +🇨🇷 +🇨🇮 +🇭🇷 +🇨🇺 +🇨🇼 +🇨🇾 +🇨🇿 +🇩🇰 +🇩🇯 +🇩🇲 +🇩🇴 +🇪🇨 +🇪🇬 +🇸🇻 +🇬🇶 +🇪🇷 +🇪🇪 +🇪🇹 +🇪🇺 +🇫🇰 +🇫🇴 +🇫🇯 +🇫🇮 +🇫🇷 +🇬🇫 +🇵🇫 +🇹🇫 +🇬🇦 +🇬🇲 +🇬🇪 +🇩🇪 +🇬🇭 +🇬🇮 +🇬🇷 +🇬🇱 +🇬🇩 +🇬🇵 +🇬🇺 +🇬🇹 +🇬🇬 +🇬🇳 +🇬🇼 +🇬🇾 +🇭🇹 +🇭🇳 +🇭🇰 +🇭🇺 +🇮🇸 +🇮🇳 +🇮🇩 +🇮🇷 +🇮🇶 +🇮🇪 +🇮🇲 +🇮🇱 +🇮🇹 +🇯🇲 +🇯🇵 +🎌 +🇯🇪 +🇯🇴 +🇰🇿 +🇰🇪 +🇰🇮 +🇽🇰 +🇰🇼 +🇰🇬 +🇱🇦 +🇱🇻 +🇱🇧 +🇱🇸 +🇱🇷 +🇱🇾 +🇱🇮 +🇱🇹 +🇱🇺 +🇲🇴 +🇲🇰 +🇲🇬 +🇲🇼 +🇲🇾 +🇲🇻 +🇲🇱 +🇲🇹 +🇲🇭 +🇲🇶 +🇲🇷 +🇲🇺 +🇾🇹 +🇲🇽 +🇫🇲 +🇲🇩 +🇲🇨 +🇲🇳 +🇲🇪 +🇲🇸 +🇲🇦 +🇲🇿 +🇲🇲 +🇳🇦 +🇳🇷 +🇳🇵 +🇳🇱 +🇳🇨 +🇳🇿 +🇳🇮 +🇳🇪 +🇳🇬 +🇳🇺 +🇳🇫 +🇰🇵 +🇲🇵 +🇳🇴 +🇴🇲 +🇵🇰 +🇵🇼 +🇵🇸 +🇵🇦 +🇵🇬 +🇵🇾 +🇵🇪 +🇵🇭 +🇵🇳 +🇵🇱 +🇵🇹 +🇵🇷 +🇶🇦 +🇷🇪 +🇷🇴 +🇷🇺 +🇷🇼 +🇼🇸 +🇸🇲 +🇸🇹 +🇸🇦 +🇸🇳 +🇷🇸 +🇸🇨 +🇸🇱 +🇸🇬 +🇸🇽 +🇸🇰 +🇸🇮 +🇬🇸 +🇸🇧 +🇸🇴 +🇿🇦 +🇰🇷 +🇸🇸 +🇪🇸 +🇱🇰 +🇧🇱 +🇸🇭 +🇰🇳 +🇱🇨 +🇵🇲 +🇻🇨 +🇸🇩 +🇸🇷 +🇸🇿 +🇸🇪 +🇨🇭 +🇸🇾 +🇹🇼 +🇹🇯 +🇹🇿 +🇹🇭 +🇹🇱 +🇹🇬 +🇹🇰 +🇹🇴 +🇹🇹 +🇹🇳 +🇹🇷 +🇹🇲 +🇹🇨 +🇻🇮 +🇹🇻 +🇺🇬 +🇺🇦 +🇦🇪 +🇬🇧 +🏴󠁧󠁢󠁥󠁮󠁧󠁿 +🏴󠁧󠁢󠁳󠁣󠁴󠁿 +🏴󠁧󠁢󠁷󠁬󠁳󠁿 +🇺🇸 +🇺🇾 +🇺🇿 +🇻🇺 +🇻🇦 +🇻🇪 +🇻🇳 +🇼🇫 +🇪🇭 +🇾🇪 +🇿🇲 +🇿🇼 +🇦🇨 +🇧🇻 +🇨🇵 +🇪🇦 +🇩🇬 +🇭🇲 +🇲🇫 +🇸🇯 +🇹🇦 +🇺🇲 \ No newline at end of file diff --git a/test/test.js b/test/test.js index 591b64b..69e3d48 100644 --- a/test/test.js +++ b/test/test.js @@ -141,6 +141,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/converters/user-to-mxid.test") require("../src/m2d/converters/diff-pins.test") require("../src/m2d/converters/event-to-message.test") + require("../src/m2d/converters/emoji.test") require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") From 5b06d5984ac91d830f161fbbcf72b6ba5ff9d11e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 20 Jan 2025 02:33:24 +1300 Subject: [PATCH 200/346] Do cache space members in member_cache --- scripts/setup.js | 4 ++-- src/d2m/actions/register-pk-user.js | 2 +- src/d2m/actions/register-user.js | 2 +- src/d2m/converters/user-to-mxid.js | 2 +- src/db/migrations/0016-foreign-keys.sql | 23 +++------------------ src/m2d/converters/event-to-message.js | 19 ++++++++++------- src/m2d/converters/event-to-message.test.js | 14 ++++++------- src/m2d/event-dispatcher.js | 4 ++-- test/ooye-test-data.sql | 22 ++++++++++---------- 9 files changed, 40 insertions(+), 52 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 3491106..6ee8c19 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -319,8 +319,8 @@ function defineEchoHandler() { await migrate.migrate(db) // add initial rows to database, like adding the bot to sim... - const botID = Buffer.from(reg.ooye.discord_token.split(".")[0], "base64").toString() - db.prepare("INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(botID, reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), reg.sender_localpart, mxid) + const client = await discord.snow.user.getSelf() + db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING").run(client.id, client.username, reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), mxid) console.log("✅ Database is ready...") diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index 411fcf8..7083006 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -33,7 +33,7 @@ async function createSim(pkMessage) { const mxid = `@${localpart}:${reg.ooye.server_name}` // Save chosen name in the database forever - db.prepare("INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(pkMessage.member.uuid, simName, localpart, mxid) + db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(pkMessage.member.uuid, simName, simName, mxid) // Register matrix user with that name try { diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index b914d41..c3bd16c 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -33,7 +33,7 @@ async function createSim(user) { // Save chosen name in the database forever // Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates - db.prepare("INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(user.id, simName, localpart, mxid) + db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(user.id, user.username, simName, mxid) // Register matrix user with that name try { diff --git a/src/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js index a619b36..e0ab137 100644 --- a/src/d2m/converters/user-to-mxid.js +++ b/src/d2m/converters/user-to-mxid.js @@ -64,7 +64,7 @@ function userToSimName(user) { } // 1. Is sim user already registered? - const existing = select("sim", "sim_name", {user_id: user.id}).pluck().get() + const existing = select("sim", "user_id", {user_id: user.id}).pluck().get() assert.equal(existing, null, "Shouldn't try to create a new name for an existing sim") // 2. Register based on username (could be new or old format) diff --git a/src/db/migrations/0016-foreign-keys.sql b/src/db/migrations/0016-foreign-keys.sql index c41f329..7a2b26c 100644 --- a/src/db/migrations/0016-foreign-keys.sql +++ b/src/db/migrations/0016-foreign-keys.sql @@ -86,24 +86,6 @@ DROP TABLE guild_space; -- 7 ALTER TABLE new_guild_space RENAME TO guild_space; --- *** member_cache *** - --- 4 -CREATE TABLE "new_member_cache" ( - "room_id" TEXT NOT NULL, - "mxid" TEXT NOT NULL, - "displayname" TEXT, - "avatar_url" TEXT, power_level INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY("room_id","mxid"), - FOREIGN KEY("room_id") REFERENCES "channel_room"("room_id") ON DELETE CASCADE -) WITHOUT ROWID; --- 5 -INSERT INTO new_member_cache (room_id, mxid, displayname, avatar_url) SELECT room_id, mxid, displayname, avatar_url FROM member_cache WHERE room_id IN (SELECT room_id FROM channel_room); --- 6 -DROP TABLE member_cache; --- 7 -ALTER TABLE new_member_cache RENAME TO member_cache; - -- *** reaction *** -- 4 @@ -142,15 +124,16 @@ ALTER TABLE new_webhook RENAME TO webhook; -- *** sim *** -- 4 --- while we're at it, rebuild this table to give it WITHOUT ROWID, remove UNIQUE, and drop the localpart column. no foreign keys needed +-- while we're at it, rebuild this table to give it WITHOUT ROWID, remove UNIQUE, and replace the localpart column with username. no foreign keys needed CREATE TABLE "new_sim" ( "user_id" TEXT NOT NULL, + "username" TEXT NOT NULL, "sim_name" TEXT NOT NULL, "mxid" TEXT NOT NULL, PRIMARY KEY("user_id") ) WITHOUT ROWID; -- 5 -INSERT INTO new_sim (user_id, sim_name, mxid) SELECT user_id, sim_name, mxid FROM sim; +INSERT INTO new_sim (user_id, username, sim_name, mxid) SELECT user_id, sim_name, sim_name, mxid FROM sim; -- 6 DROP TABLE sim; -- 7 diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index c498f1e..9a17817 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -258,13 +258,18 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) { const row = select("member_cache", ["displayname", "avatar_url"], {room_id: roomID, mxid}).get() if (row) return row return api.getStateEvent(roomID, "m.room.member", mxid).then(event => { - const displayname = event?.displayname || null - const avatar_url = event?.avatar_url || null - db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?").run( - roomID, mxid, - displayname, avatar_url, - displayname, avatar_url - ) + const room = select("channel_room", "room_id", {room_id: roomID}).get() + if (room) { + // save the member to the cache so we don't have to check with the homeserver next time + // the cache will be kept in sync by the `m.room.member` event listener + const displayname = event?.displayname || null + const avatar_url = event?.avatar_url || null + db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?").run( + roomID, mxid, + displayname, avatar_url, + displayname, avatar_url + ) + } return event }).catch(() => { return {displayname: null, avatar_url: null} diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 0dc9110..cc3d19a 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -3535,8 +3535,8 @@ test("event2message: does not cache the member if the room is not known", async }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", origin_server_ts: 1688301929913, - room_id: "!should_be_newly_cached:cadence.moe", - sender: "@should_be_newly_cached:cadence.moe", + room_id: "!not_real:cadence.moe", + sender: "@should_not_be_cached:cadence.moe", type: "m.room.message", unsigned: { age: 405299 @@ -3545,9 +3545,9 @@ test("event2message: does not cache the member if the room is not known", async api: { getStateEvent: async (roomID, type, stateKey) => { called++ - t.equal(roomID, "!should_be_newly_cached:cadence.moe") + t.equal(roomID, "!not_real:cadence.moe") t.equal(type, "m.room.member") - t.equal(stateKey, "@should_be_newly_cached:cadence.moe") + t.equal(stateKey, "@should_not_be_cached:cadence.moe") return { avatar_url: "mxc://cadence.moe/this_is_the_avatar" } @@ -3559,9 +3559,9 @@ test("event2message: does not cache the member if the room is not known", async messagesToDelete: [], messagesToEdit: [], messagesToSend: [{ - username: "should_be_newly_cached", + username: "should_not_be_cached", content: "testing the member state cache", - avatar_url: undefined, + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", allowed_mentions: { parse: ["users", "roles"] } @@ -3569,7 +3569,7 @@ test("event2message: does not cache the member if the room is not known", async } ) - t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!should_be_newly_cached:cadence.moe"}).all(), []) + t.deepEqual(select("member_cache", ["avatar_url", "displayname", "mxid"], {room_id: "!not_real:cadence.moe"}).all(), []) t.equal(called, 1, "getStateEvent should be called once") }) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 6cbd6c6..fc2a558 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -205,8 +205,8 @@ async event => { return db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) } - const room = select("channel_room", "room_id", {room_id: event.room_id}) - if (!room) return // don't cache members in unbridged rooms + const exists = select("channel_room", "room_id", {room_id: event.room_id}) ?? select("guild_space", "space_id", {space_id: event.room_id}) + if (!exists) return // don't cache members in unbridged rooms // Member is here let powerLevel = 0 diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 757ef9b..4a7d2f4 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -19,17 +19,17 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom ('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL), ('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL); -INSERT INTO sim (user_id, sim_name, mxid) VALUES -('0', 'bot', '@_ooye_bot:cadence.moe'), -('820865262526005258', 'crunch_god', '@_ooye_crunch_god:cadence.moe'), -('771520384671416320', 'bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), -('112890272819507200', '.wing.', '@_ooye_.wing.:cadence.moe'), -('114147806469554185', 'extremity', '@_ooye_extremity:cadence.moe'), -('111604486476181504', 'kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), -('1109360903096369153', 'amanda', '@_ooye_amanda:cadence.moe'), -('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'), -('320067006521147393', 'papiophidian', '@_ooye_papiophidian:cadence.moe'), -('772659086046658620', 'cadence', '@_ooye_cadence:cadence.moe'); +INSERT INTO sim (user_id, username, sim_name, mxid) VALUES +('0', 'Matrix Bridge', 'bot', '@_ooye_bot:cadence.moe'), +('820865262526005258', 'Crunch God', 'crunch_god', '@_ooye_crunch_god:cadence.moe'), +('771520384671416320', 'Bojack Horseman', 'bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), +('112890272819507200', 'wing', '.wing.', '@_ooye_.wing.:cadence.moe'), +('114147806469554185', 'extremity', 'extremity', '@_ooye_extremity:cadence.moe'), +('111604486476181504', 'kyuugryphon', 'kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), +('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'), +('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'), +('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'), +('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'); INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL), From eadefef6a357840c9cf44d30ca92e459f2c831b9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 21 Jan 2025 15:08:12 +1300 Subject: [PATCH 201/346] Clean up member_cache when unbridging --- src/d2m/actions/create-room.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 64e2b68..e0d019f 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -438,6 +438,7 @@ async function unbridgeDeletedChannel(channel, guildID) { } // delete room from database + db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID) db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) } From a579b509d31c2fce0d49d343e834e96a38e31043 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 28 Jan 2025 16:08:43 +1300 Subject: [PATCH 202/346] Catch PK API network errors --- src/d2m/actions/register-pk-user.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index 7083006..ce1665c 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -146,15 +146,20 @@ async function fetchMessage(messageID) { // Their backend is weird. Sometimes it says "message not found" (code 20006) on the first try, so we make multiple attempts. let attempts = 0 do { - var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) - if (res.ok) return res.json() + try { + var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) + if (res.ok) return res.json() + var errorGetter = res.json + } catch (e) { + // Catch any network issues too. + errorGetter = e.toString + } // I think the backend needs some time to update. - await new Promise(resolve => setTimeout(resolve, 2000)) + await new Promise(resolve => setTimeout(resolve, 1500)) } while (++attempts < 3) - const errorMessage = await res.json() - throw new Error(`PK API returned an error after ${attempts} tries: ${JSON.stringify(errorMessage)}`) + throw new Error(`PK API returned an error after ${attempts} tries: ${JSON.stringify(await errorGetter())}`) } module.exports._memberToStateContent = memberToStateContent From 6fe8c60f116cfc3eb3b306d99fe0d0823e7b8951 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 30 Jan 2025 15:34:29 +1300 Subject: [PATCH 203/346] Add analyze of new data --- src/db/migrations/0017-analyze.sql | 225 +++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/db/migrations/0017-analyze.sql diff --git a/src/db/migrations/0017-analyze.sql b/src/db/migrations/0017-analyze.sql new file mode 100644 index 0000000..802fca2 --- /dev/null +++ b/src/db/migrations/0017-analyze.sql @@ -0,0 +1,225 @@ +-- https://www.sqlite.org/lang_analyze.html + +BEGIN TRANSACTION; + +ANALYZE sqlite_schema; + +DELETE FROM "sqlite_stat1"; +INSERT INTO "sqlite_stat1" ("tbl","idx","stat") VALUES ('sim','sim','625 1'), + ('reaction','reaction','3242 1'), + ('channel_room','channel_room','389 1'), + ('channel_room','sqlite_autoindex_channel_room_1','389 1'), + ('media_proxy','media_proxy','5068 1'), + ('sim_proxy','sim_proxy','36 1'), + ('webhook','webhook','155 1'), + ('member_cache','member_cache','784 3 1'), + ('member_power','member_power','1 1 1'), + ('file','file','21862 1'), + ('message_channel','message_channel','366884 1'), + ('lottie','lottie','19 1'), + ('event_message','event_message','382920 1 1'), + ('migration',NULL,'1'), + ('sim_member','sim_member','2871 7 1'), + ('guild_space','guild_space','32 1'), + ('guild_active','guild_active','34 1'), + ('emoji','emoji','2563 1'), + ('auto_emoji','auto_emoji','3 1'); + +DELETE FROM "sqlite_stat4"; +INSERT INTO "sqlite_stat4" ("tbl","idx","neq","nlt","ndlt","sample") VALUES ('sim','sim','1','69','69',X'0231313137363631373038303932333039353039'), + ('sim','sim','1','139','139',X'0231313530383936363934333439373931323332'), + ('sim','sim','1','209','209',X'0231323231383737363334373737323139303732'), + ('sim','sim','1','279','279',X'0231333039313431353735353334313136383636'), + ('sim','sim','1','349','349',X'0231333935343433383235363034313635363434'), + ('sim','sim','1','419','419',X'0231353335363239373830383338353134373030'), + ('sim','sim','1','489','489',X'0231363930333339333730353930363636383034'), + ('sim','sim','1','559','559',X'0231383535353736303637393137323137383133'), + ('reaction','reaction','1','360','360',X'020699d5faceefb5fb4f'), + ('reaction','reaction','1','721','721',X'0206b61095e98b6b2fb1'), + ('reaction','reaction','1','1082','1082',X'0206d1dcb418603a5eaa'), + ('reaction','reaction','1','1443','1443',X'0206ef9fc42b9df746ad'), + ('reaction','reaction','1','1804','1804',X'02060f38c1f98f130605'), + ('reaction','reaction','1','2165','2165',X'02062b53df6dab7b1067'), + ('reaction','reaction','1','2526','2526',X'020645dd7e7f60c4aac7'), + ('reaction','reaction','1','2887','2887',X'0206658d2fe735805979'), + ('channel_room','channel_room','1','43','43',X'023331313434393131333330393139333231363330'), + ('channel_room','channel_room','1','87','87',X'023331313835343033343830303934303335393738'), + ('channel_room','channel_room','1','131','131',X'023331323139353036353836343139303638393839'), + ('channel_room','channel_room','1','175','175',X'023331323336353538333034323331303334393630'), + ('channel_room','channel_room','1','219','219',X'023331323933373932323135333930323234343235'), + ('channel_room','channel_room','1','263','263',X'023331333333323139363936393333323038303937'), + ('channel_room','channel_room','1','307','307',X'0231343835363635393733363433333738363938'), + ('channel_room','channel_room','1','351','351',X'0231373039303432313039353632323234363731'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','6 6','6 6',X'034b3321416a6c4c49464e6248646474424a6d4d73503a636164656e63652e6d6f6531313531333434383735363139343833373233'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','34 34','34 34',X'034b3321474b4a63424a6b527a47634e4855686c50613a636164656e63652e6d6f6531303237393433323532323237323630343637'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','43 43','43 43',X'034b3121484b50534d62736d694673506d6268414f513a636164656e63652e6d6f65313931343837343839393433343034353434'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','58 58','58 58',X'034b33214a4479425a685545706874784f6e6f6569513a636164656e63652e6d6f6531323937323836373434353633313236333532'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','87 87','87 87',X'034b33214e544d724e686e715271695755654d494d523a636164656e63652e6d6f6531323235323434353738393939373031353536'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','108 108','108 108',X'034b332151444e44796656674e7657565345656876713a636164656e63652e6d6f6531313432333134303935353535363435343830'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','131 131','131 131',X'034b3121544171536b575752654b43506f584c6a75483a636164656e63652e6d6f65383737303730363531343733363631393532'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','175 175','175 175',X'034b3321594249486864714e697255585941587845563a636164656e63652e6d6f6531323335303831373939353936373639333730'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','177 177','177 177',X'034b3321594b46454e79716667696951686956496b533a636164656e63652e6d6f6531323934363237303431343530333933373034'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','186 186','186 186',X'034b3321596f54644f55766a53765349767266716c653a636164656e63652e6d6f6531323734313936373733383435333430323933'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','202 202','202 202',X'034b3121625877616673695372655647676470535a463a636164656e63652e6d6f65373339303137363739373936343336393932'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','208 208','208 208',X'034b3321634a4b6843764943795377717a47634551423a636164656e63652e6d6f6531323732363632303331323238373331343834'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','219 219','219 219',X'034b3121656455786a56647a6755765844554951434b3a636164656e63652e6d6f65343937313631333530393334353630373738'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','242 242','242 242',X'034b31216a4d746e6e6f51414e4278466a486458494d3a636164656e63652e6d6f65373634353135323932303539323731313939'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','263 263','263 263',X'034b31216c7a776870666a5a6e59797468656a7453483a636164656e63652e6d6f65383838343831373132383438343030343534'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','264 264','264 264',X'034b33216d454c5846716a426958726d7558796943723a636164656e63652e6d6f6531313936393134373631303430393234373432'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','268 268','268 268',X'034b33216d557765577571546761574a767769576a653a636164656e63652e6d6f6531323936373131393236333032333830303834'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','291 291','291 291',X'034b3321704761494e45534643587a634e42497a724e3a636164656e63652e6d6f6531303237343531333333333533313532353232'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','306 306','306 306',X'034b3321717646656248564f4b6876454e54494563763a636164656e63652e6d6f6531323737373238383139323232303230313436'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','307 307','307 307',X'034b332171767370666d716f476449634a66794c506c3a636164656e63652e6d6f6531323936393138333638393539343633343735'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','351 351','351 351',X'034b3321774e7a7741724a47796f4c5168426f544e4b3a636164656e63652e6d6f6531303238303436373930333435333739383930'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','368 368','368 368',X'034b3321794e6d504c7765654a69756570725a677a733a636164656e63652e6d6f6531323531393631373233373731313632363234'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','376 376','376 376',X'034b3121796a4879795772466f704c66646878564e423a636164656e63652e6d6f65333336313537353037303734353233313336'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','379 379','379 379',X'034b31217a4c4f6b62766b44587551465948594555673a636164656e63652e6d6f65393933383838313433373030393330363331'), + ('media_proxy','media_proxy','1','563','563',X'02069e6054680b610946'), + ('media_proxy','media_proxy','1','1127','1127',X'0206bb489b717c9320e4'), + ('media_proxy','media_proxy','1','1691','1691',X'0206d75f602775b7a27c'), + ('media_proxy','media_proxy','1','2255','2255',X'0206f2c705ddca4e2b14'), + ('media_proxy','media_proxy','1','2819','2819',X'02061060db7a5151967b'), + ('media_proxy','media_proxy','1','3383','3383',X'02062cc47366f7550d22'), + ('media_proxy','media_proxy','1','3947','3947',X'020647d275ec0d781fc7'), + ('media_proxy','media_proxy','1','4511','4511',X'02066402024a7ea38249'), + ('sim_proxy','sim_proxy','1','4','4',X'025531316564343731342d636635652d346333372d393331382d376136353266383732636634'), + ('sim_proxy','sim_proxy','1','9','9',X'025533346636333932642d323263372d346337382d393063372d326536323734313535613266'), + ('sim_proxy','sim_proxy','1','14','14',X'025535396662363131392d626133392d346565382d393738612d386432376366303631393633'), + ('sim_proxy','sim_proxy','1','19','19',X'025539373066366536332d646234632d346531342d383063362d336639343938643961363665'), + ('sim_proxy','sim_proxy','1','24','24',X'025561636231613335642d313336662d343362332d626365622d326566646634616265306436'), + ('sim_proxy','sim_proxy','1','29','29',X'025563316635623735392d336136342d343633342d623634632d643461656436316539656632'), + ('sim_proxy','sim_proxy','1','34','34',X'025566323230373135632d633436332d343532622d626233612d373662646662306365353537'), + ('webhook','webhook','1','17','17',X'023331313532383834313435343038373230393936'), + ('webhook','webhook','1','35','35',X'023331313939303936333434333830343631313138'), + ('webhook','webhook','1','53','53',X'023331323331383036353337373032353736313938'), + ('webhook','webhook','1','71','71',X'023331323933373836383939343238383036363536'), + ('webhook','webhook','1','89','89',X'023331333132363031353130363535353537373132'), + ('webhook','webhook','1','107','107',X'0231323937323734313733303636333133373339'), + ('webhook','webhook','1','125','125',X'0231353239313736313536333938363832313137'), + ('webhook','webhook','1','143','143',X'0231363837303238373334333232313437333434'), + ('member_cache','member_cache','4 1','73 74','48 74',X'034b3921496f4866536e67625a6762747061747a494e3a636164656e63652e6d6f65406875636b6c65746f6e3a636164656e63652e6d6f65'), + ('member_cache','member_cache','2 1','86 87','57 87',X'034b2d214b5169714663546e764f6f4f424475746a7a3a636164656e63652e6d6f6540726e6c3a636164656e63652e6d6f65'), + ('member_cache','member_cache','4 1','101 104','68 104',X'034b43214e446249714e704a795076664b526e4e63723a636164656e63652e6d6f6540776f756e6465645f77617272696f723a6d61747269782e6f7267'), + ('member_cache','member_cache','4 1','110 113','73 113',X'034b3b214f485844457370624d485348716c4445614f3a636164656e63652e6d6f6540717561647261646963616c3a6d61747269782e6f7267'), + ('member_cache','member_cache','5 1','171 175','111 175',X'034b3b215450616f6a5454444446444847776c7276743a636164656e63652e6d6f6540766962656973766572796f3a6d61747269782e6f7267'), + ('member_cache','member_cache','39 1','180 208','116 208',X'034b4d2154716c79516d69667847556767456d64424e3a636164656e63652e6d6f6540726f626c6b796f6772653a6372616674696e67636f6d72616465732e6e6574'), + ('member_cache','member_cache','4 1','231 231','126 231',X'034b3b2156624f77675559777146614e4c5345644e413a636164656e63652e6d6f654061666c6f7765723a73796e646963617465642e676179'), + ('member_cache','member_cache','9 1','262 263','141 263',X'034b3b21594b46454e79716667696951686956496b533a636164656e63652e6d6f654062656e6d61633a636861742e62656e6d61632e78797a'), + ('member_cache','member_cache','3 1','283 283','149 283',X'034b35215a615a4d78456f52724d6d4e49554d79446c3a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','88 1','307 351','166 351',X'034b3b2163427874565278446c5a765356684a58564b3a636164656e63652e6d6f65406a61736b617274683a736c656570696e672e746f776e'), + ('member_cache','member_cache','11 1','408 415','177 415',X'034b5121654856655270706e6c6f57587177704a6e553a636164656e63652e6d6f65406a61636b736f6e6368656e3636363a6a61636b736f6e6368656e3636362e636f6d'), + ('member_cache','member_cache','7 1','423 424','181 424',X'034b4b2165724f7079584e465a486a48724568784e583a636164656e63652e6d6f6540616d796973636f6f6c7a3a6d61747269782e6174697573616d792e636f6d'), + ('member_cache','member_cache','96 1','436 439','187 439',X'034b4b21676865544b5a7451666c444e7070684c49673a636164656e63652e6d6f6540616c65783a73706163652e67616d65727374617665726e2e6f6e6c696e65'), + ('member_cache','member_cache','96 1','436 527','187 527',X'034b3121676865544b5a7451666c444e7070684c49673a636164656e63652e6d6f654078796c6f626f6c3a616d6265722e74656c'), + ('member_cache','member_cache','10 1','546 555','197 555',X'0351312169537958674e7851634575586f587073536e3a707573737468656361742e6f726740797562697175653a6e6f70652e63686174'), + ('member_cache','member_cache','13 1','594 601','224 601',X'034b2b216c7570486a715444537a774f744d59476d493a636164656e63652e6d6f6540656c6c69753a68617368692e7265'), + ('member_cache','member_cache','2 1','614 615','229 615',X'034b2f216d584978494644676c4861734e53427371773a636164656e63652e6d6f654077696e673a666561746865722e6f6e6c'), + ('member_cache','member_cache','4 1','616 619','230 619',X'034b2f216d616767455367755a427147425a74536e723a636164656e63652e6d6f654077696e673a666561746865722e6f6e6c'), + ('member_cache','member_cache','4 1','659 660','259 660',X'034b332172454f73706e5971644f414c4149466e69563a636164656e63652e6d6f6540656c797369613a636164656e63652e6d6f65'), + ('member_cache','member_cache','4 1','699 701','284 701',X'034b3521766e717a56767678534a586c5a504f5276533a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','1 1','703 703','285 703',X'034b3521767165714c474851616842464a56566779483a636164656e63652e6d6f654063696465723a6361746769726c2e636c6f7564'), + ('member_cache','member_cache','4 1','705 705','287 705',X'034b35217750454472596b77497a6f744e66706e57503a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','35 1','709 709','288 709',X'034b2d2177574f667376757356486f4e4e567242585a3a636164656e63652e6d6f654061613a6361747669626572732e6d65'), + ('member_cache','member_cache','14 1','747 749','291 749',X'034b3721776c534544496a44676c486d42474b7254703a636164656e63652e6d6f654062616461746e616d65733a62616461742e646576'), + ('member_power','member_power','1 1','0 0','0 0',X'03350f40636164656e63653a636164656e63652e6d6f652a'), + ('file','file','1','2429','2429',X'03815f68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313039393033313838373530303033343038382f313333313336303134333238333036303833372f50584c5f32303235303132315f3230323934323137372e6a7067'), + ('file','file','1','4859','4859',X'03817568747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313134353832313533383832323637323436362f313330323232393131303834373936373331352f53637265656e73686f745f32303234313130325f3034313332365f5265646469742e6a7067'), + ('file','file','1','7289','7289',X'03815968747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313231393439383932363436363636323433302f313239373634363930353038353636313234362f494d475f32303234313032305f3135323230302e6a7067'), + ('file','file','1','9719','9719',X'03814168747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3135393136353731343139343735393638302f313236383537333933363531343337313635392f494d475f353433362e6a7067'), + ('file','file','1','12149','12149',X'03813b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3236363736373539303634313233383032372f313237323430333931313939383730313630392f696d6167652e706e67'), + ('file','file','1','14579','14579',X'03816b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3539383730363933323736303434343936392f313237373532343532343330383632373437372f45585445524e414c5f454449545f323032345f4d5f64726166745f322e646f6378'), + ('file','file','1','17009','17009',X'03815768747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3635353231363137333639363238363734362f313333333630333132383634313036303938342f323032352d30312d32375f31372e30312e31352e706e67'), + ('file','file','1','19439','19439',X'027f68747470733a2f2f63646e2e646973636f72646170702e636f6d2f656d6f6a69732f313230323936303730343936313931323836322e706e67'), + ('message_channel','message_channel','1','40764','40764',X'023331313630333434353733303030383232383735'), + ('message_channel','message_channel','1','81529','81529',X'023331313830323437393130343238393837343333'), + ('message_channel','message_channel','1','122294','122294',X'023331313938303533383732383337363131363230'), + ('message_channel','message_channel','1','163059','163059',X'023331323237373739373330333839303738303537'), + ('message_channel','message_channel','1','203824','203824',X'023331323437303438333039303031313538373137'), + ('message_channel','message_channel','1','244589','244589',X'023331323635363939353734333034303830303034'), + ('message_channel','message_channel','1','285354','285354',X'023331323835343637363238323434313732383234'), + ('message_channel','message_channel','1','326119','326119',X'023331333038373932333935313031333736353732'), + ('lottie','lottie','1','2','2',X'0231373439303534363630373639323138363331'), + ('lottie','lottie','1','5','5',X'0231373534313037353339323030363731373635'), + ('lottie','lottie','1','8','8',X'0231373936313430363338303933343433303932'), + ('lottie','lottie','1','11','11',X'0231373936313431373032363935343835353030'), + ('lottie','lottie','1','14','14',X'0231383136303837373932323931323832393434'), + ('lottie','lottie','1','17','17',X'0231383233393736313032393736323930383636'), + ('event_message','event_message','11 1','14788 14796','14356 14796',X'03336531313532303033373639343137303839303434246d44714a474b3530424a715170394273684e534d64365f3768494354776e70362d793130786d6669766563'), + ('event_message','event_message','11 1','33806 33815','32914 33815',X'033365313135373630333638373035373836363736322459526f7139484b376e55677a397668796f6e3053424a49497978497a5750734e4f6e39756f765644664d45'), + ('event_message','event_message','1 1','42546 42546','41335 42546',X'03336531313630363236383737353835363938393237245033436e4f6d6a35462d6939454e4e79586b70796f5679306b74324a654464764276326e746d33566a4355'), + ('event_message','event_message','2 1','85093 85093','81999 85093',X'0333653131383039393939363531323930343831303424496748666562784533746e623047412d534f7176594a354e4e55385735706c68523159676854636a554734'), + ('event_message','event_message','11 1','116157 116165','111525 116165',X'03336531313933363837333730363137333237363536245a32305734766c737079566e387a6e2d526a6f64694a51745f5a7851644f5f33744e415169453755356477'), + ('event_message','event_message','1 1','127640 127640','122328 127640',X'0333653132303132353637313733373633303332323624702d626f415672476a4b45327a7158664f3738387a5a597a376a42624648717431334d386464705467476f'), + ('event_message','event_message','16 1','140270 140281','134379 140281',X'03336531323039333735363534373138343830343235246c374a4f7543526b7756306d627a69356e5843496b4538416a6951374f67473455456c2d7053445a516649'), + ('event_message','event_message','11 1','162065 162071','154933 162071',X'0333653132323434383135393033313937373537373424674c77513179796e4b6d5859496b5a597a4a55627a66557a55552d714c4b5f524f454e4250325f6e44766b'), + ('event_message','event_message','1 1','170187 170187','162530 170187',X'0333653132323937373533363839343532373038333424656c6c76416a544a5847627936767249767470677a555572787231716f5a75536e50525f474b4e35455945'), + ('event_message','event_message','11 1','178736 178736','170762 178736',X'03336531323333353238303533323338333337353537242d39304668552d36455373594b6435484d7237666d6a414a5f6a576149616e356c4776384e655436564959'), + ('event_message','event_message','10 1','180317 180325','172228 180325',X'03336531323334363237303030393333383130323637246a4679416449665a4f54432d2d735971715472473735374c7a50504f34386439657963485477686d797751'), + ('event_message','event_message','1 1','212734 212734','203278 212734',X'03336531323438303131373930303633373637363734246557493950456f4d576b614b4d33416a7030782d6d455f6c4b4a6c495f6d6d44686f6379464b5170534f59'), + ('event_message','event_message','11 1','228100 228103','217856 228103',X'033365313235333734353736363733373132313336322447326a7a746e5977716a3676304a7a4168576e30725950596470667a5f496f76426771574a4876434c516b'), + ('event_message','event_message','11 1','240172 240181','229123 240181',X'033365313235393239383538323233303630313735382472635679454c5a76647453547a37366f3669434a4f614a316a756f683835535778494d546c5072517a676f'), + ('event_message','event_message','10 1','240259 240264','229132 240264',X'0333653132353933303030343035333535373235323024535849376a465f696e71424d714c4f564b347659746852644b31724747502d435a5f355a354f6b774e3751'), + ('event_message','event_message','2 1','255280 255281','243230 255281',X'03336531323636373837343338353137343234313738246e6e4e576f526a54495757723441463770625454746b7a73784c4d336b312d6d6645665031496b43586e59'), + ('event_message','event_message','1 1','297828 297828','283436 297828',X'0333653132383637303937353334363834323032313024544342465970356f39767a2d4f6b2d33654f4e433772426d354966615934476b48536e58445257474f4767'), + ('event_message','event_message','1 1','340375 340375','323489 340375',X'033365313330393336363936343530353934303030392431664536386d2d50546e786d5474345458584c35754847594139353779396c76582d50797150496d395f30'), + ('event_message','event_message','11 1','343791 343799','326730 343799',X'03336531333131353731333337393331363537323737246731767241315269725951592d3933304f6973587142372d6961686e34684e492d6462374952714176616b'), + ('event_message','event_message','10 1','363207 363216','344868 363216',X'0333653133323434393732323633393438393834393524784f78636e4749364269545941734d7a4f7557316f526e68356b675378544e30466442386a3037695f4f67'), + ('event_message','event_message','10 1','363219 363219','344871 363219',X'033365313332343439373532363733323039393732352432586f7938567937643843375a47767472657966326b4c4f4159397546386b4755364c3642435f446b6d77'), + ('event_message','event_message','10 1','363340 363343','344918 363343',X'03336531333234353037313732333634373530393830244f46645731396649534176706d34646c36367a2d5543337236432d436354506e34752d63444f7036733345'), + ('event_message','event_message','11 1','369452 369456','350712 369456',X'0333653133323736353831313733343439323336383024574d774330644574417277375f4562554a534465546532577174506d3747584347774570646c4f79326d30'), + ('event_message','event_message','10 1','372353 372356','353425 372356',X'0333653133323933313737353039313234353035373224366259364d313472667163486d5854476349716d4a4366467471796839794472375a6a487463715a6d4f34'), + ('sim_member','sim_member','225 1','0 12','0 12',X'034b4721414956694e775a64636b4652764c4f4567433a636164656e63652e6d6f65405f6f6f79655f616b6972615f6e6965723a636164656e63652e6d6f65'), + ('sim_member','sim_member','2 1','319 319','14 319',X'034b43214456706f6e54524d56456570486378744c423a636164656e63652e6d6f65405f6f6f79655f656e746f6c6f6d613a636164656e63652e6d6f65'), + ('sim_member','sim_member','68 1','391 440','26 440',X'034b4921457a54624a496c496d45534f746b4e644e4a3a636164656e63652e6d6f65405f6f6f79655f73617475726461797465643a636164656e63652e6d6f65'), + ('sim_member','sim_member','8 1','638 639','59 639',X'034b3f21497a4f675169446e757346516977796d614c3a636164656e63652e6d6f65405f6f6f79655f636f6f6b69653a636164656e63652e6d6f65'), + ('sim_member','sim_member','31 1','743 771','86 771',X'034b49214d5071594e414a62576b72474f544a7461703a636164656e63652e6d6f65405f6f6f79655f746865666f6f6c323239343a636164656e63652e6d6f65'), + ('sim_member','sim_member','26 1','774 787','87 787',X'034b49214d687950614b4250506f496c7365794d6d743a636164656e63652e6d6f65405f6f6f79655f6b79757567727970686f6e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','877 881','104 881',X'034b45215063734371724f466a48476f41424270414c3a636164656e63652e6d6f65405f6f6f79655f62696c6c795f626f623a636164656e63652e6d6f65'), + ('sim_member','sim_member','16 1','956 959','117 959',X'034b43215158526f4a777a63506d5047546d454b454d3a636164656e63652e6d6f65405f6f6f79655f626f7472616334723a636164656e63652e6d6f65'), + ('sim_member','sim_member','32 1','997 1012','121 1012',X'034b3f215170676c734e587a4c7751594d4c6c734f503a636164656e63652e6d6f65405f6f6f79655f6a75746f6d693a636164656e63652e6d6f65'), + ('sim_member','sim_member','7 1','1274 1279','157 1279',X'034b4121554d6f6e68556765644d47585a78466658753a636164656e63652e6d6f65405f6f6f79655f6d696e696d75733a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','1415 1439','188 1439',X'034b3d21595868717249786d586e47736961796a59783a636164656e63652e6d6f65405f6f6f79655f73746161663a636164656e63652e6d6f65'), + ('sim_member','sim_member','16 1','1597 1599','217 1599',X'034b512163466a4479477274466d48796d794c6652453a636164656e63652e6d6f65405f6f6f79655f626f6a61636b5f686f7273656d616e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','1758 1761','248 1761',X'034b3d2168665a74624d656f5355564e424850736a743a636164656e63652e6d6f65405f6f6f79655f617a7572653a636164656e63652e6d6f65'), + ('sim_member','sim_member','25 1','1865 1886','270 1886',X'034b3f216b4c52714b4b555158636962494d744f706c3a636164656e63652e6d6f65405f6f6f79655f7361796f72693a636164656e63652e6d6f65'), + ('sim_member','sim_member','19 1','1918 1919','276 1919',X'034b3d216b68497350756c465369736d43646c596e493a636164656e63652e6d6f65405f6f6f79655f617a7572653a636164656e63652e6d6f65'), + ('sim_member','sim_member','33 1','1986 2015','286 2015',X'034b3d216d5451744d736a534c4f646c576f7265594d3a636164656e63652e6d6f65405f6f6f79655f73746161663a636164656e63652e6d6f65'), + ('sim_member','sim_member','37 1','2027 2028','289 2028',X'034b4d216d616767455367755a427147425a74536e723a636164656e63652e6d6f65405f6f6f79655f2e7265616c2e706572736f6e2e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','28 1','2117 2130','297 2130',X'034b3f216e4e595a794b6f4e70797859417a50466f733a636164656e63652e6d6f65405f6f6f79655f6a75746f6d693a636164656e63652e6d6f65'), + ('sim_member','sim_member','20 1','2230 2239','310 2239',X'034b41217046504c7270594879487a784e4c69594b413a636164656e63652e6d6f65405f6f6f79655f686578676f61743a636164656e63652e6d6f65'), + ('sim_member','sim_member','30 1','2381 2398','332 2398',X'034b3b21717a44626c4b6c69444c577a52524f6e465a3a636164656e63652e6d6f65405f6f6f79655f6d6e696b3a636164656e63652e6d6f65'), + ('sim_member','sim_member','38 1','2490 2518','344 2518',X'034b3d2173445250714549546e4f4e57474176496b423a636164656e63652e6d6f65405f6f6f79655f727974686d3a636164656e63652e6d6f65'), + ('sim_member','sim_member','11 1','2555 2559','358 2559',X'034b4321746751436d526b426e6474516362687150583a636164656e63652e6d6f65405f6f6f79655f6a6f7365707065793a636164656e63652e6d6f65'), + ('sim_member','sim_member','47 1','2633 2666','377 2666',X'034b4b217750454472596b77497a6f744e66706e57503a636164656e63652e6d6f65405f6f6f79655f6e61706f6c656f6e333038393a636164656e63652e6d6f65'), + ('sim_member','sim_member','52 1','2817 2837','414 2837',X'034b43217a66654e574d744b4f764f48766f727979563a636164656e63652e6d6f65405f6f6f79655f696e736f676e69613a636164656e63652e6d6f65'), + ('guild_space','guild_space','1','3','3',X'0231313132373630363639313738323431303234'), + ('guild_space','guild_space','1','7','7',X'023331313534383638343234373234343633363837'), + ('guild_space','guild_space','1','11','11',X'023331323139303338323637383430393235383138'), + ('guild_space','guild_space','1','15','15',X'023331323839353939363232353930383930313335'), + ('guild_space','guild_space','1','19','19',X'0231323733383737363437323234393935383431'), + ('guild_space','guild_space','1','23','23',X'0231353239313736313536333938363832313135'), + ('guild_space','guild_space','1','27','27',X'0231373535303134333534373334313533383138'), + ('guild_space','guild_space','1','31','31',X'0231393933383838313432343535323130303834'), + ('guild_active','guild_active','1','3','3',X'0231313132373630363639313738323431303234'), + ('guild_active','guild_active','1','7','7',X'023331313534383638343234373234343633363837'), + ('guild_active','guild_active','1','11','11',X'023331323139303338323637383430393235383138'), + ('guild_active','guild_active','1','15','15',X'023331323839353939363232353930383930313335'), + ('guild_active','guild_active','1','19','19',X'023331333333323139363936393333323038303934'), + ('guild_active','guild_active','1','23','23',X'0231343735353939303338353336373434393630'), + ('guild_active','guild_active','1','27','27',X'022f3636313932393535373737343836383438'), + ('guild_active','guild_active','1','31','31',X'0231383737303635303431393930353136373637'), + ('emoji','emoji','1','284','284',X'023331313132323031303430303637303339333332'), + ('emoji','emoji','1','569','569',X'023331323334393032303131393436383630363638'), + ('emoji','emoji','1','854','854',X'0231323735313734373438353034313935303732'), + ('emoji','emoji','1','1139','1139',X'0231333837343730383630303134393737303235'), + ('emoji','emoji','1','1424','1424',X'0231353435363639393734303130383838313932'), + ('emoji','emoji','1','1709','1709',X'0231363339313034333030393139393437323634'), + ('emoji','emoji','1','1994','1994',X'0231373532363932363230303638373832313231'), + ('emoji','emoji','1','2279','2279',X'0231383935343737353331303434363138323430'), + ('auto_emoji','auto_emoji','1','0','0',X'02114c31'), + ('auto_emoji','auto_emoji','1','1','1',X'02114c32'), + ('auto_emoji','auto_emoji','1','2','2',X'020f5f'); + +ANALYZE sqlite_schema; + +COMMIT; From d4a50cb8aaf6637bbdab6db676fd54cf32d10fba Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 30 Jan 2025 22:25:25 +1300 Subject: [PATCH 204/346] Do not run as root --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 6f2f477..1f01d01 100644 --- a/readme.md +++ b/readme.md @@ -62,7 +62,7 @@ Only necessary data and columns are queried from the database. We only contact t File uploads (like avatars from bridged members) are checked locally and deduplicated. Only brand new files are uploaded to the homeserver. This saves loads of space in the homeserver's media repo, especially for Synapse. -Switching to [WAL mode](https://www.sqlite.org/wal.html) could improve your database access speed even more. Run `node scripts/wal.js` if you want to switch to WAL mode. This will also enable `synchronous = NORMAL`. +Switching to [WAL mode](https://www.sqlite.org/wal.html) could improve your database access speed even more. Run `node scripts/wal.js` if you want to switch to WAL mode. (This will also enable `synchronous = NORMAL`.) # Setup @@ -78,6 +78,8 @@ Follow these steps: 1. [Get Node.js version 20 or later](https://nodejs.org/en/download/prebuilt-installer) +1. Switch to a normal user account. (i.e. do not run any of the following commands as root or sudo.) + 1. Clone this repo and checkout a specific tag. (Development happens on main. Stable versions are tagged.) * The latest release tag is ![](https://img.shields.io/gitea/v/release/cadence/out-of-your-element?gitea_url=https%3A%2F%2Fgitdab.com&style=flat-square&label=%20&color=black). From 5c0e83065809c1828c0aec8a22ff0dbe51381492 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 31 Jan 2025 15:07:48 +1300 Subject: [PATCH 205/346] Display XHR errors --- src/web/pug/includes/template.pug | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 6a0fb76..17b7847 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -57,8 +57,10 @@ html(lang="en") li(role="menuitem") a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`)) +guild(guild) + //- Body .mx-auto.w100.wmx9.py24.px8.fs-body1#content block body + //- Guild list popover script. document.querySelectorAll("[popovertarget]").forEach(e => { e.addEventListener("click", () => { @@ -69,3 +71,19 @@ html(lang="en") }) }) script(src=rel("/static/htmx.min.js")) + //- Error dialog + aside.s-modal#server-error(aria-hidden="true") + .s-modal--dialog + h1.s-modal--header Server error + pre.overflow-auto#server-error-content + button.s-modal--close.s-btn.s-btn__muted(aria-label="Close" type="button" onclick="hideError()")!= icons.Icons.IconClearSm + .s-modal--footer + button.s-btn.s-btn__outlined.s-btn__muted(type="button" onclick="hideError()") OK + script. + function hideError() { + document.getElementById("server-error").setAttribute("aria-hidden", "true") + } + document.body.addEventListener("htmx:responseError", event => { + document.getElementById("server-error").setAttribute("aria-hidden", "false") + document.getElementById("server-error-content").textContent = event.detail.xhr.responseText + }) From b1b9124052d7ef8ba6563cd8aa4f29226fe9ad4a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 31 Jan 2025 15:09:01 +1300 Subject: [PATCH 206/346] Fully support unlinking channels --- docs/foreign-keys.md | 2 +- src/d2m/actions/create-room.js | 74 ++++++++++++++++++++++++---------- src/web/pug/guild.pug | 5 ++- src/web/routes/link.js | 29 ++++++++++++- 4 files changed, 84 insertions(+), 26 deletions(-) diff --git a/docs/foreign-keys.md b/docs/foreign-keys.md index 9940ed0..1e5e21c 100644 --- a/docs/foreign-keys.md +++ b/docs/foreign-keys.md @@ -45,7 +45,7 @@ Here are some tables that could potentially have foreign keys added between them * The storage cost of the additional index on `sim` would not be worth the benefits. * `channel_room` <--(**C** room_id PK)-- `sim_member` * If a room is being permanently unlinked, it may be useful to see a populated member list. If it's about to be relinked to another channel, we want to keep the sims in the room for more speed and to avoid spamming state events into the timeline. - * Either way, the sims should remain in the room even after it's been unlinked. So no referential integrity is desirable here. + * Either way, the sims could remain in the room even after it's been unlinked. So no referential integrity is desirable here. * `sim` <--(PK user_id PK)-- `sim_proxy` * OOYE left joins on this. In normal operation, this relationship might not exist. * `channel_room` <--(PK channel_id PK)-- `webhook` ✅ diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index e0d019f..690ca2a 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -6,7 +6,7 @@ const Ty = require("../../types") const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") -const {discord, sync, db, select} = passthrough +const {discord, sync, db, select, from} = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") /** @type {import("../../matrix/api")} */ @@ -14,7 +14,9 @@ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") /** @type {import("../../discord/utils")} */ -const utils = sync.require("../../discord/utils") +const dUtils = sync.require("../../discord/utils") +/** @type {import("../../m2d/converters/utils")} */ +const mUtils = sync.require("../../m2d/converters/utils") /** @type {import("./create-space")} */ const createSpace = sync.require("./create-space") @@ -114,8 +116,8 @@ async function channelToKState(channel, guild, di) { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } - const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) - const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) + const everyonePermissions = dUtils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) + const everyoneCanMentionEveryone = dUtils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all() const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {}) @@ -392,7 +394,7 @@ function syncRoom(channelID) { return _syncRoom(channelID, true) } -async function _unbridgeRoom(channelID) { +async function unbridgeChannel(channelID) { /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) assert.ok(channel) @@ -407,12 +409,8 @@ async function _unbridgeRoom(channelID) { async function unbridgeDeletedChannel(channel, guildID) { const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() assert.ok(roomID) - const spaceID = select("guild_space", "space_id", {guild_id: guildID}).pluck().get() - assert.ok(spaceID) - - // remove room from being a space member - await api.sendState(roomID, "m.space.parent", spaceID, {}) - await api.sendState(spaceID, "m.space.child", roomID, {}) + const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").get() + assert.ok(row) // remove declaration that the room is bridged await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {}) @@ -421,15 +419,6 @@ async function unbridgeDeletedChannel(channel, guildID) { await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""}) } - // send a notification in the room - await api.sendEvent(roomID, "m.room.message", { - msgtype: "m.notice", - body: "⚠️ This room was removed from the bridge." - }) - - // leave room - await api.leaveRoom(roomID) - // delete webhook on discord const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get() if (webhook) { @@ -439,7 +428,48 @@ async function unbridgeDeletedChannel(channel, guildID) { // delete room from database db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID) - db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) + db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages + + // demote admins in room + /** @type {Ty.Event.M_Power_Levels} */ + const powerLevelContent = await api.getStateEvent(roomID, "m.room.power_levels", "") + powerLevelContent.users ??= {} + const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}` + for (const mxid of Object.keys(powerLevelContent.users)) { + if (mUtils.eventSenderIsFromDiscord(mxid) && mxid !== bot) { + delete powerLevelContent.users[mxid] + await api.sendState(roomID, "m.room.power_levels", "", powerLevelContent, mxid) + } + } + + // send a notification in the room + await api.sendEvent(roomID, "m.room.message", { + msgtype: "m.notice", + body: "⚠️ This room was removed from the bridge." + }) + + // if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged + // (don't do this for self-service rooms, because they might continue to be used on Matrix or linked somewhere else later) + if (row.autocreate === 1) { + // remove room from being a space member + await api.sendState(roomID, "m.space.parent", row.space_id, {}) + await api.sendState(row.space_id, "m.space.child", roomID, {}) + + // leave room + await api.leaveRoom(roomID) + } + + // if it is a self-service room, remove sim members + // (the room can be used with less clutter and the member list makes sense if it's bridged somewhere else) + if (row.autocreate === 0) { + // remove sim members + const members = select("sim_member", "mxid", {room_id: roomID}).pluck().all() + const preparedDelete = db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?") + for (const mxid of members) { + await api.leaveRoom(roomID, mxid) + preparedDelete.run(roomID, mxid) + } + } } /** @@ -488,7 +518,7 @@ module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic -module.exports._unbridgeRoom = _unbridgeRoom +module.exports.unbridgeChannel = unbridgeChannel module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel module.exports.existsOrAutocreatable = existsOrAutocreatable module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 34178bf..51b2c99 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -78,12 +78,13 @@ block body h3.mt32.fs-category Linked channels .s-card.bs-sm.p0 - .s-table-container + form.s-table-container(method="post" action="/api/unlink" hx-post="/api/unlink" hx-trigger="submit" hx-disabled-elt="this" hx-confirm="Unlink these channels?\nIt may take a moment to clean up Matrix resources.") + input(type="hidden" name="guild_id" value=guild_id) table.s-table.s-table__bx-simple each row in linkedChannelsWithDetails tr td.w40: +discord(row.channel) - td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm + td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" value=row.channel.id)!= icons.Icons.IconLinkSm td: +matrix(row) else tr diff --git a/src/web/routes/link.js b/src/web/routes/link.js index e63c01b..52c750c 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -1,7 +1,7 @@ // @ts-check const {z} = require("zod") -const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3") +const {defineEventHandler, useSession, createError, readValidatedBody, setResponseHeader} = require("h3") const Ty = require("../../types") const {discord, db, as, sync, select, from} = require("../../passthrough") @@ -19,6 +19,10 @@ const schema = { guild_id: z.string(), matrix: z.string(), discord: z.string() + }), + unlink: z.object({ + guild_id: z.string(), + channel_id: z.string() }) } @@ -61,3 +65,26 @@ as.router.post("/api/link", defineEventHandler(async event => { return null // 204 })) + +as.router.post("/api/unlink", defineEventHandler(async event => { + const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse) + const session = await useSession(event, {password: reg.as_token}) + + // Check guild ID or nonce + if (!(session.data.managedGuilds || []).includes(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) + + // Check channel is part of this guild + const channel = discord.channels.get(channel_id) + if (!channel) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} does not exist`}) + if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`}) + + // Check channel is currently bridged + const row = select("channel_room", "channel_id", {channel_id: channel_id}).get() + if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`}) + + // Do it + await createRoom.unbridgeDeletedChannel(channel, guild_id) + + setResponseHeader(event, "HX-Refresh", "true") + return null // 204 +})) From a459ee1d1cce03b6d7dc5fe8dd872f8c81a28fee Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 31 Jan 2025 16:42:15 +1300 Subject: [PATCH 207/346] Use htmx.js instead of htmx.min.js This wastes 30 kB gzipped, which I think is acceptable in exchange for having method names in the debugger. --- src/web/pug/includes/template.pug | 178 +- src/web/server.js | 4 +- src/web/static/htmx.js | 5261 +++++++++++++++++++++++++++++ src/web/static/htmx.min.js | 1 - 4 files changed, 5352 insertions(+), 92 deletions(-) create mode 100644 src/web/static/htmx.js delete mode 100644 src/web/static/htmx.min.js diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 17b7847..909244f 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -1,89 +1,89 @@ -mixin guild(guild) - span.s-avatar.s-avatar__32.s-user-card--avatar - if guild.icon - img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`) - else - .s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0] - .s-user-card--info.ai-start - strong= guild.name - ul.s-user-card--awards - li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels - -doctype html -html(lang="en") - head - title Out Of Your Element - - link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css")) - - meta(name="htmx-config" content='{"indicatorClass":"is-loading"}') - style. - .themed { - --theme-base-primary-color-h: 266; - --theme-base-primary-color-s: 53%; - --theme-base-primary-color-l: 63%; - --theme-dark-primary-color-h: 266; - --theme-dark-primary-color-s: 53%; - --theme-dark-primary-color-l: 63%; - } - .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental input[type="radio"]:checked ~ label:not(.s-toggle-switch--label-off) { - --_ts-multiple-bg: var(--green-400); - --_ts-multiple-fc: var(--white); - } - body.themed.theme-system - header.s-topbar - .s-topbar--skip-link(href="#content") Skip to main content - .s-topbar--container.wmx9 - a.s-topbar--logo(href=rel("/")) - img.s-avatar.s-avatar__32(src=rel("/icon.png")) - nav.s-topbar--navigation - ul.s-topbar--content - li.ps-relative - if !session.data.managedGuilds || session.data.managedGuilds.length === 0 - a.s-btn.s-btn__icon.as-center(href=rel("/oauth")) - != icons.Icons.IconDiscord - = ` Log in` - else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id) - button.s-topbar--item.s-btn.s-btn__muted.s-user-card(popovertarget="guilds") - +guild(discord.guilds.get(guild_id)) - else if session.data.managedGuilds - button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.s-label(popovertarget="guilds") - | Your servers - #guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible - .s-popover--arrow.s-popover--arrow__tc - .s-popover--content.overflow-y-auto.overflow-x-hidden - ul.s-menu(role="menu") - each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g) - li(role="menuitem") - a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`)) - +guild(guild) - //- Body - .mx-auto.w100.wmx9.py24.px8.fs-body1#content - block body - //- Guild list popover - script. - document.querySelectorAll("[popovertarget]").forEach(e => { - e.addEventListener("click", () => { - const rect = e.getBoundingClientRect() - const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }` - // console.log(t) - document.styleSheets[0].insertRule(t) - }) - }) - script(src=rel("/static/htmx.min.js")) - //- Error dialog - aside.s-modal#server-error(aria-hidden="true") - .s-modal--dialog - h1.s-modal--header Server error - pre.overflow-auto#server-error-content - button.s-modal--close.s-btn.s-btn__muted(aria-label="Close" type="button" onclick="hideError()")!= icons.Icons.IconClearSm - .s-modal--footer - button.s-btn.s-btn__outlined.s-btn__muted(type="button" onclick="hideError()") OK - script. - function hideError() { - document.getElementById("server-error").setAttribute("aria-hidden", "true") - } - document.body.addEventListener("htmx:responseError", event => { - document.getElementById("server-error").setAttribute("aria-hidden", "false") - document.getElementById("server-error-content").textContent = event.detail.xhr.responseText - }) +mixin guild(guild) + span.s-avatar.s-avatar__32.s-user-card--avatar + if guild.icon + img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`) + else + .s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0] + .s-user-card--info.ai-start + strong= guild.name + ul.s-user-card--awards + li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels + +doctype html +html(lang="en") + head + title Out Of Your Element + + link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css")) + + meta(name="htmx-config" content='{"indicatorClass":"is-loading"}') + style. + .themed { + --theme-base-primary-color-h: 266; + --theme-base-primary-color-s: 53%; + --theme-base-primary-color-l: 63%; + --theme-dark-primary-color-h: 266; + --theme-dark-primary-color-s: 53%; + --theme-dark-primary-color-l: 63%; + } + .s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental input[type="radio"]:checked ~ label:not(.s-toggle-switch--label-off) { + --_ts-multiple-bg: var(--green-400); + --_ts-multiple-fc: var(--white); + } + body.themed.theme-system + header.s-topbar + .s-topbar--skip-link(href="#content") Skip to main content + .s-topbar--container.wmx9 + a.s-topbar--logo(href=rel("/")) + img.s-avatar.s-avatar__32(src=rel("/icon.png")) + nav.s-topbar--navigation + ul.s-topbar--content + li.ps-relative + if !session.data.managedGuilds || session.data.managedGuilds.length === 0 + a.s-btn.s-btn__icon.as-center(href=rel("/oauth")) + != icons.Icons.IconDiscord + = ` Log in` + else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id) + button.s-topbar--item.s-btn.s-btn__muted.s-user-card(popovertarget="guilds") + +guild(discord.guilds.get(guild_id)) + else if session.data.managedGuilds + button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.s-label(popovertarget="guilds") + | Your servers + #guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible + .s-popover--arrow.s-popover--arrow__tc + .s-popover--content.overflow-y-auto.overflow-x-hidden + ul.s-menu(role="menu") + each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g) + li(role="menuitem") + a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`)) + +guild(guild) + //- Body + .mx-auto.w100.wmx9.py24.px8.fs-body1#content + block body + //- Guild list popover + script. + document.querySelectorAll("[popovertarget]").forEach(e => { + e.addEventListener("click", () => { + const rect = e.getBoundingClientRect() + const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }` + // console.log(t) + document.styleSheets[0].insertRule(t) + }) + }) + script(src=rel("/static/htmx.js")) + //- Error dialog + aside.s-modal#server-error(aria-hidden="true") + .s-modal--dialog + h1.s-modal--header Server error + pre.overflow-auto#server-error-content + button.s-modal--close.s-btn.s-btn__muted(aria-label="Close" type="button" onclick="hideError()")!= icons.Icons.IconClearSm + .s-modal--footer + button.s-btn.s-btn__outlined.s-btn__muted(type="button" onclick="hideError()") OK + script. + function hideError() { + document.getElementById("server-error").setAttribute("aria-hidden", "true") + } + document.body.addEventListener("htmx:responseError", event => { + document.getElementById("server-error").setAttribute("aria-hidden", "false") + document.getElementById("server-error-content").textContent = event.detail.xhr.responseText + }) diff --git a/src/web/server.js b/src/web/server.js index 9373947..39c0a68 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -49,12 +49,12 @@ as.router.get("/static/stacks.min.css", defineEventHandler({ } })) -as.router.get("/static/htmx.min.js", defineEventHandler({ +as.router.get("/static/htmx.js", defineEventHandler({ onBeforeResponse: compressResponse, handler: async event => { handleCacheHeaders(event, {maxAge: 86400}) defaultContentType(event, "text/javascript") - return fs.promises.readFile(join(__dirname, "static", "htmx.min.js"), "utf-8") + return fs.promises.readFile(join(__dirname, "static", "htmx.js"), "utf-8") } })) diff --git a/src/web/static/htmx.js b/src/web/static/htmx.js new file mode 100644 index 0000000..370cc0f --- /dev/null +++ b/src/web/static/htmx.js @@ -0,0 +1,5261 @@ +var htmx = (function() { + 'use strict' + + // Public API + const htmx = { + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine + /* Event processing */ + /** @type {typeof onLoadHelper} */ + onLoad: null, + /** @type {typeof processNode} */ + process: null, + /** @type {typeof addEventListenerImpl} */ + on: null, + /** @type {typeof removeEventListenerImpl} */ + off: null, + /** @type {typeof triggerEvent} */ + trigger: null, + /** @type {typeof ajaxHelper} */ + ajax: null, + /* DOM querying helpers */ + /** @type {typeof find} */ + find: null, + /** @type {typeof findAll} */ + findAll: null, + /** @type {typeof closest} */ + closest: null, + /** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ + values: function(elt, type) { + const inputValues = getInputValues(elt, type || 'post') + return inputValues.values + }, + /* DOM manipulation helpers */ + /** @type {typeof removeElement} */ + remove: null, + /** @type {typeof addClassToElement} */ + addClass: null, + /** @type {typeof removeClassFromElement} */ + removeClass: null, + /** @type {typeof toggleClassOnElement} */ + toggleClass: null, + /** @type {typeof takeClassForElement} */ + takeClass: null, + /** @type {typeof swap} */ + swap: null, + /* Extension entrypoints */ + /** @type {typeof defineExtension} */ + defineExtension: null, + /** @type {typeof removeExtension} */ + removeExtension: null, + /* Debugging */ + /** @type {typeof logAll} */ + logAll: null, + /** @type {typeof logNone} */ + logNone: null, + /* Debugging */ + /** + * The logger htmx uses to log with + * + * @see https://htmx.org/api/#logger + */ + logger: null, + /** + * A property holding the configuration htmx uses at runtime. + * + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. + * + * @see https://htmx.org/api/#config + */ + config: { + /** + * Whether to use history. + * @type boolean + * @default true + */ + historyEnabled: true, + /** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ + historyCacheSize: 10, + /** + * @type boolean + * @default false + */ + refreshOnHistoryMiss: false, + /** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ + defaultSwapStyle: 'innerHTML', + /** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ + defaultSwapDelay: 0, + /** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ + defaultSettleDelay: 20, + /** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ + includeIndicatorStyles: true, + /** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ + indicatorClass: 'htmx-indicator', + /** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ + requestClass: 'htmx-request', + /** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ + addedClass: 'htmx-added', + /** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ + settlingClass: 'htmx-settling', + /** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ + swappingClass: 'htmx-swapping', + /** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ + allowEval: true, + /** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ + allowScriptTags: true, + /** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ + inlineScriptNonce: '', + /** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ + inlineStyleNonce: '', + /** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ + attributesToSettle: ['class', 'style', 'width', 'height'], + /** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ + withCredentials: false, + /** + * @type number + * @default 0 + */ + timeout: 0, + /** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ + wsReconnectDelay: 'full-jitter', + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: 'blob', + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: '[hx-disable], [data-hx-disable]', + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'instant' + */ + scrollBehavior: 'instant', + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ['get', 'delete'], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: '204', swap: false }, + { code: '[23]..', swap: true }, + { code: '[45]..', swap: false, error: true } + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: '2.0.4' + } + // Tsc madness part 2 + htmx.onLoad = onLoadHelper + htmx.process = processNode + htmx.on = addEventListenerImpl + htmx.off = removeEventListenerImpl + htmx.trigger = triggerEvent + htmx.ajax = ajaxHelper + htmx.find = find + htmx.findAll = findAll + htmx.closest = closest + htmx.remove = removeElement + htmx.addClass = addClassToElement + htmx.removeClass = removeClassFromElement + htmx.toggleClass = toggleClassOnElement + htmx.takeClass = takeClassForElement + htmx.swap = swap + htmx.defineExtension = defineExtension + htmx.removeExtension = removeExtension + htmx.logAll = logAll + htmx.logNone = logNone + htmx.parseInterval = parseInterval + htmx._ = internalEval + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions + } + + const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERB_SELECTOR = VERBS.map(function(verb) { + return '[hx-' + verb + '], [data-hx-' + verb + ']' + }).join(', ') + + //= =================================================================== + // Utilities + //= =================================================================== + + /** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined + } + + let interval = NaN + if (str.slice(-2) == 'ms') { + interval = parseFloat(str.slice(0, -2)) + } else if (str.slice(-1) == 's') { + interval = parseFloat(str.slice(0, -1)) * 1000 + } else if (str.slice(-1) == 'm') { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60 + } else { + interval = parseFloat(str) + } + return isNaN(interval) ? undefined : interval + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name) + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) || + elt.hasAttribute('data-' + qualifiedName)) + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName) + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode + return parent + } + + /** + * @returns {Document} + */ + function getDocument() { + return document + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument() + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt) + } + + return elt || null + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) { + const attributeValue = getAttributeValue(ancestor, attributeName) + const disinherit = getAttributeValue(ancestor, 'hx-disinherit') + var inherit = getAttributeValue(ancestor, 'hx-inherit') + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) { + return attributeValue + } else { + return null + } + } + if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) { + return 'unset' + } + } + return attributeValue + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null + getClosestMatch(elt, function(e) { + return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName)) + }) + if (closestAttr !== 'unset') { + return closestAttr + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector) + return !!matchesFunction && matchesFunction.call(elt, selector) + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i + const match = tagMatcher.exec(str) + if (match) { + return match[1].toLowerCase() + } else { + return '' + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser() + return parser.parseFromString(resp, 'text/html') + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]) + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement('script') + forEach(script.attributes, function(attr) { + newScript.setAttribute(attr.name, attr.value) + }) + newScript.textContent = script.textContent + newScript.async = false + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce + } + return newScript + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '') + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in