Compare commits
10 commits
main
...
ellie-fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ebfa37d93 | |||
| 28e5bd91a6 | |||
| 3160e979a0 | |||
| 622738fcf4 | |||
| 22bebaf064 | |||
| a0f8b02c55 | |||
| 5f27fedd86 | |||
| 1879eac26c | |||
| e21cb15c11 | |||
| de8e9b693c |
45 changed files with 512 additions and 1557 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
----
|
||||
<br><br><br><br><br>
|
||||
|
||||
# Appendix
|
||||
|
||||
|
|
|
|||
384
package-lock.json
generated
384
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.6.0",
|
||||
"version": "3.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.6.0",
|
||||
"version": "3.5.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||
|
|
@ -18,15 +18,14 @@
|
|||
"@cloudrac3r/pngjs": "^7.0.3",
|
||||
"@cloudrac3r/pug": "^4.0.4",
|
||||
"@cloudrac3r/stream-type": "^1.0.0",
|
||||
"@cloudrac3r/turndown": "^7.1.5",
|
||||
"@cloudrac3r/turndown": "^7.1.4",
|
||||
"@stackoverflow/stacks": "^2.5.4",
|
||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.19.0",
|
||||
"denque": "^2.1.0",
|
||||
"discord-api-types": "^0.38.52",
|
||||
"cloudstorm": "^0.17.1",
|
||||
"discord-api-types": "^0.38.38",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
"entities": "^5.0.0",
|
||||
|
|
@ -37,8 +36,8 @@
|
|||
"lru-cache": "^11.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"prettier-bytes": "^1.0.4",
|
||||
"sharp": "^0.35.3",
|
||||
"snowtransfer": "^0.19.0",
|
||||
"sharp": "^0.34.5",
|
||||
"snowtransfer": "^0.17.5",
|
||||
"try-to-catch": "^4.0.5",
|
||||
"uqr": "^0.1.2",
|
||||
"xxhash-wasm": "^1.0.2",
|
||||
|
|
@ -268,18 +267,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@cloudrac3r/turndown": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz",
|
||||
"integrity": "sha512-pENoG62B4UM2ecsg57cuSRDa1OV/YFYTFGg1aoQsKUdHBBglhE9I7EwEMYpS96QoM/auyglfaxjgTeqhcSGCIA==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz",
|
||||
"integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domino": "^2.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -302,9 +301,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -314,19 +313,19 @@
|
|||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -336,38 +335,19 @@
|
|||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -381,9 +361,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -397,9 +377,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
|
||||
"integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -413,9 +393,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -429,9 +409,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
|
||||
"integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -445,9 +425,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
|
||||
"integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -461,9 +441,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
|
||||
"integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -477,9 +457,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -493,9 +473,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -509,9 +489,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -525,9 +505,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
|
||||
"integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -537,19 +517,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2"
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -559,19 +539,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
|
||||
"integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -581,19 +561,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
|
||||
"integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -603,19 +583,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
|
||||
"integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -625,19 +605,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2"
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -647,19 +627,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -669,19 +649,19 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -691,54 +671,38 @@
|
|||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -748,16 +712,16 @@
|
|||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
|
||||
"integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -767,16 +731,16 @@
|
|||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -786,7 +750,7 @@
|
|||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
|
|
@ -1039,9 +1003,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
|
||||
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1199,16 +1163,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
|
|
@ -1352,13 +1316,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cloudstorm": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.19.0.tgz",
|
||||
"integrity": "sha512-mjuP5o2nS/CgSljajh08KjN6jYMfNBBttSPHc1YUN74f7OkmhUhv8QE8JFDXomxCfdnLNJcqBQtjRLrs8nkfVQ==",
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz",
|
||||
"integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.49",
|
||||
"snowtransfer": "^0.19.0"
|
||||
"discord-api-types": "^0.38.47",
|
||||
"snowtransfer": "^0.17.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
|
|
@ -1493,15 +1457,6 @@
|
|||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
|
|
@ -1518,9 +1473,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.52",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.52.tgz",
|
||||
"integrity": "sha512-uwe9EKfbjsmgWc2fdFjvDbj+dQqx3lp7wqDCmIha0jInuU+xeQjkCK9tMMn+p7RXfdVQORCInq4cD3U2ymDmyg==",
|
||||
"version": "0.38.47",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
|
||||
"integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
|
|
@ -2478,9 +2433,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
|
@ -2490,52 +2445,47 @@
|
|||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
|
||||
"integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.8.5"
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.3",
|
||||
"@img/sharp-darwin-x64": "0.35.3",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.3",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2",
|
||||
"@img/sharp-linux-arm": "0.35.3",
|
||||
"@img/sharp-linux-arm64": "0.35.3",
|
||||
"@img/sharp-linux-ppc64": "0.35.3",
|
||||
"@img/sharp-linux-riscv64": "0.35.3",
|
||||
"@img/sharp-linux-s390x": "0.35.3",
|
||||
"@img/sharp-linux-x64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.3",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.3",
|
||||
"@img/sharp-win32-arm64": "0.35.3",
|
||||
"@img/sharp-win32-ia32": "0.35.3",
|
||||
"@img/sharp-win32-x64": "0.35.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
|
|
@ -2629,12 +2579,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/snowtransfer": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.19.0.tgz",
|
||||
"integrity": "sha512-Vebj4FCdpUsEKAzqC1p50ol5o09CLnrffvBCpTv0FNFefvXPXwogxR8kr4zJApIRt7oWQVZZVkxAq4XnGlICjQ==",
|
||||
"version": "0.17.7",
|
||||
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz",
|
||||
"integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.49"
|
||||
"discord-api-types": "^0.38.47"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
|
|
@ -2859,9 +2809,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/try-to-catch": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.1.0.tgz",
|
||||
"integrity": "sha512-phAezhZqLgHHTpbZYe2GgmKe8itTgEX3gfd+YeIsmllgOOpXCDPn1DkDzVWj0mGQPriJgD6AagoUEcX+M2RlGw==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.5.tgz",
|
||||
"integrity": "sha512-VKBslDQsy4pGj2TMNgDdskWb7AWSi/9dPEmcNv3sdL0+aRMQTPJo6aEqlcuN0vbOwFfsE1oAXmx3bFdf6vrJFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
|
|||
13
package.json
13
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",
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
})()
|
||||
|
|
@ -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...")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
if (isDeepStrictEqual(existing, content)) return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const assert = require("assert")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
const Ty = require("../../types")
|
||||
|
||||
|
|
@ -132,10 +132,10 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) {
|
|||
try {
|
||||
// API lookup
|
||||
var pkMessage = await fetchMessage(messageID)
|
||||
db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 0)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
|
||||
db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
|
||||
} catch (e) {
|
||||
// Fall back to offline cache
|
||||
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 0}).pluck("mxid").get()
|
||||
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get()
|
||||
if (!senderMxid) throw e
|
||||
return senderMxid
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Ty.PluRalWebhookMessage>} */
|
||||
async function fetchMessage(channelID, messageID) {
|
||||
assert(reg.ooye.plu_ral_api_key)
|
||||
try {
|
||||
var res = await fetch(`https://api.plural.gg/messages/${channelID}/${messageID}?member=true`, {
|
||||
headers: {
|
||||
Authorization: reg.ooye.plu_ral_api_key
|
||||
}
|
||||
})
|
||||
} catch (networkError) {
|
||||
// Network issue, raise a more readable message
|
||||
throw new Error(`Failed to connect to /plu/ral API: ${networkError.toString()}`)
|
||||
}
|
||||
if (!res.ok) throw new Error(`/plu/ral API returned an error: ${await res.text()}`)
|
||||
/** @type {any} */
|
||||
const root = await res.json()
|
||||
if (!root.member) throw new Error(`/plu/ral API didn't return member data: ${JSON.stringify(root)}`)
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the same sim names and fake user IDs for /plu/ral members, since unlike PluralKit they don't have a short and a long ID.
|
||||
* @param {Ty.PluRalWebhookMessage} pluRalMessage
|
||||
*/
|
||||
function getSimName(pluRalMessage) {
|
||||
return `_pl_${pluRalMessage.member_id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* A sim is an account that is being simulated by the bridge to copy events from the other side.
|
||||
* @param {Ty.PluRalWebhookMessage} pluRalMessage
|
||||
* @returns mxid
|
||||
*/
|
||||
async function createSim(pluRalMessage) {
|
||||
// Choose sim name
|
||||
const simName = getSimName(pluRalMessage)
|
||||
const localpart = reg.ooye.namespace_prefix + simName
|
||||
const mxid = `@${localpart}:${reg.ooye.server_name}`
|
||||
|
||||
// Save chosen name in the database forever
|
||||
db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(simName, simName, simName, mxid)
|
||||
|
||||
// Register matrix user with that name
|
||||
try {
|
||||
await api.register(localpart)
|
||||
} catch (e) {
|
||||
// If user creation fails, manually undo the database change. Still isn't perfect, but should help.
|
||||
// (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.)
|
||||
db.prepare("DELETE FROM sim WHERE user_id = ?").run(simName)
|
||||
throw e
|
||||
}
|
||||
return mxid
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a sim is registered for the user.
|
||||
* If there is already a sim, use that one. If there isn't one yet, register a new sim.
|
||||
* @param {Ty.PluRalWebhookMessage} pluRalMessage
|
||||
* @returns {Promise<string>} mxid
|
||||
*/
|
||||
async function ensureSim(pluRalMessage) {
|
||||
let mxid = null
|
||||
const existing = select("sim", "mxid", {user_id: getSimName(pluRalMessage)}).pluck().get()
|
||||
if (existing) {
|
||||
mxid = existing
|
||||
} else {
|
||||
mxid = await createSim(pluRalMessage)
|
||||
}
|
||||
return mxid
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a sim is registered for the user and is joined to the room.
|
||||
* @param {Ty.PluRalWebhookMessage} pluRalMessage
|
||||
* @param {string} roomID
|
||||
* @returns {Promise<string>} mxid
|
||||
*/
|
||||
async function ensureSimJoined(pluRalMessage, roomID) {
|
||||
// Ensure room ID is really an ID, not an alias
|
||||
assert.ok(roomID[0] === "!")
|
||||
|
||||
// Ensure user
|
||||
const mxid = await ensureSim(pluRalMessage)
|
||||
|
||||
// Ensure joined
|
||||
const existing = select("sim_member", "mxid", {room_id: roomID, mxid}).pluck().get()
|
||||
if (!existing) {
|
||||
await api.inviteToRoom(roomID, mxid)
|
||||
await api.joinRoom(roomID, mxid)
|
||||
db.prepare("INSERT OR IGNORE INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid)
|
||||
}
|
||||
return mxid
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate profile data based on webhook displayname and configured avatar.
|
||||
* @param {Ty.PluRalWebhookMessage} pluRalMessage
|
||||
* @param {Ty.WebhookAuthor} author
|
||||
*/
|
||||
async function memberToStateContent(pluRalMessage, author) {
|
||||
// We prefer to use the member's avatar URL data since the image upload can be cached across channels,
|
||||
// unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL.
|
||||
const avatar = pluRalMessage.member.avatar_url || file.userAvatar(author)
|
||||
|
||||
const content = {
|
||||
displayname: author.username,
|
||||
membership: "join",
|
||||
"moe.cadence.ooye.plu_ral_member": pluRalMessage.member
|
||||
}
|
||||
if (avatar) content.avatar_url = await file.uploadDiscordFileToMxc(avatar)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync profile data for a sim user. This function follows the following process:
|
||||
* 1. Look up data about proxy user from API
|
||||
* 2. If this fails, try to use previously cached data (won't sync)
|
||||
* 3. Create and join the sim to the room if needed
|
||||
* 4. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before
|
||||
* 5. Compare against the previously known state content, which is helpfully stored in the database
|
||||
* 6. If the state content has changed, send it to Matrix and update it in the database for next time
|
||||
* @param {string} channelID to call API with
|
||||
* @param {string} messageID to call API with
|
||||
* @param {Ty.WebhookAuthor} author for profile data
|
||||
* @param {string} roomID room to join member to
|
||||
* @param {boolean} shouldActuallySync whether to actually sync updated user data or just ensure it's joined
|
||||
* @returns {Promise<string>} mxid of the updated sim
|
||||
*/
|
||||
async function syncUser(channelID, messageID, author, roomID, shouldActuallySync) {
|
||||
try {
|
||||
// API lookup
|
||||
var pluRalMessage = await fetchMessage(channelID, messageID)
|
||||
const simName = getSimName(pluRalMessage)
|
||||
db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 1)").run(simName, pluRalMessage.author_id, author.username)
|
||||
} catch (e) {
|
||||
// Fall back to offline cache
|
||||
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 1}).pluck("mxid").get()
|
||||
if (!senderMxid) throw e
|
||||
return senderMxid
|
||||
}
|
||||
|
||||
// Create and join the sim to the room if needed
|
||||
const mxid = await ensureSimJoined(pluRalMessage, roomID)
|
||||
|
||||
if (shouldActuallySync) {
|
||||
// Build current profile data and sync if the hash has changed
|
||||
const content = await memberToStateContent(pluRalMessage, author)
|
||||
await registerUser._sendSyncUser(roomID, mxid, content, null)
|
||||
}
|
||||
|
||||
return mxid
|
||||
}
|
||||
|
||||
module.exports.syncUser = syncUser
|
||||
|
|
@ -4,16 +4,17 @@ const assert = require("assert").strict
|
|||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {discord, sync, db, select, from} = passthrough
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
const { discord, sync, db, select, from} = passthrough
|
||||
/** @type {import("../converters/message-to-event")} */
|
||||
const messageToEvent = sync.require("../converters/message-to-event")
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("./register-user")} */
|
||||
const registerUser = sync.require("./register-user")
|
||||
/** @type {import("./speedbump")} */
|
||||
const speedbump = sync.require("./speedbump")
|
||||
/** @type {import("./register-pk-user")} */
|
||||
const registerPkUser = sync.require("./register-pk-user")
|
||||
/** @type {import("./register-webhook-user")} */
|
||||
const registerWebhookUser = sync.require("./register-webhook-user")
|
||||
/** @type {import("../actions/create-room")} */
|
||||
const createRoom = sync.require("../actions/create-room")
|
||||
/** @type {import("../actions/poll-end")} */
|
||||
|
|
@ -27,15 +28,24 @@ const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
|||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {DiscordTypes.APIGuildChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel
|
||||
*/
|
||||
async function sendMessage(message, channel, guild) {
|
||||
async function sendMessage(message, channel, guild, row) {
|
||||
const roomID = await createRoom.ensureRoom(message.channel_id)
|
||||
const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: roomID}).pluck().get()
|
||||
assert(historicalRoomIndex)
|
||||
|
||||
let senderMxid = null
|
||||
if (dUtils.isWebhookMessage(message)) {
|
||||
senderMxid = await speedbump.getWebhookSenderId(message, guild.id, roomID)
|
||||
const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guild.id}).pluck().get() ?? 0
|
||||
if (row && row.speedbump_webhook_id === message.webhook_id) {
|
||||
// Handle the PluralKit public instance
|
||||
if (row.speedbump_id === "466378653216014359") {
|
||||
senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true)
|
||||
}
|
||||
} else if (useWebhookProfile) {
|
||||
senderMxid = await registerWebhookUser.syncUser(message.author, roomID, true)
|
||||
}
|
||||
} else {
|
||||
// not a webhook
|
||||
if (message.author.id === discord.application.id) {
|
||||
|
|
@ -50,7 +60,8 @@ async function sendMessage(message, channel, guild) {
|
|||
const detailedResultsMessage = await pollEnd.endPoll(message)
|
||||
if (detailedResultsMessage) {
|
||||
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
|
||||
const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent)
|
||||
const channelID = threadParent ? threadParent : message.channel_id
|
||||
const threadID = threadParent ? message.channel_id : undefined
|
||||
sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {discord, from, select, db, sync} = passthrough
|
||||
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
/** @type {import("./register-pk-user")} */
|
||||
const registerPkUser = sync.require("./register-pk-user")
|
||||
/** @type {import("./register-plu-ral-user")} */
|
||||
const registerPluRalUser = sync.require("./register-plu-ral-user")
|
||||
/** @type {import("./register-webhook-user")} */
|
||||
const registerWebhookUser = sync.require("./register-webhook-user")
|
||||
const {discord, select, db} = passthrough
|
||||
|
||||
const DEBUG_SPEEDBUMP = false
|
||||
|
||||
function debugSpeedbump(...args) {
|
||||
function debugSpeedbump(message) {
|
||||
if (DEBUG_SPEEDBUMP) {
|
||||
console.log(...args)
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,8 +17,7 @@ const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
|
|||
|
||||
/** @type {Set<any>} */
|
||||
const KNOWN_BOTS = new Set([
|
||||
"466378653216014359", // PluralKit
|
||||
"1291501048493768784", // /plu/ral
|
||||
"466378653216014359" // PluralKit
|
||||
])
|
||||
|
||||
/**
|
||||
|
|
@ -39,81 +29,55 @@ async function updateCache(channelID, lastChecked) {
|
|||
const now = Math.floor(Date.now() / 1000)
|
||||
if (lastChecked && now - lastChecked < SPEEDBUMP_UPDATE_FREQUENCY) return
|
||||
const webhooks = await discord.snow.webhook.getChannelWebhooks(channelID)
|
||||
const found = webhooks.filter(b => KNOWN_BOTS.has(b.application_id))
|
||||
db.transaction(() => {
|
||||
db.prepare("DELETE FROM channel_speedbump WHERE channel_id = ?").run(channelID)
|
||||
for (const webhook of found) {
|
||||
db.prepare("INSERT INTO channel_speedbump (channel_id, speedbump_webhook_id, speedbump_user_id) VALUES (?, ?, ?)").run(channelID, webhook.id, webhook.application_id)
|
||||
}
|
||||
db.prepare("UPDATE channel_room SET speedbump_checked = ? WHERE channel_id = ?").run(now, channelID)
|
||||
})()
|
||||
const found = webhooks.find(b => KNOWN_BOTS.has(b.application_id))
|
||||
const foundApplication = found?.application_id
|
||||
const foundWebhook = found?.id
|
||||
db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef BumpingEntry
|
||||
* @prop {number} number number of gateway events currently bumping for this message ID
|
||||
* @prop {boolean} hasCreate whether there was a message create within the events currently bumping
|
||||
*/
|
||||
|
||||
/** @type {Map<string, BumpingEntry>} messageID -> BumpingEntry */
|
||||
/** @type {Map<string, number>} messageID -> number of gateway events currently bumping */
|
||||
const bumping = new Map()
|
||||
|
||||
/**
|
||||
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
||||
* @param {boolean} isCreate
|
||||
* @param {string} messageID
|
||||
* @returns whether it was deleted
|
||||
*/
|
||||
async function doSpeedbump(isCreate, messageID) {
|
||||
const entry = bumping.get(messageID) ?? (() => {
|
||||
const entry = {number: 0, hasCreate: false}
|
||||
bumping.set(messageID, entry)
|
||||
return entry
|
||||
})()
|
||||
|
||||
entry.number++
|
||||
entry.hasCreate ||= isCreate
|
||||
|
||||
debugSpeedbump(`[speedbump] WAIT ${messageID}++ =`, entry)
|
||||
async function doSpeedbump(messageID) {
|
||||
let value = (bumping.get(messageID) ?? 0) + 1
|
||||
bumping.set(messageID, value)
|
||||
debugSpeedbump(`[speedbump] WAIT ${messageID}++ = ${value}`)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED))
|
||||
|
||||
if (!bumping.has(messageID)) {
|
||||
debugSpeedbump(`[speedbump] DELETED ${messageID}`)
|
||||
return {skip: true, hasCreate: null}
|
||||
return true
|
||||
}
|
||||
|
||||
if (--entry.number <= 0) {
|
||||
debugSpeedbump(`[speedbump] OK ${messageID}-- =`, entry)
|
||||
value = (bumping.get(messageID) ?? 0) - 1
|
||||
if (value <= 0) {
|
||||
debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`)
|
||||
bumping.delete(messageID)
|
||||
return {skip: false, hasCreate: entry.hasCreate}
|
||||
return false
|
||||
} else {
|
||||
debugSpeedbump(`[speedbump] MULTI ${messageID}-- =`, entry)
|
||||
return {skip: true, hasCreate: null}
|
||||
debugSpeedbump(`[speedbump] MULTI ${messageID}-- = ${value}`)
|
||||
bumping.set(messageID, value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function getSpeedbumpRows(channelID) {
|
||||
return from("channel_room").join("channel_speedbump", "channel_id").select("thread_parent", "speedbump_user_id", "speedbump_webhook_id").where({channel_id: channelID}).all()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
||||
* @param {boolean} isCreate
|
||||
* @param {{id: string, channel_id: string, author: {id: string}, backfill?: boolean}} message uses the ID to identify, and the userID to only slow down the message when the user has used PK before
|
||||
* @returns whether to skip this message, and whether the message should be created as a creation
|
||||
* @param {string} channelID
|
||||
* @param {string} messageID
|
||||
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
|
||||
*/
|
||||
async function maybeDoSpeedbump(isCreate, message) {
|
||||
let rows = getSpeedbumpRows(message.channel_id)
|
||||
if (rows[0]?.thread_parent) rows = getSpeedbumpRows(rows[0].thread_parent) // webhooks belong to the channel, not the thread
|
||||
if (!rows.length) return {skip: false} // channel not affected, no speedbump
|
||||
|
||||
if (message.backfill) return {skip: false} // don't slow messages during backfill
|
||||
if (rows.some(r => r.speedbump_webhook_id === message.author.id)) return {skip: false} // shortcut
|
||||
const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: message.author.id}).pluck().get()
|
||||
if (!userHasProxy) return {skip: false} // user has not used PK before, no speedbump
|
||||
const {skip, hasCreate} = await doSpeedbump(isCreate, message.id)
|
||||
return {skip, hasCreate} // maybe affected, and there is a speedbump
|
||||
async function maybeDoSpeedbump(channelID, messageID) {
|
||||
let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
|
||||
if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
|
||||
if (!row?.speedbump_webhook_id) return {affected: false, row: null} // not affected, no speedbump
|
||||
const affected = await doSpeedbump(messageID)
|
||||
return {affected, row} // maybe affected, and there is a speedbump
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,25 +87,7 @@ function onMessageDelete(messageID) {
|
|||
bumping.delete(messageID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessage} message
|
||||
* @param {string} guildID
|
||||
* @param {string} roomID
|
||||
*/
|
||||
async function getWebhookSenderId(message, guildID, roomID) {
|
||||
const speedbumpUserID = select("channel_speedbump", "speedbump_user_id", {channel_id: message.channel_id, speedbump_webhook_id: message.webhook_id}).pluck().get()
|
||||
const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guildID}).pluck().get() ?? 0
|
||||
if (speedbumpUserID === "466378653216014359") { // PluralKit public instance
|
||||
return await registerPkUser.syncUser(message.id, message.author, roomID, true)
|
||||
} else if (speedbumpUserID === "1291501048493768784" && reg.ooye.plu_ral_api_key) { // /plu/ral public instance
|
||||
return await registerPluRalUser.syncUser(message.channel_id, message.id, message.author, roomID, true)
|
||||
} else if (useWebhookProfile) {
|
||||
return await registerWebhookUser.syncUser(message.author, roomID, true)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
module.exports.updateCache = updateCache
|
||||
module.exports.doSpeedbump = doSpeedbump
|
||||
module.exports.maybeDoSpeedbump = maybeDoSpeedbump
|
||||
module.exports.onMessageDelete = onMessageDelete
|
||||
module.exports.getWebhookSenderId = getWebhookSenderId
|
||||
|
|
|
|||
|
|
@ -265,9 +265,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
|||
* @param {any} newEvents merge into events
|
||||
* @param {any} events will be modified
|
||||
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
|
||||
* @param {boolean} [forceMerge] if true, must merge event, will error if it had to append
|
||||
*/
|
||||
function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) {
|
||||
function mergeTextEvents(newEvents, events, forceSameMsgtype) {
|
||||
let prev = events.at(-1)
|
||||
for (const ne of newEvents) {
|
||||
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
|
||||
|
|
@ -279,8 +278,6 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false
|
|||
rep.addLine(ne.body, ne.formatted_body)
|
||||
prev.body = rep.body
|
||||
prev.formatted_body = rep.formattedBody
|
||||
} else if (forceMerge) {
|
||||
throw new Error("Unable to merge events")
|
||||
} else {
|
||||
events.push(ne)
|
||||
}
|
||||
|
|
@ -557,7 +554,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
// Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter.
|
||||
// So we scan the message ahead of time for all its emojis and ensure they are in the DB.
|
||||
const emojiMatches = [...content.matchAll(/<(a?):([^:>]+):([0-9]+)>/g)]
|
||||
const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)]
|
||||
await Promise.all(emojiMatches.map(match => {
|
||||
const id = match[3]
|
||||
const name = match[2]
|
||||
|
|
@ -970,8 +967,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
// May only be a section accessory or in an action row (up to 5)
|
||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||
assert(component.label) // required for Discord to validate link buttons
|
||||
const link = await transformContentMessageLinks(component.url)
|
||||
stack.msb.add(`[${component.label} ${link}] `, tag`<a href="${link}">${component.label}</a> `)
|
||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -984,20 +980,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
const {body, formatted_body} = stack.msb.get()
|
||||
if (body.trim().length) {
|
||||
// Create new message if Components V2 (cannot have regular content)
|
||||
if ((message.flags ?? 0) & DiscordTypes.MessageFlags.IsComponentsV2) {
|
||||
await addTextEvent(body, formatted_body, "m.text")
|
||||
}
|
||||
// Add to existing message if legacy components https://docs.discord.com/developers/components/reference#legacy-message-component-behavior
|
||||
else {
|
||||
mergeTextEvents([{
|
||||
msgtype: "m.text",
|
||||
body,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body
|
||||
}], events, false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then polls
|
||||
|
|
|
|||
|
|
@ -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: "<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">cadence</a> used <code>/🔔 Ping author</code></sub></blockquote>Psst, <strong>Red</strong> (<a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">@cadence.worm</a>), you have been pinged by <a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">@cadence.worm</a>.<br><a href=\"https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe\">Jump</a> ",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,7 +59,6 @@ class DiscordClient {
|
|||
})
|
||||
}
|
||||
|
||||
if (CONNECTION_DEBUG) {
|
||||
const addEventLogger = (eventName, logName) => {
|
||||
this.cloud.on(eventName, (...args) => {
|
||||
const d = new Date().toISOString().slice(0, 19)
|
||||
|
|
@ -72,7 +69,6 @@ class DiscordClient {
|
|||
addEventLogger("disconnected", "Disconnected")
|
||||
addEventLogger("ready", "Ready")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DiscordClient
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
// @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")
|
||||
|
||||
let checkedHomeserver = false
|
||||
|
||||
/**
|
||||
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 function onPacket(client, message, listen) {
|
||||
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")
|
||||
|
|
@ -51,31 +48,6 @@ async function onPacket(client, message, listen) {
|
|||
|
||||
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~")
|
||||
}
|
||||
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 {
|
||||
await homeserverStatus.homeserverStatus.waitForOnline(false)
|
||||
}
|
||||
|
||||
await eventDispatcher.checkMissedExpressions(message.d)
|
||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||
await eventDispatcher.checkMissedPins(client, message.d)
|
||||
|
|
@ -180,33 +152,6 @@ async function onPacket(client, message, listen) {
|
|||
|
||||
// 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)
|
||||
|
|
@ -221,7 +166,8 @@ async function dispatchPacketToBridge(client, message) {
|
|||
// Let OOYE try to handle errors too
|
||||
await eventDispatcher.onError(client, e, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.onPacket = onPacket
|
||||
module.exports.dispatchPacketToBridge = dispatchPacketToBridge
|
||||
module.exports = utils
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ module.exports = {
|
|||
|
||||
matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage)
|
||||
|
||||
const channelID = gatewayMessage.d?.["channel_id"]
|
||||
const channelID = gatewayMessage.d["channel_id"]
|
||||
if (!channelID) return
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||
if (!roomID) return
|
||||
|
|
@ -288,6 +288,7 @@ module.exports = {
|
|||
if (!guildID) return // channel must have been a DM channel or something
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
if (!roomID) return // channel wasn't being bridged in the first place
|
||||
// @ts-ignore
|
||||
await createRoom.unbridgeChannel(channel, guildID)
|
||||
},
|
||||
|
||||
|
|
@ -312,10 +313,11 @@ module.exports = {
|
|||
|
||||
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
|
||||
|
||||
const {skip} = await speedbump.maybeDoSpeedbump(true, message)
|
||||
if (skip) return
|
||||
const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id)
|
||||
if (affected) return
|
||||
|
||||
await sendMessage.sendMessage(message, channel, guild)
|
||||
// @ts-ignore
|
||||
await sendMessage.sendMessage(message, channel, guild, row)
|
||||
|
||||
retrigger.finishedBridging(message.id)
|
||||
},
|
||||
|
|
@ -333,27 +335,24 @@ module.exports = {
|
|||
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
|
||||
|
||||
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
|
||||
const {skip, hasCreate} = await speedbump.maybeDoSpeedbump(false, data)
|
||||
if (skip) return
|
||||
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
|
||||
if (affected) return
|
||||
|
||||
if (!row) {
|
||||
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
||||
if (!await retrigger.waitForMessage(data.id)) return
|
||||
}
|
||||
|
||||
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
||||
// @ts-ignore
|
||||
const message = data
|
||||
const channel = client.channels.get(message.channel_id)
|
||||
if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages.
|
||||
const guild = client.guilds.get(channel.guild_id)
|
||||
assert(guild)
|
||||
|
||||
if (!hasCreate) {
|
||||
// Standard path for most message updates
|
||||
// Check that the target message already exists, and deal with Eventual Consistency(TM)
|
||||
if (!await retrigger.waitForMessage(data.id)) return
|
||||
await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild))
|
||||
}
|
||||
else {
|
||||
// Path for edit packets that were speedbumped into the latest copy of a message that needs to be created
|
||||
// Just pretend to be MESSAGE_CREATE
|
||||
await sendMessage.sendMessage(message, channel, guild)
|
||||
}
|
||||
// @ts-ignore
|
||||
await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row))
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -415,7 +414,6 @@ module.exports = {
|
|||
* @param {DiscordTypes.GatewayTypingStartDispatchData} data
|
||||
*/
|
||||
async TYPING_START(client, data) {
|
||||
if (data.user_id === client.user.id) return
|
||||
const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get()
|
||||
if (!roomID) return
|
||||
const mxid = from("sim").join("sim_member", "mxid").where({user_id: data.user_id, room_id: roomID}).pluck("mxid").get()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
11
src/db/orm-defs.d.ts
vendored
11
src/db/orm-defs.d.ts
vendored
|
|
@ -18,17 +18,13 @@ export type Models = {
|
|||
thread_parent: string | null
|
||||
custom_avatar: string | null
|
||||
last_bridged_pin_timestamp: number | null
|
||||
speedbump_id: string | null
|
||||
speedbump_webhook_id: string | null
|
||||
speedbump_checked: number | null
|
||||
guild_id: string | null
|
||||
custom_topic: number
|
||||
}
|
||||
|
||||
channel_speedbump: {
|
||||
channel_id: string
|
||||
speedbump_webhook_id: string
|
||||
speedbump_user_id: string
|
||||
}
|
||||
|
||||
direct: {
|
||||
mxid: string
|
||||
room_id: string
|
||||
|
|
@ -48,7 +44,6 @@ export type Models = {
|
|||
event_subtype: string | null
|
||||
part: number
|
||||
reaction_part: number
|
||||
/** 0 = Matrix, 1 = Discord */
|
||||
source: number
|
||||
}
|
||||
|
||||
|
|
@ -142,8 +137,6 @@ export type Models = {
|
|||
user_id: string
|
||||
proxy_owner_id: string
|
||||
displayname: string
|
||||
/** 0 = PluralKit, 1 = /plu/ral */
|
||||
proxy_app: number
|
||||
}
|
||||
|
||||
webhook: {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ const utils = sync.require("../../matrix/utils")
|
|||
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[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<string, Set<string>>} reaction key (the emoji) -> displaynames */
|
||||
/** @type {Map<string, string[]>} */
|
||||
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"),
|
||||
}}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,6 +91,15 @@ function registerInteractions() {
|
|||
async function dispatchInteraction(interaction) {
|
||||
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
||||
try {
|
||||
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 {
|
||||
throw new Error(`Unknown message component ${interaction.data.custom_id}`)
|
||||
}
|
||||
} else {
|
||||
if (interactionId === "Matrix info") {
|
||||
await matrixInfo.interact(interaction)
|
||||
} else if (interactionId === "invite") {
|
||||
|
|
@ -113,11 +122,10 @@ async function dispatchInteraction(interaction) {
|
|||
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}`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
let stackLines = null
|
||||
if (e.stack) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const {discord} = require("../../passthrough")
|
||||
|
||||
/** @type {Set<string>} channel ids that are currently typing */
|
||||
const isTyping = new Set()
|
||||
/** @type {Map<string, NodeJS.Timeout>} 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
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,10 +33,9 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const domino = require("domino")
|
|||
const assert = require("assert").strict
|
||||
const entities = require("entities")
|
||||
const pb = require("prettier-bytes")
|
||||
const mimeTypes = require("mime-types")
|
||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
|
|
@ -98,6 +97,15 @@ turndownService.addRule("underline", {
|
|||
}
|
||||
})
|
||||
|
||||
turndownService.addRule("blockquote", {
|
||||
filter: "blockquote",
|
||||
replacement: function (content) {
|
||||
content = content.replace(/^\n+|\n+$/g, "")
|
||||
content = content.replace(/^/gm, "> ")
|
||||
return content
|
||||
}
|
||||
})
|
||||
|
||||
turndownService.addRule("spoiler", {
|
||||
filter: function (node, options) {
|
||||
return node.tagName === "SPAN" && node.hasAttribute("data-mx-spoiler")
|
||||
|
|
@ -614,20 +622,13 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
captionContent.addLine(`(Spoiler: ${fileSpoilerReason})`)
|
||||
}
|
||||
|
||||
// If filename is not specified, create one from mimetype
|
||||
let filename = event.content.filename || event.content.body
|
||||
if (!filename) {
|
||||
const extension = mimeTypes.extension(event.content.info?.mimetype)
|
||||
if (!extension) throw new Error("Filename and mimetype missing. Please report this bug to your client.")
|
||||
filename = `file.${extension}`
|
||||
}
|
||||
|
||||
// File link as alternative to uploading
|
||||
if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) {
|
||||
// Upload (unencrypted) file as link, because it's too large for Discord
|
||||
// Do this by constructing a sample Matrix message with the link and then use the text processor to convert that + the original caption.
|
||||
const url = mxUtils.getPublicUrlForMxc(event.content.url)
|
||||
assert(url)
|
||||
const filename = event.content.filename || event.content.body
|
||||
const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : ""
|
||||
if (fileIsSpoiler) {
|
||||
captionContent.addLine(`${emoji}Uploaded SPOILER file: <${url}> (${pb(event.content.info.size)})`, tag`${emoji}<em>Uploaded <strong>SPOILER</strong> file: <span data-mx-spoiler><a href="${url} ">${filename}</a></span> (${pb(event.content.info.size)})</em>`) // 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>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
||||
repliedToContent = repliedToContent.replace(/(?:\n|<br ?\/?>)+/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 `<via ${u.hostname}>`
|
||||
} catch (e) {
|
||||
return `<url>`
|
||||
}
|
||||
}
|
||||
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 <br> before </blockquote> 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|<br ?\/?>\s*)*<\/blockquote>(?:\n|<br ?\/?>\s*)*/g, "</blockquote>")
|
||||
input = input.replace(/(?:\n|<br ?\/?>\s*)*<\/blockquote>/g, "</blockquote>")
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -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: "<p>this was inspired by someone telling me</p>\n<blockquote>\n<p>betrothed is an insane vocab pull</p>\n</blockquote>\n<blockquote>\n<p>i think thats part of why im so scared of cadence</p>\n</blockquote>\n<blockquote>\n<p>if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up</p>\n</blockquote>"
|
||||
},
|
||||
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: "<blockquote>\n<p>short story</p>\n</blockquote>\n<p>by whose standards? LOL</p><blockquote>\n<p>I basically went all the way back to childhood (which analysts always like you to do)</p>\n</blockquote>\n<p>laughed out loud</p>"
|
||||
},
|
||||
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: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@_ooye_.wing.:cadence.moe">@_ooye_.wing.:cadence.moe</a><br>https://huggingface.co/blog/security-incident-july-2026</blockquote></mx-reply>this is either written by claude or somebody who's too claude-brained and I just can't get through it`,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
|
||||
}
|
||||
}
|
||||
},
|
||||
event_id: "$0Bs3rbsXaeZmSztGMx1NIsqvOrkXOpIWebN-dqr09i4",
|
||||
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||
}, data.guild.general, data.channel.general, {
|
||||
api: {
|
||||
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU", {
|
||||
"type": "m.room.message",
|
||||
"sender": "@_ooye_.wing.:cadence.moe",
|
||||
"content": {
|
||||
"m.mentions": {},
|
||||
"msgtype": "m.text",
|
||||
"body": "<https://huggingface.co/blog/security-incident-july-2026>",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href=\"https://huggingface.co/blog/security-incident-july-2026\">https://huggingface.co/blog/security-incident-july-2026</a>"
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 <@112890272819507200>:"
|
||||
+ " <via huggingface.co>..."
|
||||
+ `\nthis is either written by claude or somebody who's too claude-brained and I just can't get through it`,
|
||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: should include a reply preview when message ends with a blockquote", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ const vote = sync.require("./actions/vote")
|
|||
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
|
||||
/** @type {import("../matrix/utils")} */
|
||||
const utils = sync.require("../matrix/utils")
|
||||
/** @type {import("../matrix/mreq")}) */
|
||||
const mreq = sync.require("../matrix/mreq")
|
||||
/** @type {import("../matrix/api")}) */
|
||||
const api = sync.require("../matrix/api")
|
||||
/** @type {import("../d2m/actions/create-room")} */
|
||||
|
|
@ -34,10 +32,6 @@ const createRoom = sync.require("../d2m/actions/create-room")
|
|||
const roomUpgrade = require("../matrix/room-upgrade")
|
||||
/** @type {import("../d2m/actions/retrigger")} */
|
||||
const retrigger = sync.require("../d2m/actions/retrigger")
|
||||
/** @type {import("../matrix/homeserver-status")} */
|
||||
const homeserverStatus = sync.require("../matrix/homeserver-status")
|
||||
/** @type {import("./actions/typing")} */
|
||||
const typing = sync.require("./actions/typing")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
let lastReportedEvent = 0
|
||||
|
|
@ -172,15 +166,7 @@ async function sendError(roomID, source, type, e, payload) {
|
|||
key: "🔁"
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
if (
|
||||
e.toString().includes("fetch failed")
|
||||
|| (e instanceof mreq.MatrixServerError && [502, 503].includes(e.httpStatus) && e.errcode === "CX_SERVER_ERROR")
|
||||
) {
|
||||
// Matrix homeserver is down (reverse proxy indicated failure)
|
||||
homeserverStatus.homeserverStatus.setErrorWithPacket(payload)
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function guard(type, fn) {
|
||||
|
|
@ -529,20 +515,6 @@ async event => {
|
|||
await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.")
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "ephemeral_type:m.typing", guard("m.typing",
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Typing} event
|
||||
*/
|
||||
async event => {
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
if (!channelID) return
|
||||
if (event.content.user_ids.some(mxid => !utils.eventSenderIsFromDiscord(mxid))) {
|
||||
typing.startTyping(channelID)
|
||||
} else {
|
||||
typing.stopTyping(channelID)
|
||||
}
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
module.exports.cleanErrorStack = cleanErrorStack
|
||||
module.exports.sendError = sendError
|
||||
|
|
|
|||
|
|
@ -452,8 +452,7 @@ async function ping() {
|
|||
headers: {
|
||||
Authorization: `Bearer ${reg.as_token}`
|
||||
},
|
||||
body: "{}",
|
||||
signal: AbortSignal.timeout(15e3)
|
||||
body: "{}"
|
||||
})
|
||||
const root = await res.json()
|
||||
return {
|
||||
|
|
@ -475,7 +474,7 @@ async function ping() {
|
|||
async function getMedia(mxc, init = {}) {
|
||||
init = {...init}
|
||||
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||
assert(mediaParts)
|
||||
|
||||
let route = "download"
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert")
|
||||
const Denque = require("denque")
|
||||
const StateMachine = require("snowtransfer").StateMachine
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
/** @type {import("../d2m/discord-packets")} */
|
||||
const discordPackets = sync.require("../d2m/discord-packets")
|
||||
/** @type {import("../matrix/api")} */
|
||||
const api = sync.require("../matrix/api")
|
||||
|
||||
const DEBUG_HOMESERVER_STATUS = false
|
||||
|
||||
function debugHomeserverStatus(message) {
|
||||
if (DEBUG_HOMESERVER_STATUS) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
const homeserverStatus = new class HomeserverStatus {
|
||||
constructor() {
|
||||
/** @private */
|
||||
this.queue = new Denque()
|
||||
|
||||
/** @private */
|
||||
this.pingInterval = undefined
|
||||
|
||||
/** @private */
|
||||
this.sm = new StateMachine("online")
|
||||
.defineState("online")
|
||||
|
||||
.defineState("checking", {
|
||||
onEnter: [async () => {
|
||||
const pingResult = await api.ping().catch(e => ({ok: false}))
|
||||
if (pingResult.ok) {
|
||||
this.sm.doTransition("check ok")
|
||||
} else {
|
||||
this.sm.doTransition("check fail")
|
||||
}
|
||||
}],
|
||||
onLeave: [],
|
||||
transitions: new Map()
|
||||
})
|
||||
|
||||
.defineState("offline", {
|
||||
onEnter: [() => {
|
||||
this.pingInterval = setInterval(async () => {
|
||||
const pingResult = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
|
||||
if (pingResult.ok) {
|
||||
this.sm.doTransition("ping ok")
|
||||
}
|
||||
}, 15e3)
|
||||
}],
|
||||
onLeave: [() => {
|
||||
clearInterval(this.pingInterval)
|
||||
}],
|
||||
transitions: new Map()
|
||||
})
|
||||
|
||||
.defineState("recovering", {
|
||||
onEnter: [async () => { // Drain queue.
|
||||
while (!this.queue.isEmpty()) {
|
||||
const packet = this.queue.peekFront() // same position as .shift()
|
||||
debugHomeserverStatus(`homeserver status: ${new Date().toISOString()} dq packet ${packet.t} ${packet.d?.content}`)
|
||||
await discordPackets.dispatchPacketToBridge(passthrough.discord, packet)
|
||||
if (this.sm.currentStateName !== "recovering") return // got kicked out due to another error
|
||||
this.queue.shift()
|
||||
}
|
||||
this.sm.doTransition("recovered")
|
||||
}],
|
||||
onLeave: [],
|
||||
transitions: new Map()
|
||||
})
|
||||
|
||||
.defineUniversalTransition("error", "offline")
|
||||
.defineTransition("offline", "ping ok", "recovering")
|
||||
.defineTransition("recovering", "recovered", "online")
|
||||
.defineTransition("online", "check", "checking")
|
||||
.defineTransition("checking", "check ok", "recovering")
|
||||
.defineTransition("checking", "check fail", "offline")
|
||||
|
||||
this.sm.on("enter", st => debugHomeserverStatus(`homeserver status: ${st}`))
|
||||
this.sm.setMaxListeners(101)
|
||||
this.sm.freeze()
|
||||
}
|
||||
|
||||
isRealTime() {
|
||||
return this.sm.currentStateName === "online"
|
||||
}
|
||||
|
||||
/** @param {boolean} forceCheck */
|
||||
waitForOnline(forceCheck) {
|
||||
const onlinePromise = new Promise(resolve => {
|
||||
// Already online? Start check or just done
|
||||
if (this.sm.currentStateName === "online") {
|
||||
if (forceCheck) {
|
||||
this.sm.doTransition("check")
|
||||
} else {
|
||||
return resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Checking or not online. Wait for online.
|
||||
const onlineListener = stateName => {
|
||||
if (stateName === "online") {
|
||||
this.sm.removeListener("enter", onlineListener)
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
this.sm.on("enter", onlineListener)
|
||||
})
|
||||
return onlinePromise
|
||||
}
|
||||
|
||||
/**
|
||||
* When offline or recovering, call this for incoming packets to queue them to be sent in order later.
|
||||
*/
|
||||
queuePacket(packet) {
|
||||
assert(["offline", "recovering"].includes(this.sm.currentStateName))
|
||||
this.queue.push(packet)
|
||||
}
|
||||
|
||||
setErrorWithPacket(packet) {
|
||||
const wasRecovering = this.sm.currentStateName === "recovering"
|
||||
this.sm.doTransition("error")
|
||||
if (!wasRecovering) { // if was recovering then packet is already in the right place in queue
|
||||
this.queuePacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.homeserverStatus = homeserverStatus
|
||||
|
|
@ -9,14 +9,9 @@ const {reg} = require("./read-registration.js")
|
|||
const baseUrl = `${reg.ooye.server_origin}/_matrix`
|
||||
|
||||
class MatrixServerError extends Error {
|
||||
/**
|
||||
* @param {number} httpStatus}
|
||||
*/
|
||||
constructor(data, httpStatus, opts = {}) {
|
||||
constructor(data, opts) {
|
||||
super(data.error || data.errcode)
|
||||
this.data = data
|
||||
/** @type {number} */
|
||||
this.httpStatus = httpStatus
|
||||
/** @type {string} */
|
||||
this.errcode = data.errcode
|
||||
this.opts = opts
|
||||
|
|
@ -49,11 +44,11 @@ async function _convertBody(body) {
|
|||
async function makeMatrixServerError(res, opts = {}) {
|
||||
delete opts.headers?.["Authorization"]
|
||||
if (res.headers.get("content-type") === "application/json") {
|
||||
return new MatrixServerError(await res.json(), res.status, opts)
|
||||
return new MatrixServerError(await res.json(), opts)
|
||||
} else if (res.headers.get("content-type")?.startsWith("text/")) {
|
||||
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, res.status, opts)
|
||||
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts)
|
||||
} else {
|
||||
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, res.status, opts)
|
||||
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,12 +78,12 @@ async function mreq(method, url, bodyIn, extra = {}) {
|
|||
var root = JSON.parse(text)
|
||||
} catch (e) {
|
||||
delete opts.headers?.["Authorization"]
|
||||
throw new MatrixServerError(text, res.status, {baseUrl, url, ...opts})
|
||||
throw new MatrixServerError(text, {baseUrl, url, ...opts})
|
||||
}
|
||||
|
||||
if (!res.ok || root.errcode) {
|
||||
delete opts.headers?.["Authorization"]
|
||||
throw new MatrixServerError(root, res.status, {baseUrl, url, ...opts})
|
||||
throw new MatrixServerError(root, {baseUrl, url, ...opts})
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ function checkRegistration(reg) {
|
|||
assert(reg.ooye?.server_origin.match(/^https?:\/\//), "server origin must start with http or https")
|
||||
assert.notEqual(reg.ooye?.server_origin.slice(-1), "/", "server origin must not end in slash")
|
||||
assert.match(reg.url, /^https?:/, "url must start with http:// or https://")
|
||||
if (!reg.receive_ephemeral) {
|
||||
reg.receive_ephemeral = true
|
||||
writeRegistration(reg)
|
||||
}
|
||||
}
|
||||
|
||||
/* c8 ignore next 4 */
|
||||
|
|
@ -54,7 +50,6 @@ function getTemplateRegistration(serverName) {
|
|||
],
|
||||
sender_localpart: `${namespace_prefix}bot`,
|
||||
rate_limited: false,
|
||||
receive_ephemeral: true,
|
||||
socket: 6693,
|
||||
ooye: {
|
||||
namespace_prefix,
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ function getPublicUrlForMxc(mxc) {
|
|||
*/
|
||||
function makeMxcPublic(mxc) {
|
||||
assert(hasher, "xxhash is not ready yet")
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||
if (!mediaParts) return undefined
|
||||
|
||||
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
||||
|
|
|
|||
33
src/types.d.ts
vendored
33
src/types.d.ts
vendored
|
|
@ -18,7 +18,6 @@ export type AppServiceRegistrationConfig = {
|
|||
}
|
||||
protocols: [string]
|
||||
rate_limited: boolean
|
||||
receive_ephemeral: boolean
|
||||
socket?: string | number,
|
||||
ooye: {
|
||||
namespace_prefix: string
|
||||
|
|
@ -36,7 +35,6 @@ export type AppServiceRegistrationConfig = {
|
|||
web_password: string
|
||||
time_zone?: string
|
||||
receive_presences: boolean
|
||||
plu_ral_api_key?: string
|
||||
}
|
||||
old_bridge?: {
|
||||
as_token: string
|
||||
|
|
@ -61,7 +59,6 @@ export type InitialAppServiceRegistrationConfig = {
|
|||
}
|
||||
protocols: [string]
|
||||
rate_limited: boolean
|
||||
receive_ephemeral: boolean
|
||||
socket?: string | number,
|
||||
ooye: {
|
||||
namespace_prefix: string
|
||||
|
|
@ -125,28 +122,6 @@ export type PkMessage = {
|
|||
sender: string
|
||||
}
|
||||
|
||||
export type PluRalWebhookMessage = {
|
||||
original_id: string | null
|
||||
proxy_id: string
|
||||
author_id: string
|
||||
channel_id: string
|
||||
member_id: string
|
||||
reason: string
|
||||
webhook_id: string
|
||||
member: PluRalMember
|
||||
}
|
||||
|
||||
export type PluRalMember = {
|
||||
id: string
|
||||
name: string
|
||||
pronouns: string
|
||||
bio: string
|
||||
birthday: string
|
||||
color: number | null
|
||||
avatar_url: string
|
||||
private: boolean
|
||||
}
|
||||
|
||||
export namespace Event {
|
||||
export type Outer<T> = {
|
||||
type: string
|
||||
|
|
@ -421,14 +396,6 @@ export namespace Event {
|
|||
rotation_period_ms?: number
|
||||
rotation_period_msgs?: number
|
||||
}
|
||||
|
||||
export type Outer_M_Typing = {
|
||||
content: {
|
||||
user_ids: string[]
|
||||
}
|
||||
room_id: string
|
||||
type: "m.typing"
|
||||
}
|
||||
}
|
||||
|
||||
export namespace R {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ block body
|
|||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||
.s-popover--arrow.s-popover--arrow__tc
|
||||
+add-roles-menu(guild, guild_id)
|
||||
p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
|
||||
p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
|
||||
|
||||
h3.mt32.fs-category Features
|
||||
.s-card.d-grid.px0.g16
|
||||
|
|
@ -191,14 +191,14 @@ block body
|
|||
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
||||
+discord(channel, true, "Announcement")
|
||||
else
|
||||
.s-empty-state.p8 No Discord channels available.
|
||||
.s-empty-state.p8 All Discord channels are linked.
|
||||
.fl-grow1.s-btn-group.fd-column.w30
|
||||
each room in unlinkedRooms
|
||||
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
|
||||
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
|
||||
+matrix(room, true)
|
||||
else
|
||||
.s-empty-state.p8 No Matrix rooms available.
|
||||
.s-empty-state.p8 All Matrix rooms are linked.
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
div
|
||||
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
||||
|
|
|
|||
|
|
@ -98,11 +98,6 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
|
|||
setResponseHeader(event, "Content-Type", contentType)
|
||||
setResponseHeader(event, "Transfer-Encoding", "chunked")
|
||||
|
||||
const contentDisposition = res.headers.get("content-disposition")
|
||||
if (contentDisposition) {
|
||||
setResponseHeader(event, "Content-Disposition", contentDisposition)
|
||||
}
|
||||
|
||||
if (res.ok && query.success) {
|
||||
return MEDIA_THUMBNAIL_PRESETS[query.data.preset](res.body)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -30,13 +30,12 @@ test("web download matrix: works if a known attachment", async t => {
|
|||
api: {
|
||||
// @ts-ignore
|
||||
async getMedia(mxc, init) {
|
||||
return new Response("", {status: 200, headers: {"content-type": "image/png", "content-disposition": `attachment; filename="wa.png"`}})
|
||||
return new Response("", {status: 200, headers: {"content-type": "image/png"}})
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 200)
|
||||
t.equal(event.node.res.getHeader("content-type"), "image/png")
|
||||
t.equal(event.node.res.getHeader("content-disposition"), `attachment; filename="wa.png"`)
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +64,7 @@ test("web sheet: single emoji", async t => {
|
|||
mxcDownloader: mockGetAndConvertEmoji
|
||||
})
|
||||
t.equal(event.node.res.statusCode, 200)
|
||||
t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVRogc1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH")
|
||||
t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH")
|
||||
})
|
||||
|
||||
test("web sheet: multiple sources", async t => {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ test("web link space: check that OOYE is joined", async t => {
|
|||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
@ -368,7 +368,7 @@ test("web link room: check that bridge can join room (notices lack of via and as
|
|||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
|
||||
},
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
called++
|
||||
|
|
@ -402,7 +402,7 @@ test("web link room: check that bridge can join room (uses via for join attempt)
|
|||
async joinRoom(roomID, _, via) {
|
||||
called++
|
||||
t.deepEqual(via, ["cadence.moe", "hashi.re"])
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
|
||||
throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
|
||||
},
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
called++
|
||||
|
|
@ -710,7 +710,7 @@ test("web unlink room: checks that the channel is bridged", async t => {
|
|||
}))
|
||||
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
|
||||
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_checked, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_checked, row.guild_id, row.custom_topic)
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic)
|
||||
const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
|
||||
t.deepEqual(row, new_row)
|
||||
})
|
||||
|
|
|
|||
3
start.js
3
start.js
|
|
@ -31,9 +31,8 @@ sync.require("./src/m2d/event-dispatcher")
|
|||
|
||||
;(async () => {
|
||||
await migrate.migrate(db)
|
||||
process.stdout.write("Connecting to Discord... ")
|
||||
await discord.cloud.connect()
|
||||
console.log("ok.")
|
||||
console.log("Discord gateway started")
|
||||
sync.require("./src/web/server")
|
||||
await power.applyPower()
|
||||
|
||||
|
|
|
|||
183
test/data.js
183
test/data.js
|
|
@ -5473,189 +5473,6 @@ module.exports = {
|
|||
content: '-# Original Message ID: 1466556003645657118 · <t:1769724599:f>'
|
||||
}
|
||||
]
|
||||
},
|
||||
pk_ping_components_v1: {
|
||||
type: 23,
|
||||
content: "Psst, **Red** (<@772659086046658620>), you have been pinged by <@772659086046658620>.",
|
||||
mentions: [
|
||||
{
|
||||
id: "772659086046658620",
|
||||
username: "cadence.worm",
|
||||
avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
|
||||
discriminator: "0",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "cadence",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
},
|
||||
primary_guild: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
}
|
||||
}
|
||||
],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-25T07:07:02.626000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
id: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
id: 2,
|
||||
style: 5,
|
||||
label: "Jump",
|
||||
url: "https://discord.com/channels/1160893336324931584/1160894080998461480/1440549403667468320"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
id: "1486260105908457653",
|
||||
channel_id: "1160894080998461480",
|
||||
author: {
|
||||
id: "466378653216014359",
|
||||
username: "PluralKit",
|
||||
avatar: "b78ef67a081737a830b60aa47d9ebcd9",
|
||||
discriminator: "4020",
|
||||
public_flags: 65536,
|
||||
flags: 65536,
|
||||
bot: true,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: null,
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
application_id: "466378653216014359",
|
||||
interaction: {
|
||||
id: "1486260103928614932",
|
||||
type: 2,
|
||||
name: "🔔 Ping author",
|
||||
user: {
|
||||
id: "772659086046658620",
|
||||
username: "cadence.worm",
|
||||
avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
|
||||
discriminator: "0",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "cadence",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
},
|
||||
primary_guild: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
}
|
||||
}
|
||||
},
|
||||
webhook_id: "466378653216014359",
|
||||
message_reference: {
|
||||
type: 0,
|
||||
channel_id: "1160894080998461480",
|
||||
message_id: "1440549403667468320",
|
||||
guild_id: "1160893336324931584"
|
||||
},
|
||||
interaction_metadata: {
|
||||
id: "1486260103928614932",
|
||||
type: 2,
|
||||
user: {
|
||||
id: "772659086046658620",
|
||||
username: "cadence.worm",
|
||||
avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
|
||||
discriminator: "0",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "cadence",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
},
|
||||
primary_guild: {
|
||||
identity_guild_id: "532245108070809601",
|
||||
identity_enabled: true,
|
||||
tag: "doll",
|
||||
badge: "dba08126b4e810a0e096cc7cd5bc37f0"
|
||||
}
|
||||
},
|
||||
authorizing_integration_owners: { "0": "1160893336324931584" },
|
||||
name: "🔔 Ping author",
|
||||
command_type: 3,
|
||||
target_message_id: "1440549403667468320"
|
||||
},
|
||||
referenced_message: {
|
||||
type: 0,
|
||||
content: "test",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2025-11-19T03:49:01.948000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1440549403667468320",
|
||||
channel_id: "1160894080998461480",
|
||||
author: {
|
||||
id: "1195662438662680720",
|
||||
username: "special name",
|
||||
avatar: "a82347890f2739e5880cd82b8c1a708e",
|
||||
discriminator: "0000",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
bot: true,
|
||||
global_name: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
application_id: "466378653216014359",
|
||||
webhook_id: "1195662438662680720"
|
||||
}
|
||||
}
|
||||
},
|
||||
message_update: {
|
||||
|
|
|
|||
|
|
@ -95,8 +95,7 @@ WITH a (message_id, channel_id) AS (VALUES
|
|||
('1381212840957972480', '112760669178241024'),
|
||||
('1401760355339862066', '112760669178241024'),
|
||||
('1439351590262800565', '1438284564815548418'),
|
||||
('1404133238414376971', '112760669178241024'),
|
||||
('1440549403667468320', '1160894080998461480'))
|
||||
('1404133238414376971', '112760669178241024'))
|
||||
SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
|
||||
|
||||
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||
|
|
@ -144,8 +143,7 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
|
|||
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
|
||||
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
||||
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
||||
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1),
|
||||
('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1);
|
||||
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
|
||||
|
||||
INSERT INTO file (discord_url, mxc_url) VALUES
|
||||
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue