diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index 0f5e0c0..bff64c2 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -113,7 +113,6 @@ Total transitive production dependencies: 144 * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. -* (0) denque: Much faster than using arrays as a queue. * (0) discord-api-types: Bitfields needed at runtime and types needed for development. * (0) domino: DOM implementation that's already pulled in by turndown. * (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. diff --git a/docs/docker.md b/docs/docker.md index 04b9cf4..7e3eb7d 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -66,7 +66,7 @@ I had no hand in creating these and have not used or tested them whatsoever. I m ***The following list is distributed for your information, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.*** -- by melody: https://git.shork.ch/oci-images/out-of-your-element +- by melody: https://git.shork.ch/docker-images/out-of-your-element - by sim2kid: https://github.com/sim2kid/ooye-docker - by Katharos Technology: https://github.com/katharostech/docker_ooye - by Emma: https://cgit.rory.gay/nix/OOYE-module.git/tree diff --git a/docs/get-started.md b/docs/get-started.md index fa67678..5b14b2a 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -56,16 +56,8 @@ After that, to get into the rooms on your Matrix account, use the invite form on I hope you enjoy Out Of Your Element! -# Extras - -## /plu/ral support - -To support /plu/ral webhook proxying, OOYE needs to call the /plu/ral API authenticated. I can't provide an API key for everyone to share, so you have to make your own. Here's how: - -1. Open the [/plu/ral app on Discord](https://discord.com/discovery/applications/1291501048493768784) and "Add App" -> "Add to My Apps (use everywhere)" -2. Go to any text channel on Discord and use the `/api` command from /plu/ral. -3. Create a new application named Out Of Your Element. Copy the displayed token. -3. Run `node scripts/plu-ral-api.js` and paste the token. +---- +




# Appendix diff --git a/package-lock.json b/package-lock.json index 3a2231d..eb07b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", @@ -18,15 +18,14 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", - "@cloudrac3r/turndown": "^7.1.5", + "@cloudrac3r/turndown": "^7.1.4", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.19.0", - "denque": "^2.1.0", - "discord-api-types": "^0.38.52", + "cloudstorm": "^0.17.1", + "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", @@ -37,8 +36,8 @@ "lru-cache": "^11.0.2", "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", - "sharp": "^0.35.3", - "snowtransfer": "^0.19.0", + "sharp": "^0.34.5", + "snowtransfer": "^0.17.5", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", @@ -268,18 +267,18 @@ } }, "node_modules/@cloudrac3r/turndown": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz", - "integrity": "sha512-pENoG62B4UM2ecsg57cuSRDa1OV/YFYTFGg1aoQsKUdHBBglhE9I7EwEMYpS96QoM/auyglfaxjgTeqhcSGCIA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz", + "integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==", "license": "MIT", "dependencies": { "domino": "^2.1.6" } }, "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "license": "MIT", "optional": true, "dependencies": { @@ -302,9 +301,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -314,19 +313,19 @@ "darwin" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -336,38 +335,19 @@ "darwin" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -381,9 +361,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -397,9 +377,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -413,9 +393,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -429,9 +409,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -445,9 +425,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", "cpu": [ "riscv64" ], @@ -461,9 +441,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -477,9 +457,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -493,9 +473,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -509,9 +489,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -525,9 +505,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -537,19 +517,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -559,19 +539,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -581,19 +561,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", "cpu": [ "riscv64" ], @@ -603,19 +583,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -625,19 +605,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -647,19 +627,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -669,19 +649,19 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -691,54 +671,38 @@ "linux" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.1" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -748,16 +712,16 @@ "win32" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -767,16 +731,16 @@ "win32" ], "engines": { - "node": "^20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -786,7 +750,7 @@ "win32" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1039,9 +1003,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", "dependencies": { @@ -1199,16 +1163,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/buffer": { @@ -1352,13 +1316,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.19.0.tgz", - "integrity": "sha512-mjuP5o2nS/CgSljajh08KjN6jYMfNBBttSPHc1YUN74f7OkmhUhv8QE8JFDXomxCfdnLNJcqBQtjRLrs8nkfVQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz", + "integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.49", - "snowtransfer": "^0.19.0" + "discord-api-types": "^0.38.47", + "snowtransfer": "^0.17.5" }, "engines": { "node": ">=22.0.0" @@ -1493,15 +1457,6 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -1518,9 +1473,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.52", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.52.tgz", - "integrity": "sha512-uwe9EKfbjsmgWc2fdFjvDbj+dQqx3lp7wqDCmIha0jInuU+xeQjkCK9tMMn+p7RXfdVQORCInq4cD3U2ymDmyg==", + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -2478,9 +2433,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2490,52 +2445,47 @@ } }, "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.1.0", + "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", - "semver": "^7.8.5" + "semver": "^7.7.3" }, "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -2629,12 +2579,12 @@ } }, "node_modules/snowtransfer": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.19.0.tgz", - "integrity": "sha512-Vebj4FCdpUsEKAzqC1p50ol5o09CLnrffvBCpTv0FNFefvXPXwogxR8kr4zJApIRt7oWQVZZVkxAq4XnGlICjQ==", + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz", + "integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.49" + "discord-api-types": "^0.38.47" }, "engines": { "node": ">=22.0.0" @@ -2859,9 +2809,9 @@ } }, "node_modules/try-to-catch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.1.0.tgz", - "integrity": "sha512-phAezhZqLgHHTpbZYe2GgmKe8itTgEX3gfd+YeIsmllgOOpXCDPn1DkDzVWj0mGQPriJgD6AagoUEcX+M2RlGw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.5.tgz", + "integrity": "sha512-VKBslDQsy4pGj2TMNgDdskWb7AWSi/9dPEmcNv3sdL0+aRMQTPJo6aEqlcuN0vbOwFfsE1oAXmx3bFdf6vrJFg==", "license": "MIT", "engines": { "node": ">=22" diff --git a/package.json b/package.json index 06b2a49..9dfd2a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "out-of-your-element", - "version": "3.6.0", + "version": "3.5.1", "description": "A bridge between Matrix and Discord", "main": "index.js", "repository": { @@ -27,15 +27,14 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", - "@cloudrac3r/turndown": "^7.1.5", + "@cloudrac3r/turndown": "^7.1.4", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.19.0", - "denque": "^2.1.0", - "discord-api-types": "^0.38.52", + "cloudstorm": "^0.17.1", + "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", "entities": "^5.0.0", @@ -46,8 +45,8 @@ "lru-cache": "^11.0.2", "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", - "sharp": "^0.35.3", - "snowtransfer": "^0.19.0", + "sharp": "^0.34.5", + "snowtransfer": "^0.17.5", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", diff --git a/scripts/plu-ral-api.js b/scripts/plu-ral-api.js deleted file mode 100644 index d3468a3..0000000 --- a/scripts/plu-ral-api.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check - -const {reg, writeRegistration} = require("../src/matrix/read-registration") -const {prompt} = require("enquirer") - -;(async () => { - /** @type {{api_key: string}} */ - const apiKeyResponse = await prompt({ - type: "text", - name: "api_key", - message: "Paste your personal /plu/ral API key" - }) - - reg.ooye.plu_ral_api_key = apiKeyResponse.api_key - writeRegistration(reg) - console.log("Saved. This change should be applied instantly.") -})() diff --git a/scripts/setup.js b/scripts/setup.js index 191ca3e..69b62a2 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -359,12 +359,12 @@ function defineEchoHandler() { console.log("✅ Emojis are ready...") // set profile data on discord... - await discord.snow.user.editSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")}) + await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")}) console.log("✅ Discord profile updated...") // set profile data on homeserver... console.log("⏩ Updating Matrix profile... (If you've joined lots of rooms, this is slow. Please allow at least 30 seconds.)") - await api.profileSetDisplayname(mxid, "Out Of Your Element (bot)") + await api.profileSetDisplayname(mxid, "Out Of Your Element") await api.profileSetAvatarUrl(mxid, avatarUrl) console.log("✅ Matrix profile updated...") diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index 9f5d7e3..7a751e2 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -8,8 +8,6 @@ const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ @@ -239,12 +237,8 @@ async function syncSpaceExpressions(data, checkBeforeSync) { try { existing = await api.getStateEvent(spaceID, "im.ponies.room_emotes", eventKey) } catch (e) { - if (e instanceof mreq.MatrixServerError && e.httpStatus < 500) { - // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake. - existing = fn([], guild) - } else { - throw e - } + // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake. + existing = fn([], guild) } if (isDeepStrictEqual(existing, content)) return } diff --git a/src/d2m/actions/edit-message.js b/src/d2m/actions/edit-message.js index 31f3627..f86a9c8 100644 --- a/src/d2m/actions/edit-message.js +++ b/src/d2m/actions/edit-message.js @@ -1,24 +1,24 @@ // @ts-check +const assert = require("assert").strict + const passthrough = require("../../passthrough") const {sync, db, select, from} = passthrough -const {reg} = require("../../matrix/read-registration") /** @type {import("../converters/edit-to-changes")} */ const editToChanges = sync.require("../converters/edit-to-changes") -/** @type {import("./speedbump")} */ -const speedbump = sync.require("./speedbump") +/** @type {import("./register-pk-user")} */ +const registerPkUser = sync.require("./register-pk-user") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/mreq")} */ const mreq = sync.require("../../matrix/mreq") -/** @type {import("../../discord/utils")} */ -const dUtils = sync.require("../../discord/utils") /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message * @param {import("discord-api-types/v10").APIGuild} guild + * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel */ -async function editMessage(message, guild) { +async function editMessage(message, guild, row) { const historicalRoomOfMessage = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: message.id}).select("room_id").get() const currentRoom = from("channel_room").join("historical_channel_room", "room_id").where({channel_id: message.channel_id}).select("room_id", "historical_room_index").get() if (!currentRoom) return @@ -27,9 +27,11 @@ async function editMessage(message, guild) { let {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promotions} = await editToChanges.editToChanges(message, guild, api) - // Sync proxy user profile (if sent by proxy) - if (dUtils.isWebhookMessage(message)) { - senderMxid = await speedbump.getWebhookSenderId(message, guild.id, roomID) + if (row && row.speedbump_webhook_id === message.webhook_id) { + // Handle the PluralKit public instance + if (row.speedbump_id === "466378653216014359") { + senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true) + } } // 1. Replace all the things. diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index 4946d4f..6ecd077 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert").strict +const assert = require("assert") const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") @@ -132,10 +132,10 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) { try { // API lookup var pkMessage = await fetchMessage(messageID) - db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 0)").run(pkMessage.member.uuid, pkMessage.sender, author.username) + db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username) } catch (e) { // Fall back to offline cache - const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 0}).pluck("mxid").get() + const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get() if (!senderMxid) throw e return senderMxid } diff --git a/src/d2m/actions/register-plu-ral-user.js b/src/d2m/actions/register-plu-ral-user.js deleted file mode 100644 index 04fa28c..0000000 --- a/src/d2m/actions/register-plu-ral-user.js +++ /dev/null @@ -1,170 +0,0 @@ -// @ts-check - -const assert = require("assert").strict -const {reg} = require("../../matrix/read-registration") -const Ty = require("../../types") - -const passthrough = require("../../passthrough") -const {sync, db, select, from} = passthrough -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") -/** @type {import("../../matrix/file")} */ -const file = sync.require("../../matrix/file") -/** @type {import("./register-user")} */ -const registerUser = sync.require("./register-user") - -/** @returns {Promise} */ -async function fetchMessage(channelID, messageID) { - assert(reg.ooye.plu_ral_api_key) - try { - var res = await fetch(`https://api.plural.gg/messages/${channelID}/${messageID}?member=true`, { - headers: { - Authorization: reg.ooye.plu_ral_api_key - } - }) - } catch (networkError) { - // Network issue, raise a more readable message - throw new Error(`Failed to connect to /plu/ral API: ${networkError.toString()}`) - } - if (!res.ok) throw new Error(`/plu/ral API returned an error: ${await res.text()}`) - /** @type {any} */ - const root = await res.json() - if (!root.member) throw new Error(`/plu/ral API didn't return member data: ${JSON.stringify(root)}`) - return root -} - -/** - * Using the same sim names and fake user IDs for /plu/ral members, since unlike PluralKit they don't have a short and a long ID. - * @param {Ty.PluRalWebhookMessage} pluRalMessage - */ -function getSimName(pluRalMessage) { - return `_pl_${pluRalMessage.member_id}` -} - -/** - * A sim is an account that is being simulated by the bridge to copy events from the other side. - * @param {Ty.PluRalWebhookMessage} pluRalMessage - * @returns mxid - */ -async function createSim(pluRalMessage) { - // Choose sim name - const simName = getSimName(pluRalMessage) - const localpart = reg.ooye.namespace_prefix + simName - const mxid = `@${localpart}:${reg.ooye.server_name}` - - // Save chosen name in the database forever - db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(simName, simName, simName, mxid) - - // Register matrix user with that name - try { - await api.register(localpart) - } catch (e) { - // If user creation fails, manually undo the database change. Still isn't perfect, but should help. - // (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.) - db.prepare("DELETE FROM sim WHERE user_id = ?").run(simName) - throw e - } - return mxid -} - -/** - * Ensure a sim is registered for the user. - * If there is already a sim, use that one. If there isn't one yet, register a new sim. - * @param {Ty.PluRalWebhookMessage} pluRalMessage - * @returns {Promise} mxid - */ -async function ensureSim(pluRalMessage) { - let mxid = null - const existing = select("sim", "mxid", {user_id: getSimName(pluRalMessage)}).pluck().get() - if (existing) { - mxid = existing - } else { - mxid = await createSim(pluRalMessage) - } - return mxid -} - -/** - * Ensure a sim is registered for the user and is joined to the room. - * @param {Ty.PluRalWebhookMessage} pluRalMessage - * @param {string} roomID - * @returns {Promise} mxid - */ -async function ensureSimJoined(pluRalMessage, roomID) { - // Ensure room ID is really an ID, not an alias - assert.ok(roomID[0] === "!") - - // Ensure user - const mxid = await ensureSim(pluRalMessage) - - // Ensure joined - const existing = select("sim_member", "mxid", {room_id: roomID, mxid}).pluck().get() - if (!existing) { - await api.inviteToRoom(roomID, mxid) - await api.joinRoom(roomID, mxid) - db.prepare("INSERT OR IGNORE INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid) - } - return mxid -} - -/** - * Generate profile data based on webhook displayname and configured avatar. - * @param {Ty.PluRalWebhookMessage} pluRalMessage - * @param {Ty.WebhookAuthor} author - */ -async function memberToStateContent(pluRalMessage, author) { - // We prefer to use the member's avatar URL data since the image upload can be cached across channels, - // unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL. - const avatar = pluRalMessage.member.avatar_url || file.userAvatar(author) - - const content = { - displayname: author.username, - membership: "join", - "moe.cadence.ooye.plu_ral_member": pluRalMessage.member - } - if (avatar) content.avatar_url = await file.uploadDiscordFileToMxc(avatar) - - return content -} - -/** - * Sync profile data for a sim user. This function follows the following process: - * 1. Look up data about proxy user from API - * 2. If this fails, try to use previously cached data (won't sync) - * 3. Create and join the sim to the room if needed - * 4. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before - * 5. Compare against the previously known state content, which is helpfully stored in the database - * 6. If the state content has changed, send it to Matrix and update it in the database for next time - * @param {string} channelID to call API with - * @param {string} messageID to call API with - * @param {Ty.WebhookAuthor} author for profile data - * @param {string} roomID room to join member to - * @param {boolean} shouldActuallySync whether to actually sync updated user data or just ensure it's joined - * @returns {Promise} mxid of the updated sim - */ -async function syncUser(channelID, messageID, author, roomID, shouldActuallySync) { - try { - // API lookup - var pluRalMessage = await fetchMessage(channelID, messageID) - const simName = getSimName(pluRalMessage) - db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 1)").run(simName, pluRalMessage.author_id, author.username) - } catch (e) { - // Fall back to offline cache - const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 1}).pluck("mxid").get() - if (!senderMxid) throw e - return senderMxid - } - - // Create and join the sim to the room if needed - const mxid = await ensureSimJoined(pluRalMessage, roomID) - - if (shouldActuallySync) { - // Build current profile data and sync if the hash has changed - const content = await memberToStateContent(pluRalMessage, author) - await registerUser._sendSyncUser(roomID, mxid, content, null) - } - - return mxid -} - -module.exports.syncUser = syncUser diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 41ef70d..8550d43 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -4,16 +4,17 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") -const {discord, sync, db, select, from} = passthrough -const {reg} = require("../../matrix/read-registration") +const { discord, sync, db, select, from} = passthrough /** @type {import("../converters/message-to-event")} */ const messageToEvent = sync.require("../converters/message-to-event") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("./register-user")} */ const registerUser = sync.require("./register-user") -/** @type {import("./speedbump")} */ -const speedbump = sync.require("./speedbump") +/** @type {import("./register-pk-user")} */ +const registerPkUser = sync.require("./register-pk-user") +/** @type {import("./register-webhook-user")} */ +const registerWebhookUser = sync.require("./register-webhook-user") /** @type {import("../actions/create-room")} */ const createRoom = sync.require("../actions/create-room") /** @type {import("../actions/poll-end")} */ @@ -27,15 +28,24 @@ const channelWebhook = sync.require("../../m2d/actions/channel-webhook") * @param {DiscordTypes.GatewayMessageCreateDispatchData} message * @param {DiscordTypes.APIGuildChannel} channel * @param {DiscordTypes.APIGuild} guild + * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel */ -async function sendMessage(message, channel, guild) { +async function sendMessage(message, channel, guild, row) { const roomID = await createRoom.ensureRoom(message.channel_id) const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: roomID}).pluck().get() assert(historicalRoomIndex) let senderMxid = null if (dUtils.isWebhookMessage(message)) { - senderMxid = await speedbump.getWebhookSenderId(message, guild.id, roomID) + const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guild.id}).pluck().get() ?? 0 + if (row && row.speedbump_webhook_id === message.webhook_id) { + // Handle the PluralKit public instance + if (row.speedbump_id === "466378653216014359") { + senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true) + } + } else if (useWebhookProfile) { + senderMxid = await registerWebhookUser.syncUser(message.author, roomID, true) + } } else { // not a webhook if (message.author.id === discord.application.id) { @@ -50,7 +60,8 @@ async function sendMessage(message, channel, guild) { const detailedResultsMessage = await pollEnd.endPoll(message) if (detailedResultsMessage) { const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get() - const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent) + const channelID = threadParent ? threadParent : message.channel_id + const threadID = threadParent ? message.channel_id : undefined sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID) } } diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js index 88cea07..218f046 100644 --- a/src/d2m/actions/speedbump.js +++ b/src/d2m/actions/speedbump.js @@ -1,23 +1,14 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") - const passthrough = require("../../passthrough") -const {discord, from, select, db, sync} = passthrough - -const {reg} = require("../../matrix/read-registration") -/** @type {import("./register-pk-user")} */ -const registerPkUser = sync.require("./register-pk-user") -/** @type {import("./register-plu-ral-user")} */ -const registerPluRalUser = sync.require("./register-plu-ral-user") -/** @type {import("./register-webhook-user")} */ -const registerWebhookUser = sync.require("./register-webhook-user") +const {discord, select, db} = passthrough const DEBUG_SPEEDBUMP = false -function debugSpeedbump(...args) { +function debugSpeedbump(message) { if (DEBUG_SPEEDBUMP) { - console.log(...args) + console.log(message) } } @@ -26,8 +17,7 @@ const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours /** @type {Set} */ const KNOWN_BOTS = new Set([ - "466378653216014359", // PluralKit - "1291501048493768784", // /plu/ral + "466378653216014359" // PluralKit ]) /** @@ -39,81 +29,55 @@ async function updateCache(channelID, lastChecked) { const now = Math.floor(Date.now() / 1000) if (lastChecked && now - lastChecked < SPEEDBUMP_UPDATE_FREQUENCY) return const webhooks = await discord.snow.webhook.getChannelWebhooks(channelID) - const found = webhooks.filter(b => KNOWN_BOTS.has(b.application_id)) - db.transaction(() => { - db.prepare("DELETE FROM channel_speedbump WHERE channel_id = ?").run(channelID) - for (const webhook of found) { - db.prepare("INSERT INTO channel_speedbump (channel_id, speedbump_webhook_id, speedbump_user_id) VALUES (?, ?, ?)").run(channelID, webhook.id, webhook.application_id) - } - db.prepare("UPDATE channel_room SET speedbump_checked = ? WHERE channel_id = ?").run(now, channelID) - })() + const found = webhooks.find(b => KNOWN_BOTS.has(b.application_id)) + const foundApplication = found?.application_id + const foundWebhook = found?.id + db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID) } -/** - * @typedef BumpingEntry - * @prop {number} number number of gateway events currently bumping for this message ID - * @prop {boolean} hasCreate whether there was a message create within the events currently bumping - */ - -/** @type {Map} messageID -> BumpingEntry */ +/** @type {Map} messageID -> number of gateway events currently bumping */ const bumping = new Map() /** * Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted. - * @param {boolean} isCreate * @param {string} messageID * @returns whether it was deleted */ -async function doSpeedbump(isCreate, messageID) { - const entry = bumping.get(messageID) ?? (() => { - const entry = {number: 0, hasCreate: false} - bumping.set(messageID, entry) - return entry - })() - - entry.number++ - entry.hasCreate ||= isCreate - - debugSpeedbump(`[speedbump] WAIT ${messageID}++ =`, entry) +async function doSpeedbump(messageID) { + let value = (bumping.get(messageID) ?? 0) + 1 + bumping.set(messageID, value) + debugSpeedbump(`[speedbump] WAIT ${messageID}++ = ${value}`) await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED)) if (!bumping.has(messageID)) { debugSpeedbump(`[speedbump] DELETED ${messageID}`) - return {skip: true, hasCreate: null} + return true } - - if (--entry.number <= 0) { - debugSpeedbump(`[speedbump] OK ${messageID}-- =`, entry) + value = (bumping.get(messageID) ?? 0) - 1 + if (value <= 0) { + debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`) bumping.delete(messageID) - return {skip: false, hasCreate: entry.hasCreate} + return false } else { - debugSpeedbump(`[speedbump] MULTI ${messageID}-- =`, entry) - return {skip: true, hasCreate: null} + debugSpeedbump(`[speedbump] MULTI ${messageID}-- = ${value}`) + bumping.set(messageID, value) + return true } } -function getSpeedbumpRows(channelID) { - return from("channel_room").join("channel_speedbump", "channel_id").select("thread_parent", "speedbump_user_id", "speedbump_webhook_id").where({channel_id: channelID}).all() -} - /** * Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted. - * @param {boolean} isCreate - * @param {{id: string, channel_id: string, author: {id: string}, backfill?: boolean}} message uses the ID to identify, and the userID to only slow down the message when the user has used PK before - * @returns whether to skip this message, and whether the message should be created as a creation + * @param {string} channelID + * @param {string} messageID + * @returns whether it was deleted, and data about the channel's (not thread's) speedbump */ -async function maybeDoSpeedbump(isCreate, message) { - let rows = getSpeedbumpRows(message.channel_id) - if (rows[0]?.thread_parent) rows = getSpeedbumpRows(rows[0].thread_parent) // webhooks belong to the channel, not the thread - if (!rows.length) return {skip: false} // channel not affected, no speedbump - - if (message.backfill) return {skip: false} // don't slow messages during backfill - if (rows.some(r => r.speedbump_webhook_id === message.author.id)) return {skip: false} // shortcut - const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: message.author.id}).pluck().get() - if (!userHasProxy) return {skip: false} // user has not used PK before, no speedbump - const {skip, hasCreate} = await doSpeedbump(isCreate, message.id) - return {skip, hasCreate} // maybe affected, and there is a speedbump +async function maybeDoSpeedbump(channelID, messageID) { + let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get() + if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread + if (!row?.speedbump_webhook_id) return {affected: false, row: null} // not affected, no speedbump + const affected = await doSpeedbump(messageID) + return {affected, row} // maybe affected, and there is a speedbump } /** @@ -123,25 +87,7 @@ function onMessageDelete(messageID) { bumping.delete(messageID) } -/** - * @param {DiscordTypes.APIMessage} message - * @param {string} guildID - * @param {string} roomID - */ -async function getWebhookSenderId(message, guildID, roomID) { - const speedbumpUserID = select("channel_speedbump", "speedbump_user_id", {channel_id: message.channel_id, speedbump_webhook_id: message.webhook_id}).pluck().get() - const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guildID}).pluck().get() ?? 0 - if (speedbumpUserID === "466378653216014359") { // PluralKit public instance - return await registerPkUser.syncUser(message.id, message.author, roomID, true) - } else if (speedbumpUserID === "1291501048493768784" && reg.ooye.plu_ral_api_key) { // /plu/ral public instance - return await registerPluRalUser.syncUser(message.channel_id, message.id, message.author, roomID, true) - } else if (useWebhookProfile) { - return await registerWebhookUser.syncUser(message.author, roomID, true) - } - return null -} - module.exports.updateCache = updateCache +module.exports.doSpeedbump = doSpeedbump module.exports.maybeDoSpeedbump = maybeDoSpeedbump module.exports.onMessageDelete = onMessageDelete -module.exports.getWebhookSenderId = getWebhookSenderId diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index aeda572..7229d3d 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -265,9 +265,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) { * @param {any} newEvents merge into events * @param {any} events will be modified * @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc - * @param {boolean} [forceMerge] if true, must merge event, will error if it had to append */ -function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) { +function mergeTextEvents(newEvents, events, forceSameMsgtype) { let prev = events.at(-1) for (const ne of newEvents) { const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype) @@ -279,8 +278,6 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false rep.addLine(ne.body, ne.formatted_body) prev.body = rep.body prev.formatted_body = rep.formattedBody - } else if (forceMerge) { - throw new Error("Unable to merge events") } else { events.push(ne) } @@ -557,7 +554,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter. // So we scan the message ahead of time for all its emojis and ensure they are in the DB. - const emojiMatches = [...content.matchAll(/<(a?):([^:>]+):([0-9]+)>/g)] + const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)] await Promise.all(emojiMatches.map(match => { const id = match[3] const name = match[2] @@ -970,8 +967,7 @@ async function messageToEvent(message, guild, options = {}, di) { // May only be a section accessory or in an action row (up to 5) if (component.style === DiscordTypes.ButtonStyle.Link) { assert(component.label) // required for Discord to validate link buttons - const link = await transformContentMessageLinks(component.url) - stack.msb.add(`[${component.label} ${link}] `, tag`${component.label} `) + stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) } } @@ -984,19 +980,7 @@ async function messageToEvent(message, guild, options = {}, di) { const {body, formatted_body} = stack.msb.get() if (body.trim().length) { - // Create new message if Components V2 (cannot have regular content) - if ((message.flags ?? 0) & DiscordTypes.MessageFlags.IsComponentsV2) { - await addTextEvent(body, formatted_body, "m.text") - } - // Add to existing message if legacy components https://docs.discord.com/developers/components/reference#legacy-message-component-behavior - else { - mergeTextEvents([{ - msgtype: "m.text", - body, - format: "org.matrix.custom.html", - formatted_body - }], events, false, true) - } + await addTextEvent(body, formatted_body, "m.text") } } diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 1ef83c3..137b63b 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -1,7 +1,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../matrix/utils.test") test("message2event components: pk question mark output", async t => { const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {}) @@ -78,24 +77,3 @@ test("message2event components: pk question mark output", async t => { msgtype: "m.text", }]) }) - -test("message2event components: pk ping message legacy components", async t => { - const events = await messageToEvent(data.message_with_components.pk_ping_components_v1, data.guild.general, {}, { - api: { - async getJoinedMembers() { - return {joined: {}} - }, - getEffectivePower: mockGetEffectivePower() - } - }) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "❭ cadence used `/🔔 Ping author`" - + "\nPsst, **Red** (@cadence.worm:), you have been pinged by @cadence.worm:." - + "\n[Jump https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe] ", - format: "org.matrix.custom.html", - formatted_body: "
cadence used /🔔 Ping author
Psst, Red (@cadence.worm), you have been pinged by @cadence.worm.
Jump ", - "m.mentions": {} - }]) -}) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index b9b45e2..c2a0549 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -14,8 +14,6 @@ const {sync} = passthrough /** @type {import("./discord-packets")} */ const discordPackets = sync.require("./discord-packets") -const CONNECTION_DEBUG = false - class DiscordClient { /** * @param {string} discordToken @@ -26,7 +24,7 @@ class DiscordClient { const intents = [ "DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING", "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS", - "MESSAGE_CONTENT", "GUILD_MEMBERS" + "MESSAGE_CONTENT" ] if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES") this.discordToken = discordToken @@ -61,17 +59,15 @@ class DiscordClient { }) } - if (CONNECTION_DEBUG) { - const addEventLogger = (eventName, logName) => { - this.cloud.on(eventName, (...args) => { - const d = new Date().toISOString().slice(0, 19) - console.error(`[${d} Client ${logName}]`, ...args) - }) - } - addEventLogger("error", "Error") - addEventLogger("disconnected", "Disconnected") - addEventLogger("ready", "Ready") + const addEventLogger = (eventName, logName) => { + this.cloud.on(eventName, (...args) => { + const d = new Date().toISOString().slice(0, 19) + console.error(`[${d} Client ${logName}]`, ...args) + }) } + addEventLogger("error", "Error") + addEventLogger("disconnected", "Disconnected") + addEventLogger("ready", "Ready") } } diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 988f981..afea9ea 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -1,227 +1,173 @@ // @ts-check -const assert = require("assert") -const {scheduler} = require("timers/promises") +// Discord library internals type beat + +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") -const {sync} = passthrough +const {sync, db} = passthrough -/** @type {import("../matrix/homeserver-status")} */ -const homeserverStatus = sync.require("../matrix/homeserver-status") +const utils = { + /** + * @param {import("./discord-client")} client + * @param {import("cloudstorm").IGatewayMessage} message + * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate + */ + async onPacket(client, message, listen) { + // requiring this later so that the client is already constructed by the time event-dispatcher is loaded + /** @type {typeof import("./event-dispatcher")} */ + const eventDispatcher = sync.require("./event-dispatcher") + /** @type {import("../discord/register-interactions")} */ + const interactions = sync.require("../discord/register-interactions") -let checkedHomeserver = false + // Client internals, keep track of the state we need + if (message.t === "READY") { + if (client.ready) return + client.ready = true + client.user = message.d.user + client.application = message.d.application + console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + interactions.registerInteractions() -/** - * @param {import("./discord-client")} client - * @param {import("cloudstorm").IGatewayMessage} message - * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate - */ -async function onPacket(client, message, listen) { - // requiring this later so that the client is already constructed by the time event-dispatcher is loaded - /** @type {typeof import("./event-dispatcher")} */ - const eventDispatcher = sync.require("./event-dispatcher") - /** @type {import("../discord/register-interactions")} */ - const interactions = sync.require("../discord/register-interactions") + } else if (message.t === "GUILD_CREATE") { + message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web + client.guilds.set(message.d.id, message.d) + const arr = [] + client.guildChannelMap.set(message.d.id, arr) + for (const channel of message.d.channels || []) { + // @ts-ignore + channel.guild_id = message.d.id + arr.push(channel.id) + client.channels.set(channel.id, channel) + } + for (const thread of message.d.threads || []) { + // @ts-ignore + thread.guild_id = message.d.id + arr.push(thread.id) + client.channels.set(thread.id, thread) + } - // Client internals, keep track of the state we need - if (message.t === "READY") { - if (client.ready) return - client.ready = true - client.user = message.d.user - client.application = message.d.application - console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) - interactions.registerInteractions() - - } else if (message.t === "GUILD_CREATE") { - message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web - client.guilds.set(message.d.id, message.d) - const arr = [] - client.guildChannelMap.set(message.d.id, arr) - for (const channel of message.d.channels || []) { - // @ts-ignore - channel.guild_id = message.d.id - arr.push(channel.id) - client.channels.set(channel.id, channel) - } - for (const thread of message.d.threads || []) { - // @ts-ignore - thread.guild_id = message.d.id - arr.push(thread.id) - client.channels.set(thread.id, thread) - } - - if (listen === "full") { - try { - /* - Info about guilds is populated one guild at a time. - For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild. - If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping. - We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver. - This delay can be removed if ONE of the following is done: - 1. m->d can queue incoming events until their guild exists in memory - 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first - */ - let isMainCharacter = false - if (!checkedHomeserver) { - checkedHomeserver = true - isMainCharacter = true - console.log("Warming up guilds~") + if (listen === "full") { + try { + await eventDispatcher.checkMissedExpressions(message.d) + await eventDispatcher.checkMissedMessages(client, message.d) + await eventDispatcher.checkMissedPins(client, message.d) + await eventDispatcher.checkMissedLeaves(client, message.d) + } catch (e) { + console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") + console.error(e) } - await scheduler.wait(5000) - if (isMainCharacter) { - checkedHomeserver = true - process.stdout.write("Connecting to homeserver... ") - await homeserverStatus.homeserverStatus.waitForOnline(true) - console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.") + } + + } else if (message.t === "GUILD_UPDATE") { + const guild = client.guilds.get(message.d.id) + if (guild) { + for (const prop of Object.keys(message.d)) { + if (!["channels", "threads"].includes(prop)) { + guild[prop] = message.d[prop] + } + } + } + + } else if (message.t === "GUILD_EMOJIS_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + if (guild) { + guild.emojis = message.d.emojis + } + + } else if (message.t === "GUILD_STICKERS_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + if (guild) { + guild.stickers = message.d.stickers + } + + } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") { + const guild = client.guilds.get(message.d.guild_id) + /** Delete this in case of UPDATE or DELETE */ + const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id + /** Add this in case of CREATE or UPDATE */ + const newRoles = [] + if ("role" in message.d) newRoles.push(message.d.role) + if (guild) { + const targetIndex = guild.roles.findIndex(r => r.id === targetID) + if (targetIndex !== -1) { + // Role already exists. Delete it and maybe replace it. + guild.roles.splice(targetIndex, 1, ...newRoles) } else { - await homeserverStatus.homeserverStatus.waitForOnline(false) - } - - await eventDispatcher.checkMissedExpressions(message.d) - await eventDispatcher.checkMissedMessages(client, message.d) - await eventDispatcher.checkMissedPins(client, message.d) - await eventDispatcher.checkMissedLeaves(client, message.d) - } catch (e) { - console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") - console.error(e) - } - } - - } else if (message.t === "GUILD_UPDATE") { - const guild = client.guilds.get(message.d.id) - if (guild) { - for (const prop of Object.keys(message.d)) { - if (!["channels", "threads"].includes(prop)) { - guild[prop] = message.d[prop] + // Role doesn't already exist. + guild.roles.push(...newRoles) } } - } - } else if (message.t === "GUILD_EMOJIS_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - if (guild) { - guild.emojis = message.d.emojis - } - - } else if (message.t === "GUILD_STICKERS_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - if (guild) { - guild.stickers = message.d.stickers - } - - } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") { - const guild = client.guilds.get(message.d.guild_id) - /** Delete this in case of UPDATE or DELETE */ - const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id - /** Add this in case of CREATE or UPDATE */ - const newRoles = [] - if ("role" in message.d) newRoles.push(message.d.role) - if (guild) { - const targetIndex = guild.roles.findIndex(r => r.id === targetID) - if (targetIndex !== -1) { - // Role already exists. Delete it and maybe replace it. - guild.roles.splice(targetIndex, 1, ...newRoles) - } else { - // Role doesn't already exist. - guild.roles.push(...newRoles) + } else if (message.t === "GUILD_MEMBER_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + const member = guild?.members.find(m => m.user.id === message.d.user.id) + if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones + Object.assign(member, message.d) } - } - } else if (message.t === "GUILD_MEMBER_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - const member = guild?.members.find(m => m.user.id === message.d.user.id) - if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones - Object.assign(member, message.d) - } + } else if (message.t === "THREAD_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } - } else if (message.t === "THREAD_CREATE") { - client.channels.set(message.d.id, message.d) - if (message.d["guild_id"]) { - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) - } - - } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { - client.channels.set(message.d.id, message.d) + } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { + client.channels.set(message.d.id, message.d) - } else if (message.t === "CHANNEL_PINS_UPDATE") { - const channel = client.channels.get(message.d.channel_id) - if (channel) { - channel["last_pin_timestamp"] = message.d.last_pin_timestamp - } + } else if (message.t === "CHANNEL_PINS_UPDATE") { + const channel = client.channels.get(message.d.channel_id) + if (channel) { + channel["last_pin_timestamp"] = message.d.last_pin_timestamp + } - } else if (message.t === "GUILD_DELETE") { - client.guilds.delete(message.d.id) - const channels = client.guildChannelMap.get(message.d.id) - if (channels) { - for (const id of channels) client.channels.delete(id) - } - client.guildChannelMap.delete(message.d.id) - - - } else if (message.t === "CHANNEL_CREATE") { - client.channels.set(message.d.id, message.d) - if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) - } - - } else if (message.t === "CHANNEL_DELETE") { - client.channels.delete(message.d.id) - if (message.d["guild_id"]) { - const channels = client.guildChannelMap.get(message.d["guild_id"]) + } else if (message.t === "GUILD_DELETE") { + client.guilds.delete(message.d.id) + const channels = client.guildChannelMap.get(message.d.id) if (channels) { - const previous = channels.indexOf(message.d.id) - if (previous !== -1) channels.splice(previous, 1) + for (const id of channels) client.channels.delete(id) + } + client.guildChannelMap.delete(message.d.id) + + + } else if (message.t === "CHANNEL_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } + + } else if (message.t === "CHANNEL_DELETE") { + client.channels.delete(message.d.id) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels) { + const previous = channels.indexOf(message.d.id) + if (previous !== -1) channels.splice(previous, 1) + } + } + } + + // Event dispatcher for OOYE bridge operations + if (listen === "full" && message.t) { + try { + if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { + await eventDispatcher.onSomeReactionsRemoved(client, message.d) + + } else if (message.t === "INTERACTION_CREATE") { + await interactions.dispatchInteraction(message.d) + + } else if (message.t in eventDispatcher) { + await eventDispatcher[message.t](client, message.d) + } + } catch (e) { + // Let OOYE try to handle errors too + await eventDispatcher.onError(client, e, message) } } } - - // Event dispatcher for OOYE bridge operations - if (listen === "full" && message.t) { - const alwaysRealTimeEvents = ["PRESENCE_UPDATE"] - const optionalEvents = ["TYPING_START"] - if (alwaysRealTimeEvents.includes(message.t) || homeserverStatus.homeserverStatus.isRealTime()) { - // real time - dispatchPacketToBridge(client, message) - } else if (optionalEvents.includes(message.t)) { - // when not real time, skip optional events - } else { - // queue required events - homeserverStatus.homeserverStatus.queuePacket(message) - } - } } -/** - * @param {import("./discord-client")} client - * @param {import("cloudstorm").IGatewayMessage} message - */ -async function dispatchPacketToBridge(client, message) { - // requiring this later so that the client is already constructed by the time event-dispatcher is loaded - /** @type {typeof import("./event-dispatcher")} */ - const eventDispatcher = sync.require("./event-dispatcher") - /** @type {import("../discord/register-interactions")} */ - const interactions = sync.require("../discord/register-interactions") - - assert(message.t) // checked above - - try { - if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { - await eventDispatcher.onSomeReactionsRemoved(client, message.d) - - } else if (message.t === "INTERACTION_CREATE") { - await interactions.dispatchInteraction(message.d) - - } else if (message.t in eventDispatcher) { - await eventDispatcher[message.t](client, message.d) - } - } catch (e) { - // Let OOYE try to handle errors too - await eventDispatcher.onError(client, e, message) - } -} - -module.exports.onPacket = onPacket -module.exports.dispatchPacketToBridge = dispatchPacketToBridge +module.exports = utils diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index a49453a..8101a03 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -60,7 +60,7 @@ module.exports = { matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage) - const channelID = gatewayMessage.d?.["channel_id"] + const channelID = gatewayMessage.d["channel_id"] if (!channelID) return const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (!roomID) return @@ -288,6 +288,7 @@ module.exports = { if (!guildID) return // channel must have been a DM channel or something const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() if (!roomID) return // channel wasn't being bridged in the first place + // @ts-ignore await createRoom.unbridgeChannel(channel, guildID) }, @@ -312,10 +313,11 @@ module.exports = { if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable - const {skip} = await speedbump.maybeDoSpeedbump(true, message) - if (skip) return + const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id) + if (affected) return - await sendMessage.sendMessage(message, channel, guild) + // @ts-ignore + await sendMessage.sendMessage(message, channel, guild, row) retrigger.finishedBridging(message.id) }, @@ -333,27 +335,24 @@ module.exports = { if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only! // Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from. - const {skip, hasCreate} = await speedbump.maybeDoSpeedbump(false, data) - if (skip) return + const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) + if (affected) return + + if (!row) { + // Check that the sending-to room exists, and deal with Eventual Consistency(TM) + if (!await retrigger.waitForMessage(data.id)) return + } /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ + // @ts-ignore const message = data const channel = client.channels.get(message.channel_id) if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) assert(guild) - if (!hasCreate) { - // Standard path for most message updates - // Check that the target message already exists, and deal with Eventual Consistency(TM) - if (!await retrigger.waitForMessage(data.id)) return - await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild)) - } - else { - // Path for edit packets that were speedbumped into the latest copy of a message that needs to be created - // Just pretend to be MESSAGE_CREATE - await sendMessage.sendMessage(message, channel, guild) - } + // @ts-ignore + await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row)) }, /** @@ -415,7 +414,6 @@ module.exports = { * @param {DiscordTypes.GatewayTypingStartDispatchData} data */ async TYPING_START(client, data) { - if (data.user_id === client.user.id) return const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() if (!roomID) return const mxid = from("sim").join("sim_member", "mxid").where({user_id: data.user_id, room_id: roomID}).pluck("mxid").get() diff --git a/src/db/migrations/0039-refactor-speedbump.sql b/src/db/migrations/0039-refactor-speedbump.sql deleted file mode 100644 index de739dc..0000000 --- a/src/db/migrations/0039-refactor-speedbump.sql +++ /dev/null @@ -1,19 +0,0 @@ -BEGIN TRANSACTION; - -CREATE TABLE "channel_speedbump" ( - "channel_id" TEXT NOT NULL, - "speedbump_webhook_id" TEXT NOT NULL, - "speedbump_user_id" TEXT NOT NULL, - PRIMARY KEY("channel_id","speedbump_webhook_id"), - FOREIGN KEY("channel_id") REFERENCES "channel_room"("channel_id") -) WITHOUT ROWID; - -INSERT INTO channel_speedbump (channel_id, speedbump_webhook_id, speedbump_user_id) - SELECT channel_id, speedbump_webhook_id, speedbump_id FROM channel_room WHERE speedbump_id IS NOT NULL AND speedbump_webhook_id IS NOT NULL; - -ALTER TABLE channel_room DROP COLUMN speedbump_id; -ALTER TABLE channel_room DROP COLUMN speedbump_webhook_id; - -ALTER TABLE sim_proxy ADD COLUMN proxy_app INTEGER DEFAULT 0; - -COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index 14f320a..d95bfc3 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -18,17 +18,13 @@ export type Models = { thread_parent: string | null custom_avatar: string | null last_bridged_pin_timestamp: number | null + speedbump_id: string | null + speedbump_webhook_id: string | null speedbump_checked: number | null guild_id: string | null custom_topic: number } - channel_speedbump: { - channel_id: string - speedbump_webhook_id: string - speedbump_user_id: string - } - direct: { mxid: string room_id: string @@ -48,7 +44,6 @@ export type Models = { event_subtype: string | null part: number reaction_part: number - /** 0 = Matrix, 1 = Discord */ source: number } @@ -142,8 +137,6 @@ export type Models = { user_id: string proxy_owner_id: string displayname: string - /** 0 = PluralKit, 1 = /plu/ral */ - proxy_app: number } webhook: { diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 36853cc..bd2f856 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -16,9 +16,9 @@ const utils = sync.require("../../matrix/utils") * @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters[2]}>} */ async function* _interact({data}, {api}) { - const events = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") - .select("event_id", "room_id").where({message_id: data.target_id}).and("ORDER BY reaction_part").all() - if (!events.length) { + const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index") + .select("event_id", "room_id").where({message_id: data.target_id}).get() + if (!row) { return yield {createInteractionResponse: { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { @@ -35,23 +35,17 @@ async function* _interact({data}, {api}) { } }} - const reactions = [] - for (const event of events) { - const reactionsOnEvent = await api.getFullRelations(event.room_id, event.event_id, "m.annotation") - reactions.push(...reactionsOnEvent) - } + const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation") - /** @type {Map>} reaction key (the emoji) -> displaynames */ + /** @type {Map} */ const inverted = new Map() for (const reaction of reactions) { if (utils.eventSenderIsFromDiscord(reaction.sender)) continue const key = reaction.content["m.relates_to"].key - const displayname = select("member_cache", "displayname", {mxid: reaction.sender, room_id: events[0].room_id}).pluck().get() || reaction.sender - if (!inverted.has(key)) { - inverted.set(key, new Set()) - } + const displayname = select("member_cache", "displayname", {mxid: reaction.sender, room_id: row.room_id}).pluck().get() || reaction.sender + if (!inverted.has(key)) inverted.set(key, []) // @ts-ignore - inverted.get(key).add(displayname) + inverted.get(key).push(displayname) } if (inverted.size === 0) { @@ -61,7 +55,7 @@ async function* _interact({data}, {api}) { } return yield {editOriginalInteractionResponse: { - content: [...inverted.entries()].sort((a, b) => b[1].size - a[1].size).map(([key, value]) => `${key} ⮞ ${[...value].join(" ⬩ ")}`).join("\n"), + content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"), }} } diff --git a/src/discord/interactions/reactions.test.js b/src/discord/interactions/reactions.test.js index 8d258f4..50ddeca 100644 --- a/src/discord/interactions/reactions.test.js +++ b/src/discord/interactions/reactions.test.js @@ -43,7 +43,7 @@ test("reactions: different response if nobody reacted", async t => { }) test("reactions: shows reactions if there are some, ignoring discord users", async t => { - let called = 0 + let called = 1 const msgs = await fromAsync(_interact({ data: { target_id: "1126786462646550579" @@ -51,7 +51,6 @@ test("reactions: shows reactions if there are some, ignoring discord users", asy }, { api: { async getFullRelations(roomID, eventID) { - called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") t.equal(eventID, "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg") return [{ @@ -98,57 +97,3 @@ test("reactions: shows reactions if there are some, ignoring discord users", asy ) t.equal(called, 1) }) - -test("reactions: combines reactions from multiple events", async t => { - let called = 0 - const msgs = await fromAsync(_interact({ - data: { - target_id: "1141501302736695316" - } - }, { - api: { - async getFullRelations(roomID, eventID) { - called++ - t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") - if (eventID === "$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA") { - return [{ - sender: "@cadence:cadence.moe", - content: { - "m.relates_to": { - key: "🐈", - rel_type: "m.annotation" - } - } - }, { - sender: "@rnl:cadence.moe", - content: { - "m.relates_to": { - key: "🐈", - rel_type: "m.annotation" - } - } - }] - } else if (eventID === "$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI") { - return [{ - sender: "@cadence:cadence.moe", - content: { - "m.relates_to": { - key: "🐈‍⬛", - rel_type: "m.annotation" - } - } - }] - } else { - t.fail(`tried to read reactions on wrong event ${eventID}`) - } - } - } - })) - t.equal(msgs.length, 2) - t.equal( - msgs[1].editOriginalInteractionResponse.content, - "🐈 ⮞ cadence [they] ⬩ @rnl:cadence.moe" - + "\n🐈‍⬛ ⮞ cadence [they]" - ) - t.equal(called, 2) -}) diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index dc97287..e3d58c4 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -16,7 +16,7 @@ const ping = sync.require("./interactions/ping.js") // User must have EVERY permission in default_member_permissions to be able to use the command function registerInteractions() { - discord.snow.interaction.editApplicationCommands(id, [{ + discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Matrix info", contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message, @@ -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 6fb0b43..0d400f1 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -182,18 +182,6 @@ function filterTo(xs, fn) { return filtered } -/** - * The parameters correspond to the columns of the channel_room table. - * @param {string} rowChannelID thread ID, OR channel ID if there is no thread - * @param {string | null | undefined} rowThreadParent channel ID if there is a thread - */ -function swapThreadID(rowChannelID, rowThreadParent) { - return { - channelID: rowThreadParent ? rowThreadParent : rowChannelID, - threadID: rowThreadParent ? rowChannelID : undefined - } -} - const supportedPlaintextPreviewExtensions = new Set([ "4d", "abnf", @@ -594,5 +582,4 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact module.exports.getPublicUrlForCdn = getPublicUrlForCdn module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage module.exports.filterTo = filterTo -module.exports.swapThreadID = swapThreadID module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions diff --git a/src/m2d/actions/emoji-sheet.js b/src/m2d/actions/emoji-sheet.js index 077240c..ed5ab88 100644 --- a/src/m2d/actions/emoji-sheet.js +++ b/src/m2d/actions/emoji-sheet.js @@ -23,7 +23,7 @@ async function getAndConvertEmoji(mxc) { const res = await api.getMedia(mxc, {signal: abortController.signal}) if (res.status !== 200) { const root = await res.json() - throw new mreq.MatrixServerError(root, res.status, {mxc}) + throw new mreq.MatrixServerError(root, {mxc}) } const readable = stream.Readable.fromWeb(res.body) return emojiSheetConverter.convertImageStream(readable, () => { diff --git a/src/m2d/actions/sticker.js b/src/m2d/actions/sticker.js index f5a1e16..8eeb5d2 100644 --- a/src/m2d/actions/sticker.js +++ b/src/m2d/actions/sticker.js @@ -22,7 +22,7 @@ async function getAndResizeSticker(mxc) { const res = await api.getMedia(mxc) if (res.status !== 200) { const root = await res.json() - throw new mreq.MatrixServerError(root, res.status, {mxc}) + throw new mreq.MatrixServerError(root, {mxc}) } const streamIn = Readable.fromWeb(res.body) diff --git a/src/m2d/actions/typing.js b/src/m2d/actions/typing.js deleted file mode 100644 index c56ce4a..0000000 --- a/src/m2d/actions/typing.js +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-check - -const {discord} = require("../../passthrough") - -/** @type {Set} channel ids that are currently typing */ -const isTyping = new Set() -/** @type {Map} channel id -> interval */ -const intervals = new Map() - -/** @param {string} channelID */ -function startTyping(channelID) { - isTyping.add(channelID) - // Start a new typing session if needed - if (!intervals.has(channelID)) { - intervals.set(channelID, setInterval(refreshTyping, 8e3, channelID)) - discord.snow.channel.startChannelTyping(channelID).catch(() => {}) - } -} - -/** @param {string} channelID */ -function refreshTyping(channelID) { - if (isTyping.has(channelID)) { - // Continue typing session - discord.snow.channel.startChannelTyping(channelID).catch(() => {}) - } else { - // End typing session - clearInterval(intervals.get(channelID)) - intervals.delete(channelID) - } -} - -/** @param {string} channelID */ -function stopTyping(channelID) { - // Schedule typing session to end - isTyping.delete(channelID) -} - -module.exports.startTyping = startTyping -module.exports.stopTyping = stopTyping diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js index 1b3f1bd..1ff2bb9 100644 --- a/src/m2d/actions/update-pins.js +++ b/src/m2d/actions/update-pins.js @@ -16,9 +16,9 @@ async function updatePins(pins, prev) { .select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get() if (!row) continue if (added) { - discord.snow.channel.createChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") + discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") } else { - discord.snow.channel.deleteChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix") + discord.snow.channel.removeChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix") } } } diff --git a/src/m2d/actions/vote.js b/src/m2d/actions/vote.js index 84f8cc7..926b957 100644 --- a/src/m2d/actions/vote.js +++ b/src/m2d/actions/vote.js @@ -1,12 +1,18 @@ // @ts-check const Ty = require("../../types") +const DiscordTypes = require("discord-api-types/v10") +const {Readable} = require("stream") const assert = require("assert").strict +const crypto = require("crypto") const passthrough = require("../../passthrough") -const {sync, db, select} = passthrough +const {sync, discord, db, select} = passthrough -/** @type {import("../../discord/utils")} */ -const dUtils = sync.require("../../discord/utils") +const {reg} = require("../../matrix/read-registration") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** @type {import("../converters/poll-components")} */ const pollComponents = sync.require("../converters/poll-components") /** @type {import("./channel-webhook")} */ @@ -27,11 +33,10 @@ async function updateVote(event) { // If poll was started on Matrix, the Discord version is using components, so we can update that to the current status if (messageRow.source === 0) { - const row = select("channel_room", ["channel_id", "thread_parent"], {room_id: event.room_id}).get() - assert(row) - const {channelID, threadID} = dUtils.swapThreadID(row.channel_id, row.thread_parent) - await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID), threadID) + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + assert(channelID) + await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID)) } } -module.exports.updateVote = updateVote +module.exports.updateVote = updateVote \ No newline at end of file diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 18b9848..0a18a14 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -10,7 +10,6 @@ const domino = require("domino") const assert = require("assert").strict const entities = require("entities") const pb = require("prettier-bytes") -const mimeTypes = require("mime-types") const {tag} = require("@cloudrac3r/html-template-tag") const passthrough = require("../../passthrough") @@ -98,6 +97,15 @@ turndownService.addRule("underline", { } }) +turndownService.addRule("blockquote", { + filter: "blockquote", + replacement: function (content) { + content = content.replace(/^\n+|\n+$/g, "") + content = content.replace(/^/gm, "> ") + return content + } +}) + turndownService.addRule("spoiler", { filter: function (node, options) { return node.tagName === "SPAN" && node.hasAttribute("data-mx-spoiler") @@ -614,20 +622,13 @@ async function eventToMessage(event, guild, channel, di) { captionContent.addLine(`(Spoiler: ${fileSpoilerReason})`) } - // If filename is not specified, create one from mimetype - let filename = event.content.filename || event.content.body - if (!filename) { - const extension = mimeTypes.extension(event.content.info?.mimetype) - if (!extension) throw new Error("Filename and mimetype missing. Please report this bug to your client.") - filename = `file.${extension}` - } - // File link as alternative to uploading if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) { // Upload (unencrypted) file as link, because it's too large for Discord // Do this by constructing a sample Matrix message with the link and then use the text processor to convert that + the original caption. const url = mxUtils.getPublicUrlForMxc(event.content.url) assert(url) + const filename = event.content.filename || event.content.body const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : "" if (fileIsSpoiler) { captionContent.addLine(`${emoji}Uploaded SPOILER file: <${url}> (${pb(event.content.info.size)})`, tag`${emoji}Uploaded SPOILER file: ${filename} (${pb(event.content.info.size)})`) // the space is necessary to work around a bug in Discord's URL previewer. the preview still gets blurred in the client. @@ -636,6 +637,7 @@ async function eventToMessage(event, guild, channel, di) { } } else { // Upload file as file + let filename = event.content.filename || event.content.body if (fileIsSpoiler) filename = "SPOILER_" + filename if ("file" in event.content) { // Encrypted @@ -773,14 +775,14 @@ async function eventToMessage(event, guild, channel, di) { repliedToEvent.content = repliedToEvent.content["m.new_content"] } /** @type {string} */ - let originalRepliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body + let repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body const fileReplyContentAlternative = attachmentEmojis.get(repliedToEvent.content.msgtype) let contentPreview if (fileReplyContentAlternative) { contentPreview = " " + fileReplyContentAlternative } else if (repliedToEvent.unsigned?.redacted_because) { contentPreview = " (in reply to a deleted message)" - } else if (typeof originalRepliedToContent !== "string") { + } else if (typeof repliedToContent !== "string") { // in reply to a weird metadata event like m.room.name, m.room.member... // I'm not implementing text fallbacks for arbitrary room events. this should cover most cases // this has never ever happened in the wild anyway @@ -788,7 +790,6 @@ async function eventToMessage(event, guild, channel, di) { contentPreview = " (channel details edited)" } else { // Generate a reply preview for a standard message - let repliedToContent = originalRepliedToContent repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line @@ -803,18 +804,7 @@ async function eventToMessage(event, guild, channel, di) { const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { contentPreview = ": " + contentPreviewChunks[0] - contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, url => { - const originalUrlIndex = originalRepliedToContent.indexOf(url) - if (originalUrlIndex !== -1 && originalRepliedToContent[originalUrlIndex + url.length]?.match(/[^ )>"'`]/)) { // URL was truncated by chunking, replace it - try { - const u = new URL(url) - return `` - } catch (e) { - return `` - } - } - return `<${url}>` // Full URL is present, escape it - }) + contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, "<$&>") if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." } else { contentPreview = "" @@ -865,7 +855,7 @@ async function eventToMessage(event, guild, channel, di) { input = input.replace(/("https:\/\/matrix.to.*?<\/a>):?/g, "$1") // Element adds a bunch of
before
but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those. - input = input.replace(/(?:\n|
\s*)*<\/blockquote>(?:\n|
\s*)*/g, "") + input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "") // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason. // But I should not count it if it's between block elements. diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 5dd0af7..650e442 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -855,40 +855,6 @@ test("event2message: html lines are bridged correctly", async t => { ) }) -test("event2message: breaks between consecutive blockquotes are preserved", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: "this was inspired by someone telling me\n\n>betrothed is an insane vocab pull\n\n> i think thats part of why im so scared of cadence\n\n> if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up", - format: "org.matrix.custom.html", - formatted_body: "

this was inspired by someone telling me

\n
\n

betrothed is an insane vocab pull

\n
\n
\n

i think thats part of why im so scared of cadence

\n
\n
\n

if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up

\n
" - }, - origin_server_ts: 1783571303430, - event_id: "$cdltlkLTUp-ma2uYmLKy7o3RkDgpDbtdYfngST_igWE", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - allowed_mentions: { - parse: ["users", "roles"] - }, - username: "cadence [they]", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", - content: "this was inspired by someone telling me" - + "\n\n> betrothed is an insane vocab pull" - + "\n\n> i think thats part of why im so scared of cadence" - + "\n\n> if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up" - }] - } - ) -}) - /*test("event2message: whitespace is retained", async t => { t.deepEqual( await eventToMessage({ @@ -1328,7 +1294,7 @@ test("event2message: characters are encoded properly in code blocks", async t => ) }) -test("event2message: quotes have an appropriate amount of whitespace (br following)", async t => { +test("event2message: quotes have an appropriate amount of whitespace", async t => { t.deepEqual( await eventToMessage({ content: { @@ -1362,40 +1328,6 @@ test("event2message: quotes have an appropriate amount of whitespace (br followi ) }) -test("event2message: quotes have an appropriate amount of whitespace (p following)", async t => { - t.deepEqual( - await eventToMessage({ - content: { - msgtype: "m.text", - body: "wrong body", - format: "org.matrix.custom.html", - formatted_body: "
\n

short story

\n
\n

by whose standards? LOL

\n

I basically went all the way back to childhood (which analysts always like you to do)

\n
\n

laughed out loud

" - }, - event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", - origin_server_ts: 1688301929913, - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", - sender: "@cadence:cadence.moe", - type: "m.room.message", - unsigned: { - age: 405299 - } - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "cadence [they]", - content: "> short story\nby whose standards? LOL\n\n> I basically went all the way back to childhood (which analysts always like you to do)\nlaughed out loud", - avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - test("event2message: lists have appropriate line breaks", async t => { t.deepEqual( await eventToMessage({ @@ -2117,57 +2049,6 @@ test("event2message: should suppress embeds for links in reply preview", async t ) }) -test("event2message: truncated links in reply preview should be unlinked to prevent broken links", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: `> <@_ooye_.wing.:cadence.moe> https://huggingface.co/blog/security-incident-july-2026\n\nthis is either written by claude or somebody who's too claude-brained and I just can't get through it`, - format: "org.matrix.custom.html", - formatted_body: `
In reply to @_ooye_.wing.:cadence.moe
https://huggingface.co/blog/security-incident-july-2026
this is either written by claude or somebody who's too claude-brained and I just can't get through it`, - "m.relates_to": { - "m.in_reply_to": { - event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU" - } - } - }, - event_id: "$0Bs3rbsXaeZmSztGMx1NIsqvOrkXOpIWebN-dqr09i4", - room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" - }, data.guild.general, data.channel.general, { - api: { - getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU", { - "type": "m.room.message", - "sender": "@_ooye_.wing.:cadence.moe", - "content": { - "m.mentions": {}, - "msgtype": "m.text", - "body": "", - "format": "org.matrix.custom.html", - "formatted_body": "https://huggingface.co/blog/security-incident-july-2026" - } - }) - } - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "cadence [they]", - content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 <@112890272819507200>:" - + " ..." - + `\nthis is either written by claude or somebody who's too claude-brained and I just can't get through it`, - avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - test("event2message: should include a reply preview when message ends with a blockquote", async t => { t.deepEqual( await eventToMessage({ diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 0560f46..3580d1b 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -24,8 +24,6 @@ const vote = sync.require("./actions/vote") const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") /** @type {import("../matrix/utils")} */ const utils = sync.require("../matrix/utils") -/** @type {import("../matrix/mreq")}) */ -const mreq = sync.require("../matrix/mreq") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") /** @type {import("../d2m/actions/create-room")} */ @@ -34,10 +32,6 @@ const createRoom = sync.require("../d2m/actions/create-room") const roomUpgrade = require("../matrix/room-upgrade") /** @type {import("../d2m/actions/retrigger")} */ const retrigger = sync.require("../d2m/actions/retrigger") -/** @type {import("../matrix/homeserver-status")} */ -const homeserverStatus = sync.require("../matrix/homeserver-status") -/** @type {import("./actions/typing")} */ -const typing = sync.require("./actions/typing") const {reg} = require("../matrix/read-registration") let lastReportedEvent = 0 @@ -172,15 +166,7 @@ async function sendError(roomID, source, type, e, payload) { key: "🔁" } }) - } catch (e) { - if ( - e.toString().includes("fetch failed") - || (e instanceof mreq.MatrixServerError && [502, 503].includes(e.httpStatus) && e.errcode === "CX_SERVER_ERROR") - ) { - // Matrix homeserver is down (reverse proxy indicated failure) - homeserverStatus.homeserverStatus.setErrorWithPacket(payload) - } - } + } catch (e) {} } function guard(type, fn) { @@ -529,20 +515,6 @@ async event => { await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.") })) -sync.addTemporaryListener(as, "ephemeral_type:m.typing", guard("m.typing", -/** - * @param {Ty.Event.Outer_M_Typing} event - */ -async event => { - const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - if (!channelID) return - if (event.content.user_ids.some(mxid => !utils.eventSenderIsFromDiscord(mxid))) { - typing.startTyping(channelID) - } else { - typing.stopTyping(channelID) - } -})) - module.exports.stringifyErrorStack = stringifyErrorStack module.exports.cleanErrorStack = cleanErrorStack module.exports.sendError = sendError diff --git a/src/matrix/api.js b/src/matrix/api.js index 47fc565..9b7f280 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -452,8 +452,7 @@ async function ping() { headers: { Authorization: `Bearer ${reg.as_token}` }, - body: "{}", - signal: AbortSignal.timeout(15e3) + body: "{}" }) const root = await res.json() return { @@ -475,7 +474,7 @@ async function ping() { async function getMedia(mxc, init = {}) { init = {...init} - const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/) + const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) assert(mediaParts) let route = "download" diff --git a/src/matrix/homeserver-status.js b/src/matrix/homeserver-status.js deleted file mode 100644 index 3ceac84..0000000 --- a/src/matrix/homeserver-status.js +++ /dev/null @@ -1,134 +0,0 @@ -// @ts-check - -const assert = require("assert") -const Denque = require("denque") -const StateMachine = require("snowtransfer").StateMachine - -const passthrough = require("../passthrough") -const {sync} = passthrough -/** @type {import("../d2m/discord-packets")} */ -const discordPackets = sync.require("../d2m/discord-packets") -/** @type {import("../matrix/api")} */ -const api = sync.require("../matrix/api") - -const DEBUG_HOMESERVER_STATUS = false - -function debugHomeserverStatus(message) { - if (DEBUG_HOMESERVER_STATUS) { - console.log(message) - } -} - -const homeserverStatus = new class HomeserverStatus { - constructor() { - /** @private */ - this.queue = new Denque() - - /** @private */ - this.pingInterval = undefined - - /** @private */ - this.sm = new StateMachine("online") - .defineState("online") - - .defineState("checking", { - onEnter: [async () => { - const pingResult = await api.ping().catch(e => ({ok: false})) - if (pingResult.ok) { - this.sm.doTransition("check ok") - } else { - this.sm.doTransition("check fail") - } - }], - onLeave: [], - transitions: new Map() - }) - - .defineState("offline", { - onEnter: [() => { - this.pingInterval = setInterval(async () => { - const pingResult = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) - if (pingResult.ok) { - this.sm.doTransition("ping ok") - } - }, 15e3) - }], - onLeave: [() => { - clearInterval(this.pingInterval) - }], - transitions: new Map() - }) - - .defineState("recovering", { - onEnter: [async () => { // Drain queue. - while (!this.queue.isEmpty()) { - const packet = this.queue.peekFront() // same position as .shift() - debugHomeserverStatus(`homeserver status: ${new Date().toISOString()} dq packet ${packet.t} ${packet.d?.content}`) - await discordPackets.dispatchPacketToBridge(passthrough.discord, packet) - if (this.sm.currentStateName !== "recovering") return // got kicked out due to another error - this.queue.shift() - } - this.sm.doTransition("recovered") - }], - onLeave: [], - transitions: new Map() - }) - - .defineUniversalTransition("error", "offline") - .defineTransition("offline", "ping ok", "recovering") - .defineTransition("recovering", "recovered", "online") - .defineTransition("online", "check", "checking") - .defineTransition("checking", "check ok", "recovering") - .defineTransition("checking", "check fail", "offline") - - this.sm.on("enter", st => debugHomeserverStatus(`homeserver status: ${st}`)) - this.sm.setMaxListeners(101) - this.sm.freeze() - } - - isRealTime() { - return this.sm.currentStateName === "online" - } - - /** @param {boolean} forceCheck */ - waitForOnline(forceCheck) { - const onlinePromise = new Promise(resolve => { - // Already online? Start check or just done - if (this.sm.currentStateName === "online") { - if (forceCheck) { - this.sm.doTransition("check") - } else { - return resolve(null) - } - } - - // Checking or not online. Wait for online. - const onlineListener = stateName => { - if (stateName === "online") { - this.sm.removeListener("enter", onlineListener) - resolve(null) - } - } - this.sm.on("enter", onlineListener) - }) - return onlinePromise - } - - /** - * When offline or recovering, call this for incoming packets to queue them to be sent in order later. - */ - queuePacket(packet) { - assert(["offline", "recovering"].includes(this.sm.currentStateName)) - this.queue.push(packet) - } - - setErrorWithPacket(packet) { - const wasRecovering = this.sm.currentStateName === "recovering" - this.sm.doTransition("error") - if (!wasRecovering) { // if was recovering then packet is already in the right place in queue - this.queuePacket(packet) - } - } -} - -module.exports.homeserverStatus = homeserverStatus diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js index b6bc5b9..bb59506 100644 --- a/src/matrix/mreq.js +++ b/src/matrix/mreq.js @@ -9,14 +9,9 @@ const {reg} = require("./read-registration.js") const baseUrl = `${reg.ooye.server_origin}/_matrix` class MatrixServerError extends Error { - /** - * @param {number} httpStatus} - */ - constructor(data, httpStatus, opts = {}) { + constructor(data, opts) { super(data.error || data.errcode) this.data = data - /** @type {number} */ - this.httpStatus = httpStatus /** @type {string} */ this.errcode = data.errcode this.opts = opts @@ -49,11 +44,11 @@ async function _convertBody(body) { async function makeMatrixServerError(res, opts = {}) { delete opts.headers?.["Authorization"] if (res.headers.get("content-type") === "application/json") { - return new MatrixServerError(await res.json(), res.status, opts) + return new MatrixServerError(await res.json(), opts) } else if (res.headers.get("content-type")?.startsWith("text/")) { - return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, res.status, opts) + return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts) } else { - return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, res.status, opts) + return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts) } } @@ -83,12 +78,12 @@ async function mreq(method, url, bodyIn, extra = {}) { var root = JSON.parse(text) } catch (e) { delete opts.headers?.["Authorization"] - throw new MatrixServerError(text, res.status, {baseUrl, url, ...opts}) + throw new MatrixServerError(text, {baseUrl, url, ...opts}) } if (!res.ok || root.errcode) { delete opts.headers?.["Authorization"] - throw new MatrixServerError(root, res.status, {baseUrl, url, ...opts}) + throw new MatrixServerError(root, {baseUrl, url, ...opts}) } return root } diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index e6ca53f..d1243a7 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -17,10 +17,6 @@ function checkRegistration(reg) { assert(reg.ooye?.server_origin.match(/^https?:\/\//), "server origin must start with http or https") assert.notEqual(reg.ooye?.server_origin.slice(-1), "/", "server origin must not end in slash") assert.match(reg.url, /^https?:/, "url must start with http:// or https://") - if (!reg.receive_ephemeral) { - reg.receive_ephemeral = true - writeRegistration(reg) - } } /* c8 ignore next 4 */ @@ -54,7 +50,6 @@ function getTemplateRegistration(serverName) { ], sender_localpart: `${namespace_prefix}bot`, rate_limited: false, - receive_ephemeral: true, socket: 6693, ooye: { namespace_prefix, diff --git a/src/matrix/utils.js b/src/matrix/utils.js index fed71c9..eee635b 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -250,7 +250,7 @@ function getPublicUrlForMxc(mxc) { */ function makeMxcPublic(mxc) { assert(hasher, "xxhash is not ready yet") - const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/) + const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) if (!mediaParts) return undefined const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}` diff --git a/src/types.d.ts b/src/types.d.ts index f65ce59..be037ca 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -18,7 +18,6 @@ export type AppServiceRegistrationConfig = { } protocols: [string] rate_limited: boolean - receive_ephemeral: boolean socket?: string | number, ooye: { namespace_prefix: string @@ -36,7 +35,6 @@ export type AppServiceRegistrationConfig = { web_password: string time_zone?: string receive_presences: boolean - plu_ral_api_key?: string } old_bridge?: { as_token: string @@ -61,7 +59,6 @@ export type InitialAppServiceRegistrationConfig = { } protocols: [string] rate_limited: boolean - receive_ephemeral: boolean socket?: string | number, ooye: { namespace_prefix: string @@ -125,28 +122,6 @@ export type PkMessage = { sender: string } -export type PluRalWebhookMessage = { - original_id: string | null - proxy_id: string - author_id: string - channel_id: string - member_id: string - reason: string - webhook_id: string - member: PluRalMember -} - -export type PluRalMember = { - id: string - name: string - pronouns: string - bio: string - birthday: string - color: number | null - avatar_url: string - private: boolean -} - export namespace Event { export type Outer = { type: string @@ -421,14 +396,6 @@ export namespace Event { rotation_period_ms?: number rotation_period_msgs?: number } - - export type Outer_M_Typing = { - content: { - user_ids: string[] - } - room_id: string - type: "m.typing" - } } export namespace R { diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 7411a1e..9791ae3 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -122,7 +122,7 @@ block body #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible .s-popover--arrow.s-popover--arrow__tc +add-roles-menu(guild, guild_id) - p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. + p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. h3.mt32.fs-category Features .s-card.d-grid.px0.g16 @@ -191,14 +191,14 @@ block body label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id) +discord(channel, true, "Announcement") else - .s-empty-state.p8 No Discord channels available. + .s-empty-state.p8 All Discord channels are linked. .fl-grow1.s-btn-group.fd-column.w30 each room in unlinkedRooms input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id) label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id) +matrix(room, true) else - .s-empty-state.p8 No Matrix rooms available. + .s-empty-state.p8 All Matrix rooms are linked. input(type="hidden" name="guild_id" value=guild_id) div button.s-btn.s-btn__icon.s-btn__filled#link-button diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index 2664520..6d2772b 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -98,11 +98,6 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn setResponseHeader(event, "Content-Type", contentType) setResponseHeader(event, "Transfer-Encoding", "chunked") - const contentDisposition = res.headers.get("content-disposition") - if (contentDisposition) { - setResponseHeader(event, "Content-Disposition", contentDisposition) - } - if (res.ok && query.success) { return MEDIA_THUMBNAIL_PRESETS[query.data.preset](res.body) } else { diff --git a/src/web/routes/download-matrix.test.js b/src/web/routes/download-matrix.test.js index 76e8152..610a62d 100644 --- a/src/web/routes/download-matrix.test.js +++ b/src/web/routes/download-matrix.test.js @@ -30,13 +30,12 @@ test("web download matrix: works if a known attachment", async t => { api: { // @ts-ignore async getMedia(mxc, init) { - return new Response("", {status: 200, headers: {"content-type": "image/png", "content-disposition": `attachment; filename="wa.png"`}}) + return new Response("", {status: 200, headers: {"content-type": "image/png"}}) } } }) t.equal(event.node.res.statusCode, 200) t.equal(event.node.res.getHeader("content-type"), "image/png") - t.equal(event.node.res.getHeader("content-disposition"), `attachment; filename="wa.png"`) }) /** @@ -65,7 +64,7 @@ test("web sheet: single emoji", async t => { mxcDownloader: mockGetAndConvertEmoji }) t.equal(event.node.res.statusCode, 200) - t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVRogc1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH") + t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH") }) test("web sheet: multiple sources", async t => { diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index 32f89bf..0182093 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -73,7 +73,7 @@ test("web link space: check that OOYE is joined", async t => { api: { async joinRoom(roomID) { called++ - throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400) + throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}) } } })) @@ -368,7 +368,7 @@ test("web link room: check that bridge can join room (notices lack of via and as api: { async joinRoom(roomID) { called++ - throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400) + throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}) }, async *generateFullHierarchy(spaceID) { called++ @@ -402,7 +402,7 @@ test("web link room: check that bridge can join room (uses via for join attempt) async joinRoom(roomID, _, via) { called++ t.deepEqual(via, ["cadence.moe", "hashi.re"]) - throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400) + throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}) }, async *generateFullHierarchy(spaceID) { called++ @@ -710,7 +710,7 @@ test("web unlink room: checks that the channel is bridged", async t => { })) t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged") - db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_checked, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_checked, row.guild_id, row.custom_topic) + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic) const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get() t.deepEqual(row, new_row) }) diff --git a/start.js b/start.js index 51942c6..39e8ea0 100755 --- a/start.js +++ b/start.js @@ -31,9 +31,8 @@ sync.require("./src/m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) - process.stdout.write("Connecting to Discord... ") await discord.cloud.connect() - console.log("ok.") + console.log("Discord gateway started") sync.require("./src/web/server") await power.applyPower() diff --git a/test/data.js b/test/data.js index eab9a63..f3092bc 100644 --- a/test/data.js +++ b/test/data.js @@ -5473,189 +5473,6 @@ module.exports = { content: '-# Original Message ID: 1466556003645657118 · ' } ] - }, - pk_ping_components_v1: { - type: 23, - content: "Psst, **Red** (<@772659086046658620>), you have been pinged by <@772659086046658620>.", - mentions: [ - { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - } - ], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2026-03-25T07:07:02.626000+00:00", - edited_timestamp: null, - flags: 0, - components: [ - { - type: 1, - id: 1, - components: [ - { - type: 2, - id: 2, - style: 5, - label: "Jump", - url: "https://discord.com/channels/1160893336324931584/1160894080998461480/1440549403667468320" - } - ] - } - ], - id: "1486260105908457653", - channel_id: "1160894080998461480", - author: { - id: "466378653216014359", - username: "PluralKit", - avatar: "b78ef67a081737a830b60aa47d9ebcd9", - discriminator: "4020", - public_flags: 65536, - flags: 65536, - bot: true, - banner: null, - accent_color: null, - global_name: null, - 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, - application_id: "466378653216014359", - interaction: { - id: "1486260103928614932", - type: 2, - name: "🔔 Ping author", - user: { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - } - }, - webhook_id: "466378653216014359", - message_reference: { - type: 0, - channel_id: "1160894080998461480", - message_id: "1440549403667468320", - guild_id: "1160893336324931584" - }, - interaction_metadata: { - id: "1486260103928614932", - type: 2, - user: { - id: "772659086046658620", - username: "cadence.worm", - avatar: "466df0c98b1af1e1388f595b4c1ad1b9", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - collectibles: null, - display_name_styles: null, - banner_color: null, - clan: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - }, - primary_guild: { - identity_guild_id: "532245108070809601", - identity_enabled: true, - tag: "doll", - badge: "dba08126b4e810a0e096cc7cd5bc37f0" - } - }, - authorizing_integration_owners: { "0": "1160893336324931584" }, - name: "🔔 Ping author", - command_type: 3, - target_message_id: "1440549403667468320" - }, - referenced_message: { - type: 0, - content: "test", - mentions: [], - mention_roles: [], - attachments: [], - embeds: [], - timestamp: "2025-11-19T03:49:01.948000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - id: "1440549403667468320", - channel_id: "1160894080998461480", - author: { - id: "1195662438662680720", - username: "special name", - avatar: "a82347890f2739e5880cd82b8c1a708e", - discriminator: "0000", - public_flags: 0, - flags: 0, - bot: true, - global_name: null, - clan: null, - primary_guild: null - }, - pinned: false, - mention_everyone: false, - tts: false, - application_id: "466378653216014359", - webhook_id: "1195662438662680720" - } } }, message_update: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1662320..8dd71cd 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -95,8 +95,7 @@ WITH a (message_id, channel_id) AS (VALUES ('1381212840957972480', '112760669178241024'), ('1401760355339862066', '112760669178241024'), ('1439351590262800565', '1438284564815548418'), -('1404133238414376971', '112760669178241024'), -('1440549403667468320', '1160894080998461480')) +('1404133238414376971', '112760669178241024')) SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id; INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES @@ -144,8 +143,7 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0), ('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0), ('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1), -('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1), -('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1); +('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),