diff --git a/docs/user-guide.md b/docs/user-guide.md index d1beea1..d360806 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -84,7 +84,7 @@ Discord display names for normal users are limited to 32 characters. For webhook If the bridge software was restarted, it will attempt to catch up on messages missed while it was offline. -From Discord, for any given channel, if fewer than 100 messages were missed in that given channel, the bridge will catch up and transfer all of them to Matrix. If more than 100 messages were missed in that given channel, the bridge will only bridge the latest message. Happenings that aren't messages, such as edits and reactions to prior messages, might be missed during catch-up. +From Discord, for any given channel, if fewer than 50 messages were missed in that given channel, the bridge will catch up and transfer all of them to Matrix. If more than 50 messages were missed in that given channel, the bridge will only bridge the latest message. Happenings that aren't messages, such as edits and reactions to prior messages, might be missed during catch-up. From Matrix, all events should be bridged to Discord. diff --git a/package-lock.json b/package-lock.json index eb07b4d..10b4668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "out-of-your-element", - "version": "3.5.1", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "out-of-your-element", - "version": "3.5.1", + "version": "3.4.0", "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", @@ -24,13 +24,13 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.17.1", + "cloudstorm": "^0.17.0", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", "get-relative-path": "^1.0.2", - "h3": "^1.15.10", + "h3": "^1.15.1", "heatsync": "^2.7.2", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", @@ -48,7 +48,7 @@ "@types/node": "^22.17.1", "c8": "^11.0.0", "cross-env": "^7.0.3", - "supertape": "^13.2.0" + "supertape": "^12.0.12" }, "engines": { "node": ">=22" @@ -276,9 +276,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, "dependencies": { @@ -1003,9 +1003,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "dev": true, "license": "MIT", "dependencies": { @@ -1129,9 +1129,9 @@ "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", - "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1139,7 +1139,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, "node_modules/bindings": { @@ -1163,9 +1163,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -1316,12 +1316,12 @@ } }, "node_modules/cloudstorm": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz", - "integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.0.tgz", + "integrity": "sha512-zsd9y5ljNnbxdvDid9TgWePDqo7il4so5spzx6NDwZ67qWQjR96UUhLxJ+BAOdBBSPF9UXFM61dAzC2g918q+A==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.47", + "discord-api-types": "^0.38.40", "snowtransfer": "^0.17.5" }, "engines": { @@ -1366,9 +1366,9 @@ "license": "MIT" }, "node_modules/cookie-es": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", - "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "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": { @@ -1452,9 +1452,9 @@ } }, "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, "node_modules/destr": { @@ -1473,9 +1473,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.47", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", - "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1488,9 +1488,9 @@ "license": "MIT" }, "node_modules/domino": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.7.tgz", - "integrity": "sha512-3rcXhx0ixJV2nj8J0tljzejTF73A35LVVdnTQu79UAqTBFEgYPMgGtykMuu/BDqaOZphATku1ddRUn/RtqUHYQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", "license": "BSD-2-Clause" }, "node_modules/emoji-regex": { @@ -1587,9 +1587,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -1617,9 +1617,9 @@ "license": "MIT" }, "node_modules/fullstore": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.2.tgz", - "integrity": "sha512-syOev4kA0lZy4VkfBJZ99ZL4cIiSgiKt0G8SpP0kla1tpM1c+V/jBOVY/OqqGtR2XLVcM83SjFPFC3R2YIwqjQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.0.tgz", + "integrity": "sha512-Y9hN79Q1CFU8akjGnTZoBnTzlA/o8wmtBijJOI8dKCmdC7GLX7OekpLxmbaeRetTOi4OdFGjfsg4c5dxP3jgPw==", "dev": true, "license": "MIT", "engines": { @@ -1688,14 +1688,14 @@ } }, "node_modules/h3": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", - "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", + "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", "license": "MIT", "dependencies": { - "cookie-es": "^1.2.3", + "cookie-es": "^1.2.2", "crossws": "^0.3.5", - "defu": "^6.1.6", + "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", @@ -1753,9 +1753,9 @@ "license": "MIT" }, "node_modules/htmx.org": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz", - "integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz", + "integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==", "license": "0BSD" }, "node_modules/ieee754": { @@ -1937,9 +1937,9 @@ "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.5.8", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", - "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", "dev": true, "license": "MIT" }, @@ -1974,9 +1974,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2579,12 +2579,12 @@ } }, "node_modules/snowtransfer": { - "version": "0.17.7", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz", - "integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.5.tgz", + "integrity": "sha512-nVI1UJNFoX1ndGFZxB3zb3X5SWtD9hIAcw7wCgVKWvCf42Wg2B4UFIrZWI83HxaSBY0CGbPZmZzZb3RSt/v2wQ==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.47" + "discord-api-types": "^0.38.40" }, "engines": { "node": ">=22.0.0" @@ -2667,9 +2667,9 @@ } }, "node_modules/supertape": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.2.0.tgz", - "integrity": "sha512-UoxZnyoMOdSJHvbcmD8i28MaGXsA7I0cJ0jr8anT4CkmfaE9M1y5mt9EoXyzfC8UdnQZwXOnJLUwqyKLAeUOug==", + "version": "12.10.5", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.10.5.tgz", + "integrity": "sha512-1Px+6mhFaqcht3p4tkf3o4G8lbBazvx4pgFngm4vGwWipYm3fykm6SJ4ThXobiaNsptz53CDWA2q4B/2KtmA4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2856,9 +2856,9 @@ "license": "MIT" }, "node_modules/uqr": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz", - "integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==", + "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": { @@ -3028,9 +3028,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 9dfd2a8..0e666aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "out-of-your-element", - "version": "3.5.1", + "version": "3.4.0", "description": "A bridge between Matrix and Discord", "main": "index.js", "repository": { @@ -33,13 +33,13 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.17.1", + "cloudstorm": "^0.17.0", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", "get-relative-path": "^1.0.2", - "h3": "^1.15.10", + "h3": "^1.15.1", "heatsync": "^2.7.2", "htmx.org": "^2.0.4", "lru-cache": "^11.0.2", @@ -60,7 +60,7 @@ "@types/node": "^22.17.1", "c8": "^11.0.0", "cross-env": "^7.0.3", - "supertape": "^13.2.0" + "supertape": "^12.0.12" }, "scripts": { "start": "node --enable-source-maps start.js", diff --git a/scripts/reset-web-password.js b/scripts/reset-web-password.js index 7c3a1a2..9131efb 100644 --- a/scripts/reset-web-password.js +++ b/scripts/reset-web-password.js @@ -13,5 +13,5 @@ const {prompt} = require("enquirer") reg.ooye.web_password = passwordResponse.web_password writeRegistration(reg) - console.log("Saved. This change should be applied instantly.") + console.log("Saved. Restart Out Of Your Element to apply this change.") })() diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 7f110ad..c2ec01a 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -193,16 +193,6 @@ async function channelToKState(channel, guild, di) { // Don't overwrite room topic if the topic has been customised if (hasCustomTopic) delete channelKState["m.room.topic/"] - // Make voice channels be a Matrix voice room (MSC3417) - if (channel.type === DiscordTypes.ChannelType.GuildVoice) { - creationContent.type = "org.matrix.msc3417.call" - channelKState["org.matrix.msc3401.call/"] = { - "m.intent": "m.room", - "m.type": "m.voice", - "m.name": customName || channel.name - } - } - // Don't add a space parent if it's self service // (The person setting up self-service has already put it in their preferred space to be able to get this far.) const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get() diff --git a/src/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js index c9e098b..36fccba 100644 --- a/src/d2m/actions/create-room.test.js +++ b/src/d2m/actions/create-room.test.js @@ -190,17 +190,6 @@ test("channel2room: read-only discord channel", async t => { t.equal(api.getCalled(), 2) }) -test("channel2room: voice channel", async t => { - const api = mockAPI(t) - const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState)) - t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call") - t.deepEqual(state["org.matrix.msc3401.call/"], { - "m.intent": "m.room", - "m.name": "🍞丨[8user] Piece", - "m.type": "m.voice" - }) -}) - 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/src/d2m/actions/expression.js b/src/d2m/actions/expression.js index 0f714c6..c7ab27a 100644 --- a/src/d2m/actions/expression.js +++ b/src/d2m/actions/expression.js @@ -34,10 +34,7 @@ async function emojisToState(emojis, guild) { 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 } - e["emoji"] = { - name: emoji.name, - id: emoji.id - } + console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`) throw e }) )) diff --git a/src/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js index 43f400d..66ef19e 100644 --- a/src/d2m/actions/retrigger.js +++ b/src/d2m/actions/retrigger.js @@ -2,15 +2,7 @@ const {EventEmitter} = require("events") const passthrough = require("../../passthrough") -const {select, sync, from} = passthrough -/** @type {import("../../matrix/utils")} */ -const utils = sync.require("../../matrix/utils") - -/* - 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. -*/ +const {select} = passthrough const DEBUG_RETRIGGER = false @@ -20,140 +12,81 @@ function debugRetrigger(message) { } } -const storage = new class { - /** @private @type {Set} */ - paused = new Set() - /** @private @type {Map any)[]>} id -> list of resolvers */ - resolves = new Map() - /** @private @type {Map>} id -> timer */ - timers = new Map() +const paused = new Set() +const emitter = new EventEmitter() - /** - * The purpose of storage is to store `resolve` and call it at a later time. - * @param {string} id - * @param {(found: Boolean) => any} resolve - */ - store(id, resolve) { - debugRetrigger(`[retrigger] STORE id = ${id}`) - this.resolves.set(id, (this.resolves.get(id) || []).concat(resolve)) // add to list in map value - if (!this.timers.has(id)) { - debugRetrigger(`[retrigger] SET TIMER id = ${id}`) - this.timers.set(id, setTimeout(() => this.resolve(id, false), 60 * 1000).unref()) // 1 minute - } - } - - /** @param {string} id */ - isNotPaused(id) { - return !storage.paused.has(id) - } - - /** @param {string} id */ - pause(id) { - debugRetrigger(`[retrigger] PAUSE id = ${id}`) - this.paused.add(id) - } - - /** - * Go through `resolves` storage and resolve them all. (Also resets timer/paused.) - * @param {string} id - * @param {boolean} value - */ - resolve(id, value) { - if (this.paused.has(id)) { - debugRetrigger(`[retrigger] RESUME id = ${id}`) - this.paused.delete(id) - } - - if (this.resolves.has(id)) { - debugRetrigger(`[retrigger] RESOLVE ${value} id = ${id}`) - const fns = this.resolves.get(id) || [] - this.resolves.delete(id) - for (const fn of fns) { - fn(value) +/** + * 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[]) => any} T + * @param {string} inputID + * @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(inputID, fn, ...rest) { + if (!paused.has(inputID)) { + if (inputID.match(/^[0-9]+$/)) { + const eventID = select("event_message", "event_id", {message_id: inputID}).pluck().get() + if (eventID) { + debugRetrigger(`[retrigger] OK mid <-> eid = ${inputID} <-> ${eventID}`) + return false // event was found so don't retrigger + } + } else if (inputID.match(/^\$/)) { + const messageID = select("event_message", "message_id", {event_id: inputID}).pluck().get() + if (messageID) { + debugRetrigger(`[retrigger] OK eid <-> mid = ${inputID} <-> ${messageID}`) + return false // message was found so don't retrigger } } + } - if (this.timers.has(id)) { - clearTimeout(this.timers.get(id)) - this.timers.delete(id) + debugRetrigger(`[retrigger] WAIT id = ${inputID}`) + emitter.once(inputID, () => { + debugRetrigger(`[retrigger] TRIGGER id = ${inputID}`) + fn(...rest) + }) + // if the event never arrives, don't trigger the callback, just clean up + setTimeout(() => { + if (emitter.listeners(inputID).length) { + debugRetrigger(`[retrigger] EXPIRE id = ${inputID}`) } - } -} - -/** - * @param {string} id - * @param {(found: Boolean) => any} resolve - * @param {boolean} existsInDatabase - */ -function waitFor(id, resolve, existsInDatabase) { - if (existsInDatabase && storage.isNotPaused(id)) { // if event already exists and isn't paused then resolve immediately - debugRetrigger(`[retrigger] EXISTS id = ${id}`) - return resolve(true) - } - - // doesn't exist. wait for it to exist. storage will resolve true if it exists or false if it timed out - return storage.store(id, resolve) -} - -const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw() -/** - * @param {string} eventID - * @returns {Promise} if true then the message did not arrive - */ -function waitForEvent(eventID) { - const {promise, resolve} = Promise.withResolvers() - waitFor(eventID, resolve, !!GET_EVENT_PREPARED.get(eventID)) - return promise -} - -const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw() -/** - * @param {string} messageID - * @returns {Promise} if true then the message did not arrive - */ -function waitForMessage(messageID) { - const {promise, resolve} = Promise.withResolvers() - waitFor(messageID, resolve, !!GET_MESSAGE_PREPARED.get(messageID)) - return promise -} - -const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw() -/** - * @param {string} eventID - * @returns {Promise} if true then the message did not arrive - */ -function waitForReactionEvent(eventID) { - const {promise, resolve} = Promise.withResolvers() - waitFor(eventID, resolve, !!GET_REACTION_EVENT_PREPARED.get(utils.getEventIDHash(eventID))) - return promise + emitter.removeAllListeners(inputID) + }, 60 * 1000) // 1 minute + 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} id + * @param {string} messageID * @param {Promise} promise * @returns {Promise} */ -async function pauseChanges(id, promise) { +async function pauseChanges(messageID, promise) { try { - storage.pause(id) + debugRetrigger(`[retrigger] PAUSE id = ${messageID}`) + paused.add(messageID) return await promise } finally { - finishedBridging(id) + debugRetrigger(`[retrigger] RESUME id = ${messageID}`) + paused.delete(messageID) + messageFinishedBridging(messageID) } } /** * Triggers any pending operations that were waiting on the corresponding event ID. - * @param {string} id + * @param {string} messageID */ -function finishedBridging(id) { - storage.resolve(id, true) +function messageFinishedBridging(messageID) { + if (emitter.listeners(messageID).length) { + debugRetrigger(`[retrigger] EMIT id = ${messageID}`) + } + emitter.emit(messageID) } -module.exports.waitForMessage = waitForMessage -module.exports.waitForEvent = waitForEvent -module.exports.waitForReactionEvent = waitForReactionEvent +module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger +module.exports.messageFinishedBridging = messageFinishedBridging module.exports.pauseChanges = pauseChanges -module.exports.finishedBridging = finishedBridging \ No newline at end of file diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 7229d3d..3f598f2 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -109,7 +109,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ /** * @param {{room?: boolean, user_ids?: string[]}} mentions - * @param {Omit} attachment + * @param {Omit} attachment * @param {boolean} [alwaysLink] */ async function attachmentToEvent(mentions, attachment, alwaysLink) { @@ -256,8 +256,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) { const username = interaction.member?.nick || interaction.user.global_name || interaction.user.username const thinkingText = isThinkingInteraction ? " — interaction loading..." : "" return { - body: `❭ ${username} used \`/${interaction.name}\`${thinkingText}`, - html: `
❭ ${mxid ? tag`${username}` : username} used /${interaction.name}${thinkingText}
` + body: `↪️ ${username} used \`/${interaction.name}\`${thinkingText}`, + html: `
↪️ ${mxid ? tag`${username}` : username} used /${interaction.name}${thinkingText}
` } } @@ -357,17 +357,6 @@ async function messageToEvent(message, guild, options = {}, di) { }] } - if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) { - return [{ - $type: "m.room.message", - msgtype: "m.emote", - body: `set this room to receive announcements from ${message.content}`, - format: "org.matrix.custom.html", - formatted_body: tag`set this room to receive announcements from ${message.content}`, - "m.mentions": {} - }] - } - let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) @@ -640,8 +629,8 @@ async function messageToEvent(message, guild, options = {}, di) { const flags = message.flags || 0 if (flags & DiscordTypes.MessageFlags.IsCrosspost) { - body = `[↷ ${message.author.username}]\n` + body - html = `↷ ${message.author.username}
` + html + body = `[🔀 ${message.author.username}]\n` + body + html = `🔀 ${message.author.username}
` + html } // Fallback body/formatted_body for replies @@ -669,7 +658,7 @@ async function messageToEvent(message, guild, options = {}, di) { const match = repliedToEventSenderMxid.match(/^@([^:]*)/) assert(match) repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever - repliedToUserHtml = tag`${repliedToDisplayName}` + repliedToUserHtml = `${repliedToDisplayName}` } else { repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user" repliedToUserHtml = repliedToDisplayName @@ -694,12 +683,6 @@ async function messageToEvent(message, guild, options = {}, di) { + html body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions + "\n\n" + body - } else if (referenced.type === DiscordTypes.MessageType.UserJoin) { - // Discord user join messages are bridged as joins, not text events. Generate substitute text for reply. - const joinerMxid = select("sim", "mxid", {user_id: referenced.author.id}).pluck().get() - const joinerHtml = joinerMxid ? tag`${repliedToDisplayName}` : tag`${repliedToDisplayName}` - html = `
${joinerHtml} joined the room
` + html - body = `> ${repliedToDisplayName} joined the room\n\n` + body } else { // repliedToUnknownEvent const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp) html = `
In reply to ${dateDisplay} from ${repliedToDisplayName}:` @@ -768,20 +751,20 @@ async function messageToEvent(message, guild, options = {}, di) { if (row && "event_id" in row) { const via = await getViaServersMemo(row.room_id) forwardedNotice.addLine( - `[↷ Forwarded from #${roomName}]`, - tag`↷ Forwarded from ${roomName} [jump to event]` + `[🔀 Forwarded from #${roomName}]`, + tag`🔀 Forwarded from ${roomName} [jump to event]` ) } else { const via = await getViaServersMemo(room.room_id) forwardedNotice.addLine( - `[↷ Forwarded from #${roomName}]`, - tag`↷ Forwarded from ${roomName} [jump to room]` + `[🔀 Forwarded from #${roomName}]`, + tag`🔀 Forwarded from ${roomName} [jump to room]` ) } } else { forwardedNotice.addLine( - `[↷ Forwarded message]`, - tag`↷ Forwarded message` + `[🔀 Forwarded message]`, + tag`🔀 Forwarded message` ) } @@ -1127,7 +1110,7 @@ async function messageToEvent(message, guild, options = {}, di) { } } else { let body = stickerItem.name - const sticker = guild.stickers?.find(sticker => sticker.id === stickerItem.id) + const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id) if (sticker && sticker.description) body += ` - ${sticker.description}` return { $type: "m.sticker", diff --git a/src/d2m/converters/message-to-event.test.embeds.js b/src/d2m/converters/message-to-event.test.embeds.js index 91bbe2b..fdb0807 100644 --- a/src/d2m/converters/message-to-event.test.embeds.js +++ b/src/d2m/converters/message-to-event.test.embeds.js @@ -8,9 +8,9 @@ test("message2event embeds: interaction loading", async t => { const events = await messageToEvent(data.interaction_message.thinking_interaction, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", - body: "❭ Brad used `/stats` — interaction loading...", + body: "↪️ Brad used `/stats` — interaction loading...", format: "org.matrix.custom.html", - formatted_body: "
Brad used /stats — interaction loading...
", + formatted_body: "
↪️ Brad used /stats — interaction loading...
", "m.mentions": {}, msgtype: "m.notice", }]) @@ -22,12 +22,12 @@ test("message2event embeds: nothing but a field", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.notice", - body: "❭ PapiOphidian used `/stats`" + body: "↪️ PapiOphidian used `/stats`" + "\n| ### Amanda 🎵#2192 :online:" + "\n| willow tree, branch 0" + "\n| **❯ Uptime:**\n| 3m 55s\n| **❯ Memory:**\n| 64.45MB", format: "org.matrix.custom.html", - formatted_body: '
PapiOphidian used /stats
' + formatted_body: '
↪️ PapiOphidian used /stats
' + '

Amanda 🎵#2192 \":online:\"' + '
willow tree, branch 0
' + '
❯ Uptime:
3m 55s' @@ -153,10 +153,10 @@ test("message2event embeds: title without url", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", - body: "❭ PapiOphidian used `/stats`" + body: "↪️ PapiOphidian used `/stats`" + "\n| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", format: "org.matrix.custom.html", - formatted_body: '

PapiOphidian used /stats
' + formatted_body: '
↪️ PapiOphidian used /stats
' + `

Hi, I'm Amanda!

I condone pirating music!

`, "m.mentions": {} }]) @@ -167,10 +167,10 @@ test("message2event embeds: url without title", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", - body: "❭ PapiOphidian used `/stats`" + body: "↪️ PapiOphidian used `/stats`" + "\n| I condone pirating music!", format: "org.matrix.custom.html", - formatted_body: '
PapiOphidian used /stats
' + formatted_body: '
↪️ PapiOphidian used /stats
' + `

I condone pirating music!

`, "m.mentions": {} }]) @@ -181,10 +181,10 @@ test("message2event embeds: author without url", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", - body: "❭ PapiOphidian used `/stats`" + body: "↪️ PapiOphidian used `/stats`" + "\n| ## Amanda\n| \n| I condone pirating music!", format: "org.matrix.custom.html", - formatted_body: '
PapiOphidian used /stats
' + formatted_body: '
↪️ PapiOphidian used /stats
' + `

Amanda

I condone pirating music!

`, "m.mentions": {} }]) @@ -195,10 +195,10 @@ test("message2event embeds: author url without name", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", - body: "❭ PapiOphidian used `/stats`" + body: "↪️ PapiOphidian used `/stats`" + "\n| I condone pirating music!", format: "org.matrix.custom.html", - formatted_body: '
PapiOphidian used /stats
' + formatted_body: '
↪️ PapiOphidian used /stats
' + `

I condone pirating music!

`, "m.mentions": {} }]) @@ -209,9 +209,9 @@ test("message2event embeds: 4 images", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", - body: "[↷ Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046", + body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046", format: "org.matrix.custom.html", - formatted_body: "↷ Forwarded message
https://fixupx.com/i/status/2032003668787020046
", + formatted_body: "🔀 Forwarded message
https://fixupx.com/i/status/2032003668787020046
", "m.mentions": {} }, { $type: "m.room.message", diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index be1d99f..c4b812d 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -4,7 +4,6 @@ const {MatrixServerError} = require("../../matrix/mreq") const data = require("../../../test/data") const {mockGetEffectivePower} = require("../../matrix/utils.test") const Ty = require("../../types") -const {db} = require("../../passthrough") /** * @param {string} roomID @@ -734,31 +733,6 @@ test("message2event: reply to a Discord message that wasn't bridged", async t => }]) }) -test("message2event: reply to a Discord member join (who didn't join on Matrix)", async t => { - const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party", - format: "org.matrix.custom.html", - formatted_body: "
PEASANT!! joined the room
when the broke friend who we pay to bring food shows up at the medieval lord party", - "m.mentions": {} - }]) -}) - -test("message2event: reply to a Discord member join (who did join on Matrix)", async t => { - db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1461677775554478161', 'peasant321_76775', 'peasant321_76775', '@_ooye_peasant321_76775:cadence.moe')").run() - const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party", - format: "org.matrix.custom.html", - formatted_body: `
PEASANT!! joined the room
when the broke friend who we pay to bring food shows up at the medieval lord party`, - "m.mentions": {} - }]) -}) - 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: { @@ -1168,19 +1142,6 @@ test("message2event: type 4 channel name change", async t => { }]) }) -test("message2event: type 12 channel follow add", async t => { - const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - "m.mentions": {}, - msgtype: "m.emote", - body: "set this room to receive announcements from PluralKit #downtime", - format: "org.matrix.custom.html", - formatted_body: "set this room to receive announcements from PluralKit #downtime", - "m.mentions": {} - }]) -}) - test("message2event: thread start message reference", async t => { const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { api: { @@ -1273,9 +1234,9 @@ test("message2event: crossposted announcements say where they are crossposted fr $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "[↷ Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands", + body: "[🔀 Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands", format: "org.matrix.custom.html", - 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" + 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" }]) }) @@ -1344,9 +1305,9 @@ test("message2event: forwarded image", async t => { t.deepEqual(events, [ { $type: "m.room.message", - body: "[↷ Forwarded message]", + body: "[🔀 Forwarded message]", format: "org.matrix.custom.html", - formatted_body: "↷ Forwarded message", + formatted_body: "🔀 Forwarded message", "m.mentions": {}, msgtype: "m.notice", }, @@ -1385,10 +1346,10 @@ test("message2event: constructed forwarded message", async t => { t.deepEqual(events, [ { $type: "m.room.message", - body: "[↷ Forwarded from #wonderland]" + body: "[🔀 Forwarded from #wonderland]" + "\n» What's cooking, good looking? :hipposcope:", format: "org.matrix.custom.html", - formatted_body: `↷ Forwarded from wonderland [jump to event]` + formatted_body: `🔀 Forwarded from wonderland [jump to event]` + `
What's cooking, good looking? :hipposcope:
`, "m.mentions": {}, msgtype: "m.text", @@ -1444,10 +1405,10 @@ test("message2event: constructed forwarded text", async t => { t.deepEqual(events, [ { $type: "m.room.message", - body: "[↷ Forwarded from #amanda-spam]" + body: "[🔀 Forwarded from #amanda-spam]" + "\n» What's cooking, good looking?", format: "org.matrix.custom.html", - formatted_body: `↷ Forwarded from amanda-spam [jump to room]` + formatted_body: `🔀 Forwarded from amanda-spam [jump to room]` + `
What's cooking, good looking?
`, "m.mentions": {}, msgtype: "m.text", @@ -1467,10 +1428,10 @@ test("message2event: don't scan forwarded messages for mentions", async t => { t.deepEqual(events, [ { $type: "m.room.message", - body: "[↷ Forwarded message]" + body: "[🔀 Forwarded message]" + "\n» If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114", format: "org.matrix.custom.html", - formatted_body: `↷ Forwarded message` + formatted_body: `🔀 Forwarded message` + `
If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114
`, "m.mentions": {}, msgtype: "m.text" @@ -1820,9 +1781,9 @@ test("message2event: forwarded message with unreferenced mention", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", - body: "[↷ Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", + body: "[🔀 Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", format: "org.matrix.custom.html", - formatted_body: "↷ Forwarded message
@unknown-user:
🎞️ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
", + formatted_body: "🔀 Forwarded message
@unknown-user:
🎞️ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
", "m.mentions": {} }]) }) diff --git a/src/d2m/converters/pins-to-list.js b/src/d2m/converters/pins-to-list.js index 4ad8800..5a33c7c 100644 --- a/src/d2m/converters/pins-to-list.js +++ b/src/d2m/converters/pins-to-list.js @@ -22,7 +22,7 @@ function pinsToList(pins, kstate) { /** @type {string[]} */ const result = [] for (const pin of pins.items) { - const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get() + const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get() if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID) } result.reverse() diff --git a/src/d2m/converters/remove-reaction.js b/src/d2m/converters/remove-reaction.js index b6b0407..4ca22b6 100644 --- a/src/d2m/converters/remove-reaction.js +++ b/src/d2m/converters/remove-reaction.js @@ -34,7 +34,7 @@ function removeReaction(data, reactions, key) { // Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have // reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user. // Also need to clean up the database. - const hash = utils.getEventIDHash(eventID) + const hash = utils.getEventIDHash(event.event_id) removals.push({eventID, mxid: null, hash}) } if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) { diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 8101a03..c86cc13 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -2,7 +2,6 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") -const {id: botID} = require("../../addbot") const {sync, db, select, from} = require("../passthrough") /** @type {import("./actions/send-message")}) */ @@ -39,8 +38,6 @@ const removeMember = sync.require("./actions/remove-member") const vote = sync.require("./actions/poll-vote") /** @type {import("../m2d/event-dispatcher")} */ const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") -/** @type {import("../m2d/actions/redact.js")} */ -const redact = sync.require("../m2d/actions/redact.js") /** @type {import("../discord/interactions/matrix-info")} */ const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info") @@ -105,7 +102,7 @@ module.exports = { // 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`) let messages try { - messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 100}) + messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50}) } catch (e) { if (e.message === `{"message": "Missing Access", "code": 50001}`) { // pathetic error handling from SnowTransfer console.log(`[check missed messages] no permissions to look back in channel ${channel.name} (${channel.id})`) @@ -319,7 +316,7 @@ module.exports = { // @ts-ignore await sendMessage.sendMessage(message, channel, guild, row) - retrigger.finishedBridging(message.id) + retrigger.messageFinishedBridging(message.id) }, /** @@ -340,7 +337,7 @@ module.exports = { if (!row) { // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (!await retrigger.waitForMessage(data.id)) return + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return } /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ @@ -378,16 +375,6 @@ module.exports = { * @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data */ async onSomeReactionsRemoved(client, data) { - // Don't attempt to double-bridge our own m2d deleted reactions back to Matrix - if ("user_id" in data && data.user_id === botID) { - const emojiIdOrName = data.emoji.id || data.emoji.name - const i = redact.m2dDeletedReactions.findIndex(x => data.message_id === x.messageID && emojiIdOrName === x.emojiIdOrName) - if (i !== -1) { - redact.m2dDeletedReactions.splice(i, 1) - return - } - } - await removeReaction.removeSomeReactions(data) }, @@ -397,7 +384,7 @@ module.exports = { */ async MESSAGE_DELETE(client, data) { speedbump.onMessageDelete(data.id) - if (!await retrigger.waitForMessage(data.id)) return + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_DELETE, client, data)) return await deleteMessage.deleteMessage(data) }, @@ -445,12 +432,12 @@ module.exports = { * @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data */ async MESSAGE_POLL_VOTE_ADD(client, data) { - if (!await retrigger.waitForMessage(data.message_id)) return + if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_ADD, client, data)) return await vote.addVote(data) }, async MESSAGE_POLL_VOTE_REMOVE(client, data) { - if (!await retrigger.waitForMessage(data.message_id)) return + if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_REMOVE, client, data)) return await vote.removeVote(data) }, diff --git a/src/db/migrations/0037-remove-leaked-webhooks.js b/src/db/migrations/0037-remove-leaked-webhooks.js deleted file mode 100644 index 0228053..0000000 --- a/src/db/migrations/0037-remove-leaked-webhooks.js +++ /dev/null @@ -1,42 +0,0 @@ -const {discord, db, from, select, sync} = require("../../passthrough") -/** @type {import("../../discord/utils")} */ -const dUtils = sync.require("../../discord/utils") - -const ones = "₀₁₂₃₄₅₆₇₈₉" -const tens = "0123456789" - -/* c8 ignore start */ - -module.exports = async function(db) { - // added tolerance to https://discordstatus.com/incidents/4hpm4454hxtx - const OUTAGE_START = 1778263200000 - const OUTAGE_END = 1778284800000 - - const startSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_START) - const endSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_END) - - const affectedChannels = from("message_room").join("historical_channel_room", "historical_room_index") - .pluck("reference_channel_id").selectUnsafe("DISTINCT reference_channel_id") - .and("WHERE message_id >= ? AND message_id <= ? AND length(message_id) = ?").all(startSnowflake, endSnowflake, startSnowflake.length) - let affectedWebhooks = select("webhook", ["channel_id", "webhook_id", "webhook_token"], {channel_id: affectedChannels}).all() - affectedWebhooks = affectedWebhooks.filter(w => BigInt(w.webhook_id) < BigInt(endSnowflake)) // if webhook ID is already newly generated then no need to replace - - if (affectedWebhooks.length) { - process.stdout.write(` revoking ${affectedWebhooks.length} possibly compromised webhooks... `) - for (let counter = 1; counter <= affectedWebhooks.length; counter++) { - const webhook = affectedWebhooks[counter-1] - - await discord.snow.webhook.deleteWebhookToken(webhook.webhook_id, webhook.webhook_token, "Webhook token possibly compromised during 8th May 2026 outage").catch(e => { - if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) { - // OK - } else { - throw e - } - }) - db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(webhook.channel_id) - - process.stdout.write(String(counter).at(-1) === "0" ? tens[(counter/10)%10] : ones[counter%10]) - } - process.stdout.write("\n") - } -} diff --git a/src/db/migrations/0038-fix-emoji-file-format.sql b/src/db/migrations/0038-fix-emoji-file-format.sql deleted file mode 100644 index 9e63150..0000000 --- a/src/db/migrations/0038-fix-emoji-file-format.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN TRANSACTION; - -DELETE FROM emoji WHERE mxc_url NOT IN (SELECT mxc_url FROM file WHERE discord_url LIKE 'https://cdn.discordapp.com/emojis/%.webp%'); - -COMMIT; diff --git a/src/db/orm.js b/src/db/orm.js index 8763314..4d9b6f1 100644 --- a/src/db/orm.js +++ b/src/db/orm.js @@ -104,16 +104,6 @@ class From { return r } - pluckUnsafe(col) { - /** @type {Pluck} */ - // @ts-ignore - const r = this - r.cols = [col] - r.makeColsSafe = false - r.isPluck = true - return r - } - /** * @param {string} sql */ diff --git a/src/db/orm.test.js b/src/db/orm.test.js index 4639090..6f6018e 100644 --- a/src/db/orm.test.js +++ b/src/db/orm.test.js @@ -68,8 +68,3 @@ test("orm: select unsafe works (to select complex column names that can't be typ .all() t.equal(results[0].power_level, 150) }) - -test("orm: pluck unsafe works (to select complex column names that can't be type verified)", t => { - const result = from("channel_room").where({guild_id: "112760669178241024"}).pluckUnsafe("count(*)").get() - t.equal(result, 7) -}) diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index dcc9943..0b9a525 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -54,7 +54,6 @@ async function _interact({guild_id, data}, {api}) { // from Matrix const event = await api.getEvent(message.room_id, message.event_id) const via = await utils.getViaServersQuery(message.room_id, api) - const channelsInGuild = discord.guildChannelMap.get(guild_id) assert(channelsInGuild) const inChannels = channelsInGuild @@ -62,11 +61,6 @@ async function _interact({guild_id, data}, {api}) { .map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid)) .sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels)) .filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get()) - let inChannelsText = inChannels.map(c => `<#${c.id}>`).join(" • ") - if (inChannelsText.length > 1024) { - inChannelsText = `In ${inChannels.length} channels` - } - const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() let name = matrixMember?.displayname || event.sender let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url) @@ -104,7 +98,7 @@ async function _interact({guild_id, data}, {api}) { color: 0x0dbd8b, fields: [{ name: "In Channels", - value: inChannelsText + value: inChannels.map(c => `<#${c.id}>`).join(" • ") }, { name: "\u200b", value: idInfo diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index 66012b4..e3d58c4 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -91,32 +91,40 @@ function registerInteractions() { async function dispatchInteraction(interaction) { const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"] try { - if (interactionId === "Matrix info") { - await matrixInfo.interact(interaction) - } else if (interactionId === "invite") { - await invite.interact(interaction) - } else if (interactionId === "invite_channel") { - await invite.interactButton(interaction) - } else if (interactionId === "Permissions") { - await permissions.interact(interaction) - } else if (interactionId === "permissions_edit") { - await permissions.interactEdit(interaction) - } else if (interactionId === "Responses") { - /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore - const messageInteraction = interaction - if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) { - await pollResponses.interact(messageInteraction) + if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) { + // All we get is custom_id, don't know which context the button was clicked in. + // So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_. + if (interaction.data.custom_id.startsWith("POLL_")) { + await poll.interact(interaction) } else { - await reactions.interact(messageInteraction) + throw new Error(`Unknown message component ${interaction.data.custom_id}`) } - } else if (interactionId === "ping") { - await ping.interact(interaction) - } else if (interactionId === "privacy") { - await privacy.interact(interaction) - } else if (interactionId.startsWith("POLL_")) { - await poll.interact(interaction) } else { - throw new Error(`Unknown interaction ${interactionId}`) + if (interactionId === "Matrix info") { + await matrixInfo.interact(interaction) + } else if (interactionId === "invite") { + await invite.interact(interaction) + } else if (interactionId === "invite_channel") { + await invite.interactButton(interaction) + } else if (interactionId === "Permissions") { + await permissions.interact(interaction) + } else if (interactionId === "permissions_edit") { + await permissions.interactEdit(interaction) + } else if (interactionId === "Responses") { + /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore + const messageInteraction = interaction + if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) { + await pollResponses.interact(messageInteraction) + } else { + await reactions.interact(messageInteraction) + } + } else if (interactionId === "ping") { + await ping.interact(interaction) + } else if (interactionId === "privacy") { + await privacy.interact(interaction) + } else { + throw new Error(`Unknown interaction ${interactionId}`) + } } } catch (e) { let stackLines = null diff --git a/src/discord/utils.js b/src/discord/utils.js index 0d400f1..aed7068 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -182,394 +182,6 @@ function filterTo(xs, fn) { return filtered } -const supportedPlaintextPreviewExtensions = new Set([ - "4d", - "abnf", - "accesslog", - "actionscript", - "ada", - "adoc", - "alan", - "angelscript", - "ansi", - "apache", - "apacheconf", - "applescript", - "arcade", - "arduino", - "arm", - "armasm", - "as", - "asc", - "asciidoc", - "aspectj", - "ass", - "atom", - "autohotkey", - "autoit", - "avrasm", - "awk", - "axapta", - "bash", - "basic", - "bat", - "bbcode", - "bf", - "bind", - "blade", - "bnf", - "brainfuck", - "c", - "c++", - "cal", - "capnp", - "capnproto", - "cc", - "chaos", - "chapel", - "chpl", - "cisco", - "clj", - "clojure", - "cls", - "cmake.in", - "cmake", - "cmd", - "coffee", - "coffeescript", - "console", - "coq", - "cos", - "cpc", - "cpp", - "cr", - "craftcms", - "crm", - "crmsh", - "crystal", - "cs", - "csharp", - "cshtml", - "cson", - "csp", - "css", - "csv", - "cxx", - "cypher", - "d", - "dart", - "delphi", - "dfm", - "diff", - "django", - "dns", - "docker", - "dockerfile", - "dos", - "dpr", - "dsconfig", - "dst", - "dts", - "dust", - "dylan", - "ebnf", - "elixir", - "elm", - "erl", - "erlang", - "ex", - "extempore", - "f90", - "f95", - "fix", - "fortran", - "freepascal", - "fs", - "fsharp", - "gams", - "gauss", - "gawk", - "gcode", - "gdscript", - "gemspec", - "gf", - "gherkin", - "glsl", - "gms", - "gn", - "gni", - "go", - "godot", - "golang", - "golo", - "gololang", - "gradle", - "graph", - "groovy", - "gss", - "gyp", - "h", - "h++", - "haml", - "handlebars", - "haskell", - "haxe", - "hbs", - "hcl", - "hh", - "hpp", - "hs", - "html.handlebars", - "html.hbs", - "html", - "http", - "https", - "hx", - "hxx", - "hy", - "hylang", - "i", - "i7", - "iced", - "iecst", - "inform7", - "ini", - "ino", - "instances", - "iol", - "irb", - "irpf90", - "java", - "javascript", - "jinja", - "jolie", - "js", - "json", - "jsp", - "jsx", - "julia-repl", - "julia", - "k", - "kaos", - "kdb", - "kotlin", - "kt", - "lasso", - "lassoscript", - "lazarus", - "ldif", - "leaf", - "lean", - "less", - "lfm", - "lisp", - "livecodeserver", - "livescript", - "ln", - "lock", - "log", - "lpr", - "ls", - "ls", - "lua", - "mak", - "make", - "makefile", - "markdown", - "mathematica", - "matlab", - "mawk", - "maxima", - "md", - "mel", - "mercury", - "mirc", - "mizar", - "mk", - "mkd", - "mkdown", - "ml", - "ml", - "mm", - "mma", - "mojolicious", - "monkey", - "moon", - "moonscript", - "mrc", - "n1ql", - "nawk", - "nc", - "never", - "nginx", - "nginxconf", - "nim", - "nimrod", - "nix", - "nsis", - "obj-c", - "obj-c++", - "objc", - "objective-c++", - "objectivec", - "ocaml", - "ocl", - "ol", - "openscad", - "osascript", - "oxygene", - "p21", - "parser3", - "pas", - "pascal", - "patch", - "pcmk", - "perl", - "pf.conf", - "pf", - "pgsql", - "php", - "php3", - "php4", - "php5", - "php6", - "php7", - "pl", - "plaintext", - "plist", - "pm", - "podspec", - "pony", - "postgres", - "postgresql", - "powershell", - "pp", - "processing", - "profile", - "prolog", - "properties", - "proto", - "protobuf", - "ps", - "ps1", - "puppet", - "py", - "pycon", - "python-repl", - "python", - "qml", - "r", - "razor-cshtml", - "razor", - "rb", - "re", - "reasonml", - "rebol", - "red-system", - "red", - "redbol", - "rf", - "rib", - "robot", - "rpm-spec", - "rpm-specfile", - "rpm", - "rs", - "rsl", - "rss", - "ruby", - "ruleslanguage", - "rust", - "sas", - "SAS", - "sc", - "scad", - "scala", - "scheme", - "sci", - "scilab", - "scl", - "scss", - "sh", - "shell", - "shexc", - "smali", - "smalltalk", - "sml", - "sol", - "solidity", - "spec", - "specfile", - "sql", - "srt", - "ssa", - "st", - "stan", - "stanfuncs", - "stata", - "step", - "stp", - "structured-text", - "styl", - "stylus", - "subunit", - "supercollider", - "svelte", - "svg", - "swift", - "tao", - "tap", - "tcl", - "terraform", - "tex", - "text", - "tf", - "thor", - "thrift", - "tk", - "toml", - "tp", - "ts", - "tsql", - "tsx", - "ttml", - "twig", - "txt", - "typescript", - "unicorn-rails-log", - "v", - "vala", - "vb", - "vba", - "vbnet", - "vbs", - "vbscript", - "verilog", - "vhdl", - "vim", - "vtt", - "wl", - "x++", - "x86asm", - "xhtml", - "xjb", - "xl", - "xml", - "xpath", - "xq", - "xquery", - "xsd", - "xsl", - "xtlang", - "xtm", - "yaml", - "yml", - "zep", - "zephir", - "zone", - "zsh" -]) - module.exports.getPermissions = getPermissions module.exports.getDefaultPermissions = getDefaultPermissions module.exports.hasPermission = hasPermission @@ -582,4 +194,3 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact module.exports.getPublicUrlForCdn = getPublicUrlForCdn module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage module.exports.filterTo = filterTo -module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions diff --git a/src/m2d/actions/add-reaction.js b/src/m2d/actions/add-reaction.js index c453244..e4981fb 100644 --- a/src/m2d/actions/add-reaction.js +++ b/src/m2d/actions/add-reaction.js @@ -17,7 +17,7 @@ const retrigger = sync.require("../../d2m/actions/retrigger") */ async function addReaction(event) { // Wait until the corresponding channel and message have already been bridged - if (!await retrigger.waitForEvent(event.content["m.relates_to"].event_id)) return + if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, () => as.emit("type:m.reaction", event))) return // These will exist because it passed retrigger const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") @@ -50,8 +50,6 @@ async function addReaction(event) { } db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji, original_encoding) VALUES (?, ?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding, key) - - retrigger.finishedBridging(event.event_id) } module.exports.addReaction = addReaction diff --git a/src/m2d/actions/redact.js b/src/m2d/actions/redact.js index 7e49753..3135d31 100644 --- a/src/m2d/actions/redact.js +++ b/src/m2d/actions/redact.js @@ -10,9 +10,6 @@ const utils = sync.require("../../matrix/utils") /** @type {import("../../d2m/actions/retrigger")} */ const retrigger = sync.require("../../d2m/actions/retrigger") -/** @type {{messageID: string, emojiIdOrName: string}[]} */ -const m2dDeletedReactions = [] - /** * @param {Ty.Event.Outer_M_Room_Redaction} event */ @@ -27,21 +24,6 @@ async function deleteMessage(event) { db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id) } -/** - * @param {Ty.Event.Outer_M_Room_Redaction} event - */ -async function removeMessageEvent(event) { - // Could be for removing a message or suppressing embeds. For more information, the message needs to be bridged first. - if (!await retrigger.waitForEvent(event.redacts)) return - - const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get() - if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) { - await suppressEmbeds(event) - } else { - await deleteMessage(event) - } -} - /** * @param {Ty.Event.Outer_M_Room_Redaction} event */ @@ -59,20 +41,11 @@ async function suppressEmbeds(event) { * @param {Ty.Event.Outer_M_Room_Redaction} event */ async function removeReaction(event) { - if (!await retrigger.waitForReactionEvent(event.redacts)) return - const hash = utils.getEventIDHash(event.redacts) const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") .select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get() if (!row) return - // See how many Matrix-side reactions there are, and delete if it's the last one - const numberOfReactions = from("reaction").where({message_id: row.message_id, encoded_emoji: row.encoded_emoji}).pluckUnsafe("count(*)").get() - if (numberOfReactions === 1) { - // If a unicode emoji, the name is already the Discord preferred version because that's what was added and stored to encoded_emoji - const emojiIdOrName = decodeURIComponent(row.encoded_emoji).split(":").slice(-1)[0] - m2dDeletedReactions.push({messageID: row.message_id, emojiIdOrName}) - await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji) - } + await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji) db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash) } @@ -81,12 +54,18 @@ async function removeReaction(event) { * @param {Ty.Event.Outer_M_Room_Redaction} event */ async function handle(event) { - // Don't know if it's a redaction for a reaction or an event, try both at the same time (otherwise waitFor will block) - await Promise.all([ - removeMessageEvent(event), - removeReaction(event) - ]) + // If this is for removing a reaction, try it + await removeReaction(event) + + // Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first. + if (retrigger.eventNotFoundThenRetrigger(event.redacts, () => as.emit("type:m.room.redaction", event))) return + + const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get() + if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) { + await suppressEmbeds(event) + } else { + await deleteMessage(event) + } } module.exports.handle = handle -module.exports.m2dDeletedReactions = m2dDeletedReactions \ No newline at end of file diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js index 1ff2bb9..d06f6e8 100644 --- a/src/m2d/actions/update-pins.js +++ b/src/m2d/actions/update-pins.js @@ -13,7 +13,7 @@ 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_room", "message_id").join("historical_channel_room", "historical_room_index") - .select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get() + .select("reference_channel_id", "message_id").get() if (!row) continue if (added) { discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 0a18a14..95e477f 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -29,8 +29,6 @@ const pollComponents = sync.require("./poll-components") const setupEmojis = sync.require("../actions/setup-emojis") /** @type {import("../../d2m/converters/user-to-mxid")} */ const userToMxid = sync.require("../../d2m/converters/user-to-mxid") -/** @type {import("../../web/routes/letter-avatar")} */ -const letterAvatar = sync.require("../../web/routes/letter-avatar") /** @type {[RegExp, string][]} */ const markdownEscapes = [ @@ -584,13 +582,6 @@ async function eventToMessage(event, guild, channel, di) { displayNameRunoff = "" } - // Avatar post-processing. Use a thumbnail for media, or generate letter avatar if none present. - if (avatarURL) { - avatarURL = avatarURL + "?preset=avatar" - } else { - avatarURL = letterAvatar.getLetterAvatarURL(event.sender, displayNameShortened) - } - let content = event.content["body"] || "" // ultimate fallback /** @type {{id: string, filename: string}[]} */ const attachments = [] @@ -825,12 +816,22 @@ async function eventToMessage(event, guild, channel, di) { if (shouldProcessTextEvent) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body + if (perMessageProfile?.has_fallback) { + // Strip fallback elements added for clients that don't support per-message profiles. + // Deviates from recommended regexp in MSC to be less strict. Avoiding an HTML parser for performance reasons. + // ┌────A────┐ Opening HTML tag: capture tag name and stay within tag + // ┆ ┆┌─────────────B────────────┐ This text in the tag somewhere, presumably an attribute name + // ┆ ┆┆ ┆┌─C──┐ Rest of the opening tag + // ┆ ┆┆ ┆┆ ┆┌─D─┐ Tag content (no more tags allowed within) + // ┆ ┆┆ ┆┆ ┆┆ ┆┌─E──┐ Closing tag matching opening tag name + input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") + } if (event.content.msgtype === "m.emote") { input = `* ${displayName} ${input}` } // Handling mentions of Discord users - input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")/g, (whole, attributeValue, mxid) => { + input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")>/g, (whole, attributeValue, mxid) => { mxid = decodeURIComponent(mxid) if (mxUtils.eventSenderIsFromDiscord(mxid)) { // Handle mention of an OOYE sim user by their mxid @@ -885,9 +886,8 @@ async function eventToMessage(event, guild, channel, di) { const doc = domino.createDocument( // DOM parsers arrange elements in the and . Wrapping in a custom element ensures elements are reliably arranged in a single element. '' + input + '' - ) - const root = doc.getElementById("turndown-root") - assert(root) + ); + const root = doc.getElementById("turndown-root"); async function forEachNode(event, node) { for (; node; node = node.nextSibling) { // Check written mentions @@ -901,12 +901,9 @@ async function eventToMessage(event, guild, channel, di) { } // Check for incompatible backticks in code blocks let preNode - let isBackticksTextInPre = node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"])) - let isLongPre = node.tagName === "PRE" && node.textContent.length > 1800 && (preNode = node) - if (isBackticksTextInPre || isLongPre) { + if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) { if (preNode.firstChild?.nodeName === "CODE") { - let ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] - if (!dUtils.supportedPlaintextPreviewExtensions.has(ext)) ext = "txt" + const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt" const filename = `inline_code.${ext}` // Build the replacement node const replacementCode = doc.createElement("code") @@ -943,7 +940,6 @@ async function eventToMessage(event, guild, channel, di) { } } await forEachNode(event, root) - if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove()) // SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet. // First we need to determine which emojis are at the end. diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 650e442..bc73df7 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -61,7 +61,7 @@ test("event2message: body is used when there is no formatted_body", async t => { messagesToSend: [{ username: "cadence [they]", content: "testing plaintext", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -99,7 +99,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -134,7 +134,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -167,7 +167,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -195,7 +195,7 @@ test("event2message: links in plaintext body are not broken when preceded by a n 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -225,7 +225,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -264,7 +264,7 @@ test("event2message: markdown in link text does not attempt to be escaped becaus messagesToSend: [{ username: "cadence [they]", content: "hey [@mario sports mix [she/her]](), is it possible to listen on a unix socket?", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["roles"], users: [] @@ -293,7 +293,7 @@ test("event2message: markdown in link url does not attempt to be escaped (plaint messagesToSend: [{ username: "cadence [they]", content: "the wikimedia commons freaks are gonna love this one https://commons.wikimedia.org/wiki/File:Car_covered_in_traffic_cones.jpg", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -328,7 +328,7 @@ test("event2message: markdown in link url does not attempt to be escaped (plaint messagesToSend: [{ username: "cadence [they]", content: "the wikimedia commons freaks are gonna love this one ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -366,7 +366,7 @@ test("event2message: embeds are suppressed if the guild does not have embed link messagesToSend: [{ username: "cadence [they]", content: "posting one of my favourite songs recently (starts at timestamp) ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -402,7 +402,7 @@ test("event2message: embeds are suppressed if the guild does not have embed link messagesToSend: [{ username: "cadence [they]", content: "posting one of my favourite songs recently (starts at timestamp) ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -445,7 +445,7 @@ test("event2message: embeds are suppressed if the channel does not have embed li messagesToSend: [{ username: "cadence [they]", content: "posting one of my favourite songs recently (starts at timestamp) ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -476,7 +476,7 @@ test("event2message: links retain angle brackets (formatted body)", async t => { messagesToSend: [{ username: "cadence [they]", content: "posting one of my favourite songs recently (starts at timestamp) ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -505,7 +505,7 @@ test("event2message: links retain angle brackets (plaintext body)", async t => { messagesToSend: [{ username: "cadence [they]", content: "posting one of my favourite songs recently (starts at timestamp) ", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -546,7 +546,7 @@ test("event2message: links don't have angle brackets added by accident", async t messagesToSend: [{ username: "Erquint", content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=E&hue=180", + avatar_url: undefined, allowed_mentions: { parse: ["roles"], users: [] @@ -581,7 +581,7 @@ test("event2message: basic html is converted to markdown", async t => { messagesToSend: [{ username: "cadence [they]", content: "this **is** a _**test** __of___ ~~_formatting_~~", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -615,7 +615,7 @@ test("event2message: spoilers work", async t => { messagesToSend: [{ username: "cadence [they]", content: "this **is** a ||_test_|| of ||spoilers||", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -649,7 +649,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -689,7 +689,7 @@ test("event2message: media spoilers work", async t => { messagesToSend: [{ username: "underscore_x", content: "", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=U&hue=270", + avatar_url: undefined, attachments: [{id: "0", filename: "SPOILER_pitstop.png"}], pendingFiles: [{ mxc: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT", @@ -735,7 +735,7 @@ test("event2message: media spoilers with reason work", async t => { parse: ["users", "roles"] }, content: "(Spoiler: golden witch solutions)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=U&hue=270", + avatar_url: undefined, attachments: [{id: "0", filename: "SPOILER_pitstop.png"}], pendingFiles: [{ mxc: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT", @@ -781,7 +781,7 @@ test("event2message: spoiler files too large for Discord are linked and retain r parse: ["users", "roles"] }, content: "(Spoiler: golden witch secrets)\n🖼️ _Uploaded **SPOILER** file: ||[pitstop.png](https://bridge.example.org/download/matrix/agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT )|| (40 MB)_", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=U&hue=270" + avatar_url: undefined }] } ) @@ -812,7 +812,7 @@ test("event2message: markdown syntax is escaped", async t => { messagesToSend: [{ username: "cadence [they]", content: "this \\*\\*is\\*\\* an **_extreme_** \\\\\\*test\\\\\\* of", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -846,7 +846,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -880,7 +880,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -915,7 +915,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -951,7 +951,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -983,14 +983,14 @@ test("event2message: long messages are split", async t => { messagesToSend: [{ username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(15)).slice(0, -1), - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } }, { username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(4)).slice(0, -1), - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1024,7 +1024,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1054,7 +1054,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1084,7 +1084,7 @@ test("event2message: code blocks use double backtick as delimiter when necessary messagesToSend: [{ username: "cadence [they]", content: "``backtick in ` the middle``, `` backtick at the edge` ``", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1114,7 +1114,7 @@ test("event2message: inline code is converted to code block if it contains both messagesToSend: [{ username: "cadence [they]", content: "``` ` one two `` ```", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1146,39 +1146,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - -test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (use txt extension if discord does not recognise the language)", 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: "!kLRqKKUQXcibIMtOpl: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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1210,39 +1178,7 @@ test("event2message: code blocks are uploaded as attachments instead if they con 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - -test("event2message: code blocks are uploaded as attachments instead if they are really long", 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
${"A".repeat(2000)}
it should print a markdown formatted code block` - }, - event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "cadence [they]", - content: "So if you run code like this `[inline_code.js]` it should print a markdown formatted code block", - attachments: [{id: "0", filename: "inline_code.js"}], - pendingFiles: [{name: "inline_code.js", buffer: Buffer.from("A".repeat(2000))}], - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1285,7 +1221,7 @@ test("event2message: characters are encoded properly in code blocks", async t => + '\n .map(|c| c.get(1).unwrap().as_str())' + '\n .collect::();' )}], - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1319,7 +1255,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1360,7 +1296,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=M&hue=210", + avatar_url: undefined, allowed_mentions: { parse: ["roles"], users: [] @@ -1402,7 +1338,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=M&hue=210", + avatar_url: undefined, allowed_mentions: { parse: ["roles"], users: [] @@ -1435,7 +1371,7 @@ test("event2message: m.emote plaintext works", async t => { messagesToSend: [{ username: "cadence [they]", content: "\\* cadence \\[they\\] tests an m.emote message", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1469,7 +1405,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1522,7 +1458,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1578,7 +1514,7 @@ test("event2message: rich reply to a sim user, explicitly enabling mentions in c content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " Slow news day." + "\nTesting this reply, ignore", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["roles"], users: ["111604486476181504"] @@ -1633,7 +1569,7 @@ test("event2message: rich reply to a sim user, explicitly disabling mentions in content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + " Slow news day." + "\nTesting this reply, ignore", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["roles"], users: [] @@ -1706,7 +1642,7 @@ test("event2message: rich reply to a rich reply to a multi-line message should c 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -1786,7 +1722,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1839,7 +1775,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1888,7 +1824,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1939,7 +1875,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -1991,7 +1927,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2040,7 +1976,7 @@ test("event2message: should suppress embeds for links in reply preview", async t 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 "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=R&hue=240", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2129,7 +2065,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2213,7 +2149,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2266,7 +2202,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2316,7 +2252,7 @@ test("event2message: reply preview converts emoji formatting when replying to a content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + " <:hippo:230201364309868544>" + "\nreply", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2366,7 +2302,7 @@ test("event2message: reply preview can guess custom emoji based on the name if i content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + " <:hippo:230201364309868544>" + "\nreply", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2416,7 +2352,7 @@ test("event2message: reply preview uses emoji title text when replying to an unk content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + " :svkftngur_gkdne:" + "\nreply", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2466,7 +2402,7 @@ test("event2message: reply preview ignores garbage image", async t => { content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>**Ⓜcadence [they]**:" + " I am having a nice day" + "\nreply", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2515,7 +2451,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -2598,7 +2534,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2654,7 +2590,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2709,7 +2645,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2718,7 +2654,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2771,7 +2707,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2832,7 +2768,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2889,7 +2825,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -2951,7 +2887,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -3007,7 +2943,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -3062,7 +2998,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -3119,7 +3055,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://bridge.example.org/download/matrix/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP", allowed_mentions: { parse: ["users", "roles"] } @@ -3177,7 +3113,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://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", allowed_mentions: { parse: ["users", "roles"] } @@ -3223,7 +3159,7 @@ test("event2message: rich reply to a state event with no body", async t => { messagesToSend: [{ username: "Ampflower 🌺", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647> (channel details edited)\nnice room topic", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/PRfhXYBTOalvgQYtmCLeUXko", allowed_mentions: { parse: ["users", "roles"] } @@ -3311,7 +3247,7 @@ test("event2message: rich reply with an image", async t => { id: "0", }, ], - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, pendingFiles: [ { mxc: "mxc://cadence.moe/yxMobQMbSqNHpajxgSHtaooG", @@ -3348,7 +3284,7 @@ test("event2message: raw mentioning discord users in plaintext body works", asyn messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> what do you think?", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3382,7 +3318,7 @@ test("event2message: raw mentioning discord users in formatted body works", asyn messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> what do you think?", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3416,7 +3352,7 @@ test("event2message: mentioning discord users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just <@114147806469554185> testing mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3425,50 +3361,6 @@ test("event2message: mentioning discord users works", async t => { ) }) -test("event2message: mentioning discord users with extra html attributes works", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@lavender.pet:queer.sh", - content: { - msgtype: "m.text", - body: "also @_ooye_ampflower:cadence.moe fun fact at some point there is plans for FTE to have a built in map editor", - "m.mentions": { - user_ids: [ - "@_ooye_ampflower:cadence.moe" - ] - }, - format: "org.matrix.custom.html", - formatted_body: "

also @Ampflower fun fact at some point there is plans for FTE to have a built in map editor

" - }, - room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe", - origin_server_ts: 1778616745263, - unsigned: { - age: 100363692, - membership: "join" - }, - event_id: "$AHCkieLEVCrCEA3INTCl0rh44V29fCASlZpBKw7DzQU", - user_id: "@lavender.pet:queer.sh", - age: 100363692 - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "lavender.pet", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=L&hue=30", - content: "also <@196188877885538304> fun fact at some point there is plans for FTE to have a built in map editor", - allowed_mentions: { - parse: ["roles"], - users: ["196188877885538304"] - } - }] - } - ) -}) - - test("event2message: mentioning discord users works when URL encoded", async t => { t.deepEqual( await eventToMessage({ @@ -3494,7 +3386,7 @@ test("event2message: mentioning discord users works when URL encoded", async t = messagesToSend: [{ username: "cadence [they]", content: "<@771520384671416320> a sample message", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3528,7 +3420,7 @@ test("event2message: mentioning PK discord users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just **@Azalea &flwr; 🌺** (<@196188877885538304>) testing mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3562,7 +3454,7 @@ test("event2message: mentioning matrix users works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just [@▲]() testing mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3603,7 +3495,7 @@ test("event2message: matrix mentions are not double-escaped when embed links per messagesToSend: [{ username: "cadence [they]", content: "I'm just [@▲]() testing mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3637,7 +3529,7 @@ test("event2message: multiple mentions are both escaped", async t => { messagesToSend: [{ username: "cadence [they]", content: "[@cadence:cadence.moe]() can you kick my old account over there [@amyiscoolz:matrix.atiusamy.com]()", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3667,7 +3559,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3701,7 +3593,7 @@ test("event2message: mentioning bridged rooms works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3733,7 +3625,7 @@ test("event2message: mentioning bridged rooms works (plaintext body)", async t = messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3776,7 +3668,7 @@ test("event2message: mentioning bridged rooms by alias works", async t => { messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3818,7 +3710,7 @@ test("event2message: mentioning bridged rooms by alias works (plaintext body)", messagesToSend: [{ username: "cadence [they]", content: "I'm just <#1100319550446252084> testing channel mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3859,7 +3751,7 @@ test("event2message: mentioning bridged rooms by alias skips the link when alias messagesToSend: [{ username: "cadence [they]", content: "I'm just and <#1100319550446252084> testing channel mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3892,7 +3784,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3926,7 +3818,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3960,7 +3852,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -3994,7 +3886,7 @@ test("event2message: mentioning known bridged events followed by line break and messagesToSend: [{ username: "cadence [they]", content: "https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020<@114147806469554185>", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4040,7 +3932,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4085,7 +3977,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4129,7 +4021,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4159,7 +4051,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4189,7 +4081,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4217,7 +4109,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4251,7 +4143,7 @@ test("event2message: colon after mentions is stripped", async t => { messagesToSend: [{ username: "cadence [they]", content: "<@114147806469554185> hey, I'm just [@▲]() testing mentions", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4296,7 +4188,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?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", allowed_mentions: { parse: ["users", "roles"] } @@ -4304,7 +4196,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: "!qzDBLKlildpzrrOnFZ:cadence.moe", mxid: "@should_be_newly_cached:cadence.moe"}).all(), [ + 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") @@ -4346,7 +4238,7 @@ test("event2message: does not cache the member if the room is not known", async messagesToSend: [{ username: "should_not_be_cached", content: "testing the member state cache", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/this_is_the_avatar", allowed_mentions: { parse: ["users", "roles"] } @@ -4392,7 +4284,7 @@ test("event2message: skips caching the member if the member does not exist, some messagesToSend: [{ username: "not_real", content: "should honestly never happen", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=N&hue=180", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4439,7 +4331,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=I&hue=270", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4475,7 +4367,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=I&hue=270", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -4508,7 +4400,7 @@ test("event2message: text attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "chiki-powerups.txt"}], pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}] }] @@ -4544,7 +4436,7 @@ test("event2message: image attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "cool cat.png"}], pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] @@ -4580,7 +4472,7 @@ test("event2message: image attachments can have a plaintext caption", async t => messagesToSend: [{ username: "cadence [they]", content: "Cat emoji surrounded by pink hearts", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "cool cat.png"}], pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}], allowed_mentions: { @@ -4629,7 +4521,7 @@ test("event2message: image attachments can have a formatted caption", async t => messagesToSend: [{ username: "cadence [they]", content: "this event has `formatting`", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "5740.jpg"}], pendingFiles: [{name: "5740.jpg", mxc: "mxc://thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh"}], allowed_mentions: { @@ -4682,7 +4574,7 @@ test("event2message: encrypted image attachments work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "image.png"}], pendingFiles: [{ name: "image.png", @@ -4767,7 +4659,7 @@ test("event2message: evil encrypted image attachment works", async t => { messagesToSend: [{ username: "Austin Huang", content: "", - avatar_url: "https://bridge.example.org/download/matrix/tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e", attachments: [{id: "0", filename: "Screenshot 2025-06-29 at 13.36.46.png"}], pendingFiles: [{ name: "Screenshot 2025-06-29 at 13.36.46.png", @@ -4810,7 +4702,7 @@ test("event2message: large attachments are uploaded if the server boost level is messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", attachments: [{id: "0", filename: "cool cat.png"}], pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] }] @@ -4846,7 +4738,7 @@ test("event2message: files too large for Discord are linked as as URL", async t messagesToSend: [{ username: "cadence [they]", content: "🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -4883,7 +4775,7 @@ test("event2message: files too large for Discord can have a plaintext caption", messagesToSend: [{ username: "cadence [they]", content: "Cat emoji surrounded by pink hearts\n🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -4930,7 +4822,7 @@ test("event2message: files too large for Discord can have a formatted caption", messagesToSend: [{ username: "cadence [they]", content: "this event has `formatting`\n🖼️ _Uploaded file: [5740.jpg](https://bridge.example.org/download/matrix/thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh) (40 MB)_", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -4972,7 +4864,7 @@ test("event2message: stickers work", async t => { messagesToSend: [{ username: "cadence [they]", content: "[get_real2](https://bridge.example.org/download/sticker/cadence.moe/NyMXQFAAdniImbHzsygScbmN/_.webp)", - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -5000,7 +4892,7 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi messagesToSend: [{ username: "cadence [they]", content: "[YESYESYES](https://bridge.example.org/download/sticker/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf/_.webp)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5030,7 +4922,7 @@ test("event2message: static emojis work", async t => { messagesToSend: [{ username: "cadence [they]", content: "<:hippo:230201364309868544>", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5060,7 +4952,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://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", allowed_mentions: { parse: ["users", "roles"] } @@ -5090,7 +4982,7 @@ test("event2message: animated emojis work", async t => { messagesToSend: [{ username: "cadence [they]", content: "", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5120,7 +5012,7 @@ test("event2message: unknown emojis in the middle are linked", async t => { messagesToSend: [{ username: "cadence [they]", content: "a [:ms_robot_grin:](https://bridge.example.org/download/matrix/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy) b", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5169,7 +5061,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5222,7 +5114,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5265,7 +5157,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5295,7 +5187,7 @@ 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: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5352,7 +5244,7 @@ test("event2message: @room converts to @everyone and is allowed when the room do messagesToSend: [{ username: "cadence [they]", content: "@everyone dinner's ready", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles", "everyone"] } @@ -5409,7 +5301,7 @@ test("event2message: @room converts to @everyone but is not allowed when the roo messagesToSend: [{ username: "cadence [they]", content: "@room dinner's ready", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5468,7 +5360,7 @@ test("event2message: @room converts to @everyone and is allowed if the user has messagesToSend: [{ username: "cadence [they]", content: "@everyone dinner's ready", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles", "everyone"] } @@ -5498,7 +5390,7 @@ test("event2message: @room in the middle of a link is not converted", async t => messagesToSend: [{ username: "cadence [they]", content: "https://github.com/@room/repositories https://github.com/@room/repositories", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5534,7 +5426,7 @@ test("event2message: table", async t => { + "\nAardvark Bee Crocodile" + "\nArgon Boron Carbon ```" + "more content", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5564,7 +5456,7 @@ test("event2message: unknown emoji at the end is used for sprite sheet", async t messagesToSend: [{ username: "cadence [they]", content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5594,7 +5486,7 @@ test("event2message: known emoji from an unreachable server at the end is used f messagesToSend: [{ username: "cadence [they]", content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FbZFuuUSEebJYXUMSxuuSuLTa)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5624,7 +5516,7 @@ test("event2message: known and unknown emojis in the end are used for sprite she messagesToSend: [{ username: "cadence [they]", content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown: [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FWbYqNlACRuicynBfdnPYtmvc&e=cadence.moe%2FHYcztccFIPgevDvoaWNsEtGJ)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5658,7 +5550,7 @@ test("event2message: com.beeper.per_message_profile overrides displayname and av messagesToSend: [{ username: "Unstable Name", content: "hello from unstable profile", - avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", allowed_mentions: { parse: ["users", "roles"] } @@ -5691,7 +5583,7 @@ test("event2message: com.beeper.per_message_profile empty avatar_url clears avat messagesToSend: [{ username: "No Avatar User", content: "hello with cleared avatar", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=N&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5727,7 +5619,7 @@ test("event2message: data-mx-profile-fallback element is stripped from formatted messagesToSend: [{ username: "Tidus Herboren", content: "one more test", - avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo?preset=avatar", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", allowed_mentions: { parse: ["users", "roles"] } @@ -5760,7 +5652,7 @@ test("event2message: displayname prefix is stripped from plain body when per-mes messagesToSend: [{ username: "Tidus Herboren", content: "one more test", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=T&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } @@ -5789,7 +5681,7 @@ test("event2message: all unknown chess emojis are used for sprite sheet", async messagesToSend: [{ username: "cadence [they]", content: "testing [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj&e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj)", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", + avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] } diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 3580d1b..c11b696 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -94,11 +94,6 @@ function printError(type, source, e, payload) { console.dir(payload, {depth: null}) } -/** @param {string} stack */ -function cleanErrorStack(stack) { - return stack.replace(/(\/webhooks\/[0-9]+\/)[a-zA-Z0-9_-]+/g, "$1(redacted)") -} - /** * @param {string} roomID * @param {"Discord" | "Matrix"} source @@ -139,7 +134,7 @@ async function sendError(roomID, source, type, e, payload) { builder.addLine(errorIntroLine) // Where - const stack = cleanErrorStack(stringifyErrorStack(e)) + const stack = stringifyErrorStack(e) builder.addLine(`Error trace:\n${stack}`, tag`
Error trace
${stack}
`) // How @@ -148,7 +143,7 @@ async function sendError(roomID, source, type, e, payload) { // Send try { - const errorEventID = await api.sendEvent(roomID, "m.room.message", { + await api.sendEvent(roomID, "m.room.message", { ...builder.get(), "moe.cadence.ooye.error": { source: source.toLowerCase(), @@ -158,14 +153,6 @@ async function sendError(roomID, source, type, e, payload) { user_ids: ["@cadence:cadence.moe"] } }) - // Add reaction indicating that errors may be retried - await api.sendEvent(roomID, "m.reaction", { - "m.relates_to": { - rel_type: "m.annotation", - event_id: errorEventID, - key: "🔁" - } - }) } catch (e) {} } @@ -185,7 +172,6 @@ const errorRetrySema = new Semaphore() * @param {Ty.Event.Outer} reactionEvent */ async function onRetryReactionAdd(reactionEvent) { - if (reactionEvent.sender === `@${reg.sender_localpart}:${reg.ooye.server_name}`) return // Don't respond to the bot's own indicative reaction const roomID = reactionEvent.room_id await errorRetrySema.request(async () => { const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id) @@ -225,7 +211,7 @@ async event => { // @ts-ignore await matrixCommandHandler.execute(event) } - retrigger.finishedBridging(event.event_id) + retrigger.messageFinishedBridging(event.event_id) await api.ackEvent(event) })) @@ -236,7 +222,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker", async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) - retrigger.finishedBridging(event.event_id) + retrigger.messageFinishedBridging(event.event_id) await api.ackEvent(event) })) @@ -516,6 +502,5 @@ async event => { })) module.exports.stringifyErrorStack = stringifyErrorStack -module.exports.cleanErrorStack = cleanErrorStack module.exports.sendError = sendError module.exports.printError = printError diff --git a/src/m2d/event-dispatcher.test.js b/src/m2d/event-dispatcher.test.js index 2de6381..de754da 100644 --- a/src/m2d/event-dispatcher.test.js +++ b/src/m2d/event-dispatcher.test.js @@ -1,7 +1,7 @@ // @ts-check const {test} = require("supertape") -const {stringifyErrorStack, cleanErrorStack} = require("./event-dispatcher") +const {stringifyErrorStack} = require("./event-dispatcher") test("stringify error stack: works", t => { function a() { @@ -21,30 +21,3 @@ test("stringify error stack: works", t => { t.match(str, /^ \[prop\]: 2.1$/m) } }) - -test("clean error stack: removes webhook token", t => { - t.notMatch( - cleanErrorStack(` - DiscordAPIError: Service resource is being rate limited. - at fn (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:591:13) - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - at exports.RequestHandler.request (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:546:17) - at WebhookMethods.executeWebhook (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/methods/Webhook.ts:249:35) - at /var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:65:31 - at withWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:47:9) - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - at async Object.sendMessageWithWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:64:17) - at async Object.sendEvent (/var/home/cadence/out-of-your-element/src/m2d/actions/send-event.js:132:27) - at async /var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:208:27 - at async AppService. (/var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:162:11) { - [method]: "POST" - [path]: "/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG" - [code]: 40062 - [httpStatus]: 429 - [request]: {"endpoint":"/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG","method":"POST","dataType":"json","data":{"content":"https://discordstatus.com/#day\nOnly what discord tell us right now","allowed_mentions":{"parse":["roles"],"users":[]},"username":"lewri","avatar_url":"https://bridge.cadence.moe/download/matrix/matrix.org/URWwrtSUONGOYhfMsdUzcrir"}} - [response]: {} - [name]: "DiscordAPIError"` - ), - /pfRqHl9v/ - ) -}) diff --git a/src/matrix/api.js b/src/matrix/api.js index 9b7f280..f24f4d9 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -463,29 +463,17 @@ async function ping() { } /** - * Given an mxc:// URL, and optional parameters for thumbnailing, get the file from the content repository. Returns res. - * - * Note that Synapse currently doesn't support animated thumbnails: https://github.com/element-hq/synapse/pull/18831 - * @see https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid + * Given an mxc:// URL, and an optional height for thumbnailing, get the file from the content repository. Returns res. * @param {string} mxc - * @param {RequestInit & {thumbnail?: {height?: number | string, width?: number | string, animated?: boolean, method?: "crop" | "scale"}}} [init] + * @param {RequestInit & {height?: number | string}} [init] * @return {Promise}>} */ async function getMedia(mxc, init = {}) { - init = {...init} - const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) assert(mediaParts) - - let route = "download" - let query = "" - - if (init.thumbnail) { - route = "thumbnail" - query = "?" + new URLSearchParams(Object.keys(init.thumbnail).map(k => [k, String(init.thumbnail?.[k])])) - } - - let url = `${mreq.baseUrl}/client/v1/media/${route}/${mediaParts[1]}/${mediaParts[2]}${query}` + const downloadOrThumbnail = init.height ? "thumbnail" : "download" + let url = `${mreq.baseUrl}/client/v1/media/${downloadOrThumbnail}/${mediaParts[1]}/${mediaParts[2]}` + if (init.height) url += "?" + new URLSearchParams({height: String(init.height), width: String(init.height)}) const res = await fetch(url, { headers: { Authorization: `Bearer ${reg.as_token}` diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index d1243a7..114bf75 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -78,15 +78,6 @@ function readRegistration() { /** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore let reg = readRegistration() -if (reg) { - fs.watch(registrationFilePath, {persistent: false}, () => { - let newReg = readRegistration() - if (newReg) { - Object.assign(reg, newReg) - } - }) -} - module.exports.registrationFilePath = registrationFilePath module.exports.readRegistration = readRegistration module.exports.getTemplateRegistration = getTemplateRegistration diff --git a/src/stdin.js b/src/stdin.js index 43f9607..fea5fad 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -15,37 +15,18 @@ const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const file = sync.require("./matrix/file") const sendEvent = sync.require("./m2d/actions/send-event") -const redact = sync.require("./m2d/actions/redact") const eventDispatcher = sync.require("./d2m/event-dispatcher") const updatePins = sync.require("./d2m/actions/update-pins") const speedbump = sync.require("./d2m/actions/speedbump") const ks = sync.require("./matrix/kstate") const setPresence = sync.require("./d2m/actions/set-presence") const channelWebhook = sync.require("./m2d/actions/channel-webhook") -const dUtils = sync.require("./discord/utils") -const mxUtils = sync.require("./matrix/utils") const guildID = "112760669178241024" -async function ping() { - const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) - if (result.ok) { - return "Ping OK. The homeserver and OOYE are talking to each other fine." - } else { - if (typeof result.root === "string") { - var msg = `Cannot reach homeserver: ${result.root}` - } else if (result.root.error) { - var msg = `Homeserver said: [${result.status}] ${result.root.error}` - } else { - var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}` - } - return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again." - } -} - if (process.stdin.isTTY) { setImmediate(() => { if (!passthrough.repl) { - const cli = repl.start({prompt: "", eval: customEval, writer: s => s}) + const cli = repl.start({ prompt: "", eval: customEval, writer: s => s }) Object.assign(cli.context, passthrough) passthrough.repl = cli } diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index 6d2772b..82e2f7e 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -3,9 +3,6 @@ const assert = require("assert/strict") const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3") const {z} = require("zod") -const {ReadableStream} = require("stream/web") -const {Readable} = require("stream") -const sharp = require("sharp") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null @@ -22,27 +19,11 @@ const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet") /** @type {import("../../m2d/actions/sticker")} */ const sticker = sync.require("../../m2d/actions/sticker") -// Resizing client-side because server-side is too slow, at least with Synapse. Really need it to be fast because webhook avatars show a placeholder in the interim. -/** @type {{[presetKey: string]: (body: ReadableStream) => ReadableStream}} */ -const MEDIA_THUMBNAIL_PRESETS = { - avatar: body => - Readable.toWeb( - Readable.fromWeb(body).pipe( - sharp() - .resize({height: 210, width: 210, fit: "cover"}) // the largest display of the webhook pfp on Discord Android in screen pixels - .jpeg({force: false, quality: 90}) // File size works out to up to ~110k for a PNG, less for a JPEG - ) - ) -} - const schema = { - media: z.object({ + params: z.object({ server_name: z.string(), media_id: z.string() }), - mediaQuery: z.object({ - preset: z.enum(Object.keys(MEDIA_THUMBNAIL_PRESETS)) // list of possible thumbnail presets - }), sheet: z.object({ e: z.array(z.string()).or(z.string()) }), @@ -84,8 +65,7 @@ function verifyMediaHash(serverAndMediaID) { } as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => { - const params = await getValidatedRouterParams(event, schema.media.parse) - const query = await getValidatedQuery(event, schema.mediaQuery.safeParse) + const params = await getValidatedRouterParams(event, schema.params.parse) verifyMediaHash(`${params.server_name}/${params.media_id}`) const api = getAPI(event) @@ -97,12 +77,7 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn setResponseStatus(event, res.status) setResponseHeader(event, "Content-Type", contentType) setResponseHeader(event, "Transfer-Encoding", "chunked") - - if (res.ok && query.success) { - return MEDIA_THUMBNAIL_PRESETS[query.data.preset](res.body) - } else { - return res.body - } + return res.body })) as.router.get(`/download/sheet`, defineEventHandler(async event => { diff --git a/src/web/routes/letter-avatar.js b/src/web/routes/letter-avatar.js deleted file mode 100644 index d12b004..0000000 --- a/src/web/routes/letter-avatar.js +++ /dev/null @@ -1,117 +0,0 @@ -// @ts-check - -const h3 = require("h3") -const {defineEventHandler, getValidatedQuery, setResponseHeader} = h3 -const sharp = require("sharp") -const {z} = require("zod") - -const {as} = require("../../passthrough") -const {reg} = require("../../matrix/read-registration") - -/* - Create a 300x300 avatar image consisting of a dark coloured background, and a single character in a lighter colour centered in the middle. - Note: Where dimensions are changed, font size must also be changed too to produce an identical image as before. - Simply put, 100px = 60pt for font. -*/ - -const SIZE = 300 -const POSSIBLE_HUES = 12 - -/** Helper function: To get accurate complimenting colours we need to work in HSL, then convert back to RGB at the end */ -function hslToRgb(h, s, l) { - s /= 100; - l /= 100; - - const a = s * Math.min(l, 1 - l); - - const f = n => { - const k = (n + h / 30) % 12; - return l - a * Math.max(-1, Math.min(Math.min(k - 3, 9 - k), 1)); - }; - - return { - r: Math.round(255 * f(0)), - g: Math.round(255 * f(8)), - b: Math.round(255 * f(4)) - }; -} - -/** - * Use the MXID to generate deterministic avatar colours for each user. - * Here, we use the string hash code as a hue value, with a 360 wrap modulo. - * @param {string} mxid - */ -function mxidToHue(mxid) { - // Element Classic string hasher - let hash = 0; - let i; - let chr; - if (mxid.length === 0) { - return hash; - } - for (i = 0; i < mxid.length; i++) { - chr = mxid.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash |= 0; - } - hash = Math.abs(hash) - return (hash % POSSIBLE_HUES) * (360 / POSSIBLE_HUES) -} - -/** - * Get first useful character in username to put in the avatar. - * @param {string} username - */ -function usernameToLetter(username) { - return (username.match(/[a-z0-9]/i)?.[0] || "#").toUpperCase() -} - -/** - * @param {string} mxid - * @param {string} username - */ -function getLetterAvatarURL(mxid, username) { - const p = new URLSearchParams({letter: usernameToLetter(username), hue: String(mxidToHue(mxid))}) - return `${reg.ooye.bridge_origin}/download/letter-avatar?${p}` -} - -const schema = { - letterAvatar: z.object({ - hue: z.coerce.number().min(0).max(360), - letter: z.string().regex(/^[A-Z0-9#]$/) - }) -} - -/** - * Produce a PNG letter-avatar from given parameters. - * @param {string} letter - * @param {number} hue - */ -as.router.get("/download/letter-avatar", defineEventHandler(async event => { - const {letter, hue} = await getValidatedQuery(event, schema.letterAvatar.parse) - - const bg_rgb = hslToRgb(hue, 65, 18); - const text_rgb = hslToRgb(hue, 70, 65); - const text_rgbahex = `#${text_rgb.r.toString(16).padStart(2, "0")}${text_rgb.g.toString(16).padStart(2, "0")}${text_rgb.b.toString(16).padStart(2, "0")}ff` - - const streamOut = sharp({ - create: { - width: SIZE, height: SIZE, channels: 4, - background: { - r: bg_rgb.r, g: bg_rgb.g, b: bg_rgb.b, alpha: 1 - } - } - }).composite([{ - input: { - text: { - text: `${letter}`, - font: "Noto Sans Bold 180", align: "center", rgba: true - } - } - }]).png() - - setResponseHeader(event, "content-type", "image/png") - return streamOut -})) - -module.exports.getLetterAvatarURL = getLetterAvatarURL diff --git a/src/web/routes/stats.js b/src/web/routes/stats.js deleted file mode 100644 index 1bfd300..0000000 --- a/src/web/routes/stats.js +++ /dev/null @@ -1,85 +0,0 @@ -// @ts-check - -const {defineEventHandler, getValidatedQuery, H3Event, setResponseHeader} = require("h3") -const {as, db, sync} = require("../../passthrough") -const {reg} = require("../../matrix/read-registration") - -/** @type {import("../../discord/utils")} */ -const dUtils = sync.require("../../discord/utils") - -// Calculation takes time and is single-threaded. I could add database indexes, but this is simpler and doesn't need storage. -const STATS_CACHE_TIME = 10 * 60 * 1000 // 10 minutes - -function getMessageCountLastDuration(duration) { - const snowflake = dUtils.timestampToSnowflakeInexact(Date.now() - duration) - return db.prepare("select count(*) from message_room where message_id >= ? and length(message_id) = ?").pluck().get(snowflake, snowflake.length) -} - -function getStats() { - const durations = [ - ["week", 7 * 24 * 60 * 60 * 1000], - ["day", 1 * 24 * 60 * 60 * 1000], - ["hour", 1 * 60 * 60 * 1000] - ] - - // console.time("get stats") - let temp = { - guilds: db.prepare("select count(*) from guild_space").pluck().get(), - channels: db.prepare("select count(*) from channel_room").pluck().get(), - messages: db.prepare("select count(*) from message_room").pluck().get(), - ...durations.reduce((a, c) => (a[`messages_last_${c[0]}`] = getMessageCountLastDuration(c[1]), a), {}), - message_sources: db.prepare("select count(*) from event_message where part = 0 group by source order by source").pluck().all(), - oldest_message: new Date(dUtils.snowflakeToTimestampExact(db.prepare("select min(message_id) from event_message where source = 0").pluck().get())), // good until 2090 - discord_users: db.prepare("select count(*) from sim").pluck().get(), - matrix_users: db.prepare("select count(distinct mxid) from member_cache where mxid not like ?").pluck().get(reg.namespaces.users[0].regex.replace(/\.\*.*/, "%")), - } - // console.timeEnd("get stats") - return temp -} - -/** @type {ReturnType} */ -let stats -let statsUpdatedAt = 0 - -function updateStatsIfOld() { - if (statsUpdatedAt < Date.now() - STATS_CACHE_TIME) { - stats = getStats() - statsUpdatedAt = Date.now() - } -} - -as.router.get("/api/stats", defineEventHandler(async event => { - updateStatsIfOld() - return { - ...stats, - oldest_message: stats.oldest_message.toISOString(), - } -})) - -as.router.get("/metrics", defineEventHandler(async event => { - updateStatsIfOld() - setResponseHeader(event, "content-type", "text/plain") - return ` -# HELP guilds Total number of guilds -# TYPE guilds gauge -ooye_guilds_total ${stats.guilds} - -# HELP channels Total number of channels -# TYPE channels gauge -ooye_channels_total ${stats.channels} - -# HELP messages_total Total number of messages sent from each side -# TYPE messages_total gauge -ooye_messages_total{type="matrix"} ${stats.message_sources[0]} -ooye_messages_total{type="discord"} ${stats.message_sources[1]} - -# HELP oldest_message_timestamp Unix timestamp of the oldest message -# TYPE oldest_message_timestamp gauge -ooye_oldest_message_timestamp_seconds ${stats.oldest_message.getTime() / 1000} - -# HELP ooye_users_total Total number of users on each side -# TYPE ooye_users_total gauge -ooye_users_total{type="matrix"} ${stats.matrix_users} -ooye_users_total{type="discord"} ${stats.discord_users} -`.trimStart() -})) diff --git a/src/web/server.js b/src/web/server.js index e28060d..837e14d 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -130,9 +130,7 @@ sync.require("./routes/download-discord") sync.require("./routes/guild-settings") sync.require("./routes/guild") sync.require("./routes/info") -sync.require("./routes/letter-avatar") sync.require("./routes/link") sync.require("./routes/log-in-with-matrix") sync.require("./routes/oauth") sync.require("./routes/password") -sync.require("./routes/stats") diff --git a/test/data.js b/test/data.js index f3092bc..45e0388 100644 --- a/test/data.js +++ b/test/data.js @@ -19,26 +19,6 @@ module.exports = { default_thread_rate_limit_per_user: 0, guild_id: "112760669178241024" }, - voice: { - voice_background_display: null, - version: 1774469910848, - user_limit: 0, - type: 2, - theme_color: null, - status: null, - rtc_region: null, - rate_limit_per_user: 0, - position: 0, - permission_overwrites: [], - parent_id: "805261291908104252", - nsfw: false, - name: "🍞丨[8user] Piece", - last_message_id: "1459912691098325137", - id: "1036840786093953084", - flags: 0, - bitrate: 256000, - guild_id: "112760669178241024" - }, updates: { type: 0, topic: "Updates and release announcements for Out Of Your Element.", @@ -2035,80 +2015,6 @@ module.exports = { tts: false } }, - reply_to_member_join: { - type: 19, - content: "when the broke friend who we pay to bring food shows up at the medieval lord party", - mentions: [], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2026-03-30T12:11:04.443000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - id: "1488148556962332692", - channel_id: "475599038536744962", - author: { - id: "576945009408999426", - username: "randomllama121", - avatar: "08510a70f957106dad1580323c40cd7a", - discriminator: "0", - public_flags: 128, - flags: 128, - banner: null, - accent_color: null, - global_name: "random :3", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: null, - primary_guild: null - }, - pinned: false, - mention_everyone: false, - tts: false, - message_reference: { - type: 0, - channel_id: "475599038536744962", - message_id: "1488146734352826478", - guild_id: "475599038536744960" - }, - referenced_message: { - type: 7, - content: "", - mentions: [], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2026-03-30T12:03:49.899000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - id: "1488146734352826478", - channel_id: "475599038536744962", - author: { - id: "1461677775554478161", - username: "peasant321_76775", - avatar: null, - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "PEASANT!!", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: null, - primary_guild: null - }, - pinned: false, - mention_everyone: false, - tts: false - } - }, attachment_no_content: { id: "1124628646670389348", type: 0, @@ -6264,37 +6170,6 @@ module.exports = { components: [], position: 12 }, - channel_follow_add: { - type: 12, - content: "PluralKit #downtime", - attachments: [], - embeds: [], - timestamp: "2026-03-24T23:16:04.097Z", - edited_timestamp: null, - flags: 0, - components: [], - id: "1486141581047369888", - channel_id: "1451125453082591314", - author: { - id: "154058479798059009", - username: "exaptations", - discriminator: "0", - avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80", - bot: false, - flags: 0, - globalName: "Exa", - }, - pinned: false, - mentions: [], - mention_roles: [], - mention_everyone: false, - tts: false, - message_reference: { - type: 0, - channel_id: "1015204661701124206", - guild_id: "466707357099884544" - } - }, updated_to_start_thread_from_here: { t: "MESSAGE_UPDATE", s: 19, diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 8dd71cd..07f8c24 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -192,7 +192,6 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) V ('!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), -('!qzDBLKlildpzrrOnFZ:cadence.moe', '@lavender.pet:queer.sh', 'lavender.pet', NULL, 0), ('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0), ('!zq94fae5bVKUubZLp7:agiadn.org', '@underscore_x:agiadn.org', 'underscore_x', NULL, 100);