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.
+----
+ This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO`
+ formatted_body: ` hthrflwrs on cohost`
+ + ` This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO`
+ ` 1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:`
+ `
# Appendix
diff --git a/package-lock.json b/package-lock.json
index 986e6ca..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.9",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
- "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+ "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.53",
- "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.53.tgz",
- "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==",
+ "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-room.js b/src/d2m/actions/create-room.js
index a6230b2..7f110ad 100644
--- a/src/d2m/actions/create-room.js
+++ b/src/d2m/actions/create-room.js
@@ -163,7 +163,6 @@ async function channelToKState(channel, guild, di) {
events_default: everyoneCanSend ? 0 : READ_ONLY_ROOM_EVENTS_DEFAULT_POWER,
events: {
"m.reaction": 0,
- "org.matrix.msc3381.poll.response": 0,
"m.room.redaction": 0, // only affects redactions of own events, required to be able to un-react
...pollStartPowerLevel
},
diff --git a/src/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js
index fec5c55..c9e098b 100644
--- a/src/d2m/actions/create-room.test.js
+++ b/src/d2m/actions/create-room.test.js
@@ -147,7 +147,6 @@ test("channel2room: read-only discord channel", async t => {
events_default: 50, // <-- it should be read-only!
events: {
"m.reaction": 0,
- "org.matrix.msc3381.poll.response": 0,
"m.room.redaction": 0
},
notifications: {
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 e68d2cc..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")
@@ -103,7 +103,6 @@ async function memberToStateContent(pkMessage, 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 = pkMessage.member.avatar_url || pkMessage.member.webhook_avatar_url || pkMessage.system.avatar_url || file.userAvatar(author)
- const color = pkMessage.member.color ?? pkMessage.system.color
const content = {
displayname: author.username,
@@ -111,12 +110,6 @@ async function memberToStateContent(pkMessage, author) {
"moe.cadence.ooye.pk_member": pkMessage.member
}
if (avatar) content.avatar_url = await file.uploadDiscordFileToMxc(avatar)
- if (color) {
- content["eu.she-a.color"] = {
- on_dark: `#${color}`,
- on_light: `#${color}`
- }
- }
return content
}
@@ -139,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
${html}` 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 authorPsst, Red (@cadence.worm), you have been pinged by @cadence.worm.`, - "m.mentions": {} - }]) -}) - test("message2event embeds: 4 images", async t => { const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general) t.deepEqual(events, [{ @@ -282,7 +254,9 @@ test("message2event embeds: vx image", async t => { }, { $type: "m.room.message", msgtype: "m.notice", - body: "| ## Twitter https://twitter.com/tomorrowcorp/status/1760330671074287875" + body: "| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix" + + "\n| " + + "\n| ## Twitter https://twitter.com/tomorrowcorp/status/1760330671074287875" + "\n| " + "\n| ## Tomorrow Corporation (@TomorrowCorp) https://vxtwitter.com/TomorrowCorp/status/1760330671074287875" + "\n| " @@ -292,16 +266,15 @@ test("message2event embeds: vx image", async t => { + "\n| " + "\n| ๐ 123 ๐ 36" + "\n| " - + "\n| ๐ธ https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg" - + "\n| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix", + + "\n| ๐ธ https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg", format: "org.matrix.custom.html", - formatted_body: `This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO` - + `
1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:` - + `
hthrflwrs on cohost โ 29/2/2024, 2:17 PM
- Both players draw eight cards` - + `
- Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand` - + `
- Both players present their best five-or-less-card pok...
`, "m.mentions": {} }]) }) @@ -318,16 +291,16 @@ test("message2event embeds: vx video", async t => { }, { $type: "m.room.message", msgtype: "m.notice", - body: "| ## McDonaldโs๐ค@studiopierrot" + body: "| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix" + + "\n| \n| ## McDonaldโs๐ค@studiopierrot" + "\n| \n| ๐ 89 ๐ 21 https://twitter.com/McDonalds/status/1759971752254341417" + "\n| \n| ## McDonald's (@McDonalds) https://vxtwitter.com/McDonalds/status/1759971752254341417" - + "\n| \n| ๐๏ธ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12" - + "\n| via vxTwitter / fixvx https://github.com/dylanpdx/BetterTwitFix", + + "\n| \n| ๐๏ธ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12", format: "org.matrix.custom.html", - formatted_body: `Twitter` + formatted_body: `
`, + + `vxTwitter / fixvx` + + `
Twitter` + `
Tomorrow Corporation (@TomorrowCorp)` + `
Mark your calendar with a wet black stain! World of Goo 2 releases on May 23, 2024 on Nintendo Switch, Epic Games Store (Win/Mac), and http://WorldOfGoo2.com (Win/Mac/Linux).` + `
https://tomorrowcorporation.com/posts/world-of-goo-2-now-with-100-more-release-dates-and-platforms` + `
๐ 123 ๐ 36` - + `๐ธ https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg
` - + `vxTwitter / fixvx๐ธ https://pbs.twimg.com/media/GG3zUMGbIAAxs3h.jpg
`, "m.mentions": {} }]) }) @@ -344,7 +317,8 @@ test("message2event embeds: youtube video", async t => { }, { $type: "m.room.message", msgtype: "m.notice", - body: "| ## Happy O Funny https://www.youtube.com/channel/UCEpQ9aEb1NafpvWp5Aoizrg" + body: "| via YouTube https://www.youtube.com" + + "\n| \n| ## Happy O Funny https://www.youtube.com/channel/UCEpQ9aEb1NafpvWp5Aoizrg" + "\n| \n| ## Shoebill stork clattering sounds like machine guun~!! (Japan Matsue... https://www.youtube.com/watch?v=kDMHHw8JqLE" + "\n| \n| twitter" + "\n| https://twitter.com/matsuevogelpark" @@ -353,7 +327,8 @@ test("message2event embeds: youtube video", async t => { + "\n| \n| #shoebill #livingdinosaur #happyofunny #weirdcreature #weirdsoun..." + "\n| \n| ๐๏ธ https://www.youtube.com/embed/kDMHHw8JqLE", format: "org.matrix.custom.html", - formatted_body: `McDonaldโs๐ค@studiopierrot\n\n๐ 89 ๐ 21` + formatted_body: `
`, + + `vxTwitter / fixvx` + + `
McDonaldโs๐ค@studiopierrot\n\n๐ 89 ๐ 21` + `
McDonald's (@McDonalds)` - + `
๐๏ธ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12
` - + `vxTwitter / fixvx๐๏ธ https://video.twimg.com/ext_tw_video/1759967449548541952/pu/vid/avc1/1280x720/XN1LFIJqAFBdtaoh.mp4?tag=12
") // 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. @@ -900,12 +890,6 @@ async function eventToMessage(event, guild, channel, di) { assert(root) async function forEachNode(event, node) { for (; node; node = node.nextSibling) { - // Don't process mx-reply further - if (node.nodeType === 1 && node.tagName === "MX-REPLY") { - node.remove() - continue - } - // Check written mentions if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) { const result = await checkWrittenMentions(node.nodeValue, event.sender, event.room_id, guild, di) 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: "Happy O Funny` + formatted_body: `
` + + `") + input = input.replace(/(?:\n|Happy O Funny` + `
Shoebill stork clattering sounds like machine guun~!! (Japan Matsue...` + `
https://twitter.com/matsuevogelpark
The shoebill (Balaeniceps rex) also known as whalehead, whale-headed stork, or shoe-billed stork, is a very large stork-like bird. It derives its name from its enormous shoe-shaped bill
some people also called them the living dinosaur~~
#shoebill #livingdinosaur #happyofunny #weirdcreature #weirdsoun...` + `๐๏ธ https://www.youtube.com/embed/kDMHHw8JqLE` diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 0742c7b..be1d99f 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1612,7 +1612,7 @@ test("message2event: expired/invalid invites are sent as-is", async t => { snow: { invite: { async getInvite() { - throw new Error(`Unknown Invite`) + throw new Error(`{"message": "Unknown Invite", "code": 10006}`) } } } 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 02cb20f..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 @@ -107,7 +107,7 @@ module.exports = { try { messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 100}) } catch (e) { - if (e.message === "Missing Access") { + if (e.message === `{"message": "Missing Access", "code": 50001}`) { // pathetic error handling from SnowTransfer console.log(`[check missed messages] no permissions to look back in channel ${channel.name} (${channel.id})`) continue // Sucks. } else { @@ -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) }, @@ -297,7 +298,6 @@ module.exports = { */ async MESSAGE_CREATE(client, message) { if (message.author.username === "Deleted User") return // Nothing we can do for deleted users. - if (select("opt_out", "user_id", {user_id: message.author.id}).pluck().get()) return // This user opted out. 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. @@ -313,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) }, @@ -334,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)) }, /** @@ -362,7 +360,6 @@ module.exports = { * @param {DiscordTypes.GatewayMessageReactionAddDispatchData} data */ async MESSAGE_REACTION_ADD(client, data) { - if (select("opt_out", "user_id", {user_id: data.user_id}).pluck().get()) return // This user opted out. if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. if (data.emoji.name === "โ" && select("event_message", "message_id", {message_id: data.message_id, source: 0, part: 0}).get()) { // source 0 = matrix const guild_id = data.guild_id ?? client.channels.get(data.channel_id)?.["guild_id"] @@ -417,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/0037-remove-leaked-webhooks.js b/src/db/migrations/0037-remove-leaked-webhooks.js index b839bd7..0228053 100644 --- a/src/db/migrations/0037-remove-leaked-webhooks.js +++ b/src/db/migrations/0037-remove-leaked-webhooks.js @@ -27,7 +27,7 @@ module.exports = async function(db) { const webhook = affectedWebhooks[counter-1] await discord.snow.webhook.deleteWebhookToken(webhook.webhook_id, webhook.webhook_token, "Webhook token possibly compromised during 8th May 2026 outage").catch(e => { - if (e.message === "Unknown Webhook") { + if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) { // OK } else { throw e 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/migrations/0040-opt-out.sql b/src/db/migrations/0040-opt-out.sql deleted file mode 100644 index 094dff9..0000000 --- a/src/db/migrations/0040-opt-out.sql +++ /dev/null @@ -1,9 +0,0 @@ -BEGIN TRANSACTION; - -CREATE TABLE "opt_out" ( - "user_id" TEXT NOT NULL, - "opted_out_at" INTEGER NOT NULL, - PRIMARY KEY("user_id") -) WITHOUT ROWID; - -COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index ff63d32..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 } @@ -115,11 +110,6 @@ export type Models = { historical_room_index: number } - opt_out: { - user_id: string - opted_out_at: number - } - role_default: { guild_id: string role_id: string @@ -147,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/poll.js b/src/discord/interactions/poll.js index 7190d63..6d7a015 100644 --- a/src/discord/interactions/poll.js +++ b/src/discord/interactions/poll.js @@ -55,10 +55,10 @@ async function* _interact({data, message, member, user}, {api}) { title: "Poll", components: [{ type: DiscordTypes.ComponentType.TextDisplay, - content: `## ${pollRow.question_text}` + content: `-# ${pollComponents.getMultiSelectString(pollRow.max_selections, options.length)}` }, { type: DiscordTypes.ComponentType.Label, - label: pollComponents.getMultiSelectString(pollRow.max_selections, options.length), + label: pollRow.question_text, component: { type: DiscordTypes.ComponentType.RadioGroup, custom_id: "POLL_MODAL_SELECTION", 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/channel-webhook.js b/src/m2d/actions/channel-webhook.js index 01b3dd4..09c642b 100644 --- a/src/m2d/actions/channel-webhook.js +++ b/src/m2d/actions/channel-webhook.js @@ -45,7 +45,7 @@ async function ensureWebhook(channelID, forceCreate = false) { async function withWebhook(channelID, callback) { const webhook = await ensureWebhook(channelID, false) return callback(webhook).catch(async e => { - if (e.message === "Unknown Webhook") { + if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) { // pathetic error handling from SnowTransfer // Our webhook is gone. Maybe somebody deleted it, or removed and re-added OOYE from the guild. const newWebhook = await ensureWebhook(channelID, true) return callback(newWebhook) // not caught; if the error happens again just throw it instead of looping 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 c4f72a0..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") @@ -384,7 +392,7 @@ function linkEndOfMessageSpriteSheet(content) { */ async function handleRoomOrMessageLinks(input, di) { let offset = 0 - for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/((?:#|%23|!|%21)[^"/, ?)]+)(?:\/((?:\$|%24)[^"/ ?)]+))?(?:\?[^",:!? )]*?)?)(">|[,<\n )]|$)/g)]) { + for (const match of [...input.matchAll(/("?https:\/\/matrix.to\/#\/((?:#|%23|!)[^"/, ?)]+)(?:\/(\$[^"/ ?)]+))?(?:\?[^",:!? )]*?)?)(">|[,<\n )]|$)/g)]) { assert(typeof match.index === "number") let [_, attributeValue, roomID, eventID, endMarker] = match let result @@ -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|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|
)+/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
\s*)*<\/blockquote>(?:\n|
\s*)*/g, "
\s*)*<\/blockquote>/g, "
this was inspired by someone telling me
\n\n\nbetrothed is an insane vocab pull
\n
\n\ni think thats part of why im so scared of cadence
\n
\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: "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
\n\nshort story
\n
by whose standards? LOL
\n\nI basically went all the way back to childhood (which analysts always like you to do)
\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