forked from cadence/out-of-your-element
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8712b635d | |||
| f92d280494 | |||
| 414da4caf4 | |||
| 8779b8b1b0 | |||
| 51e7d8479b | |||
| 8ec7b3f67a | |||
| 401985a861 | |||
| cad815896b | |||
| 47ccf986d8 | |||
| 73f410bcad | |||
| 34fc21fa15 | |||
| ab051f301f | |||
| 51d57051f6 | |||
| f7609b2040 | |||
| b576869764 | |||
| 47dc0504ff | |||
| fbade33ff0 | |||
| e2ab9fa9bf | |||
| 18b6efdd18 | |||
| 313efb29d8 | |||
| af6ea072f3 | |||
| 24c2dee7d3 | |||
| 16867d57fb | |||
| aecfde54c8 | |||
| ee406caf24 | |||
| 9b37705a73 | |||
| 7f7a366cd5 | |||
| 99eacd8c47 | |||
| e0eb7deb2f | |||
| e435b78e28 | |||
| d76936b157 | |||
| dec216c0c2 | |||
| 7781d1e34d | |||
| 93bbc5ea0f | |||
| 43b8b02b40 | |||
| eb676256e4 | |||
| 4815d28aa4 | |||
| 191a98e1dc | |||
| 678a1b77bb | |||
| 2aff1fbd06 | |||
| 92d6ada71b | |||
| d8fb4be509 | |||
| 4698835549 | |||
| e7cbfb9fc9 | |||
| 91bce76fc8 | |||
|
|
12f4103870 | ||
| e28eac6bfa | |||
| 857fb7583b | |||
| 59012d9613 | |||
| 953b3e7741 | |||
| 8c023cc936 | |||
| e9fe820666 | |||
| f742d8572a | |||
| 8224ed5341 | |||
| 0b513b7ee0 | |||
| 07ec9832b2 | |||
| a8b7d64e91 | |||
| 41692b11ff | |||
| d8c0a947f2 | |||
| 5c9e569a2a | |||
| 201814e9f4 |
53 changed files with 2311 additions and 612 deletions
|
|
@ -113,6 +113,7 @@ Total transitive production dependencies: 144
|
||||||
* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer.
|
* (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.
|
* (1) chunk-text: It does what I want.
|
||||||
* (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust.
|
* (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) discord-api-types: Bitfields needed at runtime and types needed for development.
|
||||||
* (0) domino: DOM implementation that's already pulled in by turndown.
|
* (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.
|
* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ Discord display names for normal users are limited to 32 characters. For webhook
|
||||||
|
|
||||||
If the bridge software was restarted, it will attempt to catch up on messages missed while it was offline.
|
If the bridge software was restarted, it will attempt to catch up on messages missed while it was offline.
|
||||||
|
|
||||||
From Discord, for any given channel, if fewer than 50 messages were missed in that given channel, the bridge will catch up and transfer all of them to Matrix. If more than 50 messages were missed in that given channel, the bridge will only bridge the latest message. Happenings that aren't messages, such as edits and reactions to prior messages, might be missed during catch-up.
|
From Discord, for any given channel, if fewer than 100 messages were missed in that given channel, the bridge will catch up and transfer all of them to Matrix. If more than 100 messages were missed in that given channel, the bridge will only bridge the latest message. Happenings that aren't messages, such as edits and reactions to prior messages, might be missed during catch-up.
|
||||||
|
|
||||||
From Matrix, all events should be bridged to Discord.
|
From Matrix, all events should be bridged to Discord.
|
||||||
|
|
||||||
|
|
|
||||||
156
package-lock.json
generated
156
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.4.0",
|
"version": "3.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.4.0",
|
"version": "3.6.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||||
|
|
@ -18,26 +18,27 @@
|
||||||
"@cloudrac3r/pngjs": "^7.0.3",
|
"@cloudrac3r/pngjs": "^7.0.3",
|
||||||
"@cloudrac3r/pug": "^4.0.4",
|
"@cloudrac3r/pug": "^4.0.4",
|
||||||
"@cloudrac3r/stream-type": "^1.0.0",
|
"@cloudrac3r/stream-type": "^1.0.0",
|
||||||
"@cloudrac3r/turndown": "^7.1.4",
|
"@cloudrac3r/turndown": "^7.1.5",
|
||||||
"@stackoverflow/stacks": "^2.5.4",
|
"@stackoverflow/stacks": "^2.5.4",
|
||||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||||
"ansi-colors": "^4.1.3",
|
"ansi-colors": "^4.1.3",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"chunk-text": "^2.0.1",
|
"chunk-text": "^2.0.1",
|
||||||
"cloudstorm": "^0.17.0",
|
"cloudstorm": "^0.19.0",
|
||||||
|
"denque": "^2.1.0",
|
||||||
"discord-api-types": "^0.38.38",
|
"discord-api-types": "^0.38.38",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"entities": "^5.0.0",
|
"entities": "^5.0.0",
|
||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"h3": "^1.15.1",
|
"h3": "^1.15.10",
|
||||||
"heatsync": "^2.7.2",
|
"heatsync": "^2.7.2",
|
||||||
"htmx.org": "^2.0.4",
|
"htmx.org": "^2.0.4",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"prettier-bytes": "^1.0.4",
|
"prettier-bytes": "^1.0.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"snowtransfer": "^0.17.5",
|
"snowtransfer": "^0.19.0",
|
||||||
"try-to-catch": "^4.0.5",
|
"try-to-catch": "^4.0.5",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "^0.1.2",
|
||||||
"xxhash-wasm": "^1.0.2",
|
"xxhash-wasm": "^1.0.2",
|
||||||
|
|
@ -48,7 +49,7 @@
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.1",
|
||||||
"c8": "^11.0.0",
|
"c8": "^11.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"supertape": "^12.0.12"
|
"supertape": "^13.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|
@ -267,18 +268,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudrac3r/turndown": {
|
"node_modules/@cloudrac3r/turndown": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz",
|
||||||
"integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==",
|
"integrity": "sha512-pENoG62B4UM2ecsg57cuSRDa1OV/YFYTFGg1aoQsKUdHBBglhE9I7EwEMYpS96QoM/auyglfaxjgTeqhcSGCIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domino": "^2.1.6"
|
"domino": "^2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1003,9 +1004,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.15",
|
"version": "22.19.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1129,9 +1130,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/better-sqlite3": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "12.8.0",
|
"version": "12.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
|
||||||
"integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
|
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1139,7 +1140,7 @@
|
||||||
"prebuild-install": "^7.1.1"
|
"prebuild-install": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bindings": {
|
"node_modules/bindings": {
|
||||||
|
|
@ -1163,9 +1164,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1316,13 +1317,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cloudstorm": {
|
"node_modules/cloudstorm": {
|
||||||
"version": "0.17.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.19.0.tgz",
|
||||||
"integrity": "sha512-zsd9y5ljNnbxdvDid9TgWePDqo7il4so5spzx6NDwZ67qWQjR96UUhLxJ+BAOdBBSPF9UXFM61dAzC2g918q+A==",
|
"integrity": "sha512-mjuP5o2nS/CgSljajh08KjN6jYMfNBBttSPHc1YUN74f7OkmhUhv8QE8JFDXomxCfdnLNJcqBQtjRLrs8nkfVQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord-api-types": "^0.38.40",
|
"discord-api-types": "^0.38.49",
|
||||||
"snowtransfer": "^0.17.5"
|
"snowtransfer": "^0.19.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
|
|
@ -1366,9 +1367,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie-es": {
|
"node_modules/cookie-es": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
||||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cross-env": {
|
"node_modules/cross-env": {
|
||||||
|
|
@ -1452,11 +1453,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/destr": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
|
|
@ -1473,9 +1483,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/discord-api-types": {
|
"node_modules/discord-api-types": {
|
||||||
"version": "0.38.42",
|
"version": "0.38.49",
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz",
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.49.tgz",
|
||||||
"integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==",
|
"integrity": "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"scripts/actions/documentation"
|
"scripts/actions/documentation"
|
||||||
|
|
@ -1488,9 +1498,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/domino": {
|
"node_modules/domino": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.7.tgz",
|
||||||
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==",
|
"integrity": "sha512-3rcXhx0ixJV2nj8J0tljzejTF73A35LVVdnTQu79UAqTBFEgYPMgGtykMuu/BDqaOZphATku1ddRUn/RtqUHYQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
|
|
@ -1587,9 +1597,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||||
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
|
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
|
@ -1617,9 +1627,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fullstore": {
|
"node_modules/fullstore": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-4.0.2.tgz",
|
||||||
"integrity": "sha512-Y9hN79Q1CFU8akjGnTZoBnTzlA/o8wmtBijJOI8dKCmdC7GLX7OekpLxmbaeRetTOi4OdFGjfsg4c5dxP3jgPw==",
|
"integrity": "sha512-syOev4kA0lZy4VkfBJZ99ZL4cIiSgiKt0G8SpP0kla1tpM1c+V/jBOVY/OqqGtR2XLVcM83SjFPFC3R2YIwqjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1688,14 +1698,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/h3": {
|
"node_modules/h3": {
|
||||||
"version": "1.15.6",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz",
|
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
|
||||||
"integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==",
|
"integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.3",
|
||||||
"crossws": "^0.3.5",
|
"crossws": "^0.3.5",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.6",
|
||||||
"destr": "^2.0.5",
|
"destr": "^2.0.5",
|
||||||
"iron-webcrypto": "^1.2.1",
|
"iron-webcrypto": "^1.2.1",
|
||||||
"node-mock-http": "^1.0.4",
|
"node-mock-http": "^1.0.4",
|
||||||
|
|
@ -1753,9 +1763,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/htmx.org": {
|
"node_modules/htmx.org": {
|
||||||
"version": "2.0.8",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
|
||||||
"integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==",
|
"integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
|
|
@ -1937,9 +1947,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-with-bigint": {
|
"node_modules/json-with-bigint": {
|
||||||
"version": "3.5.7",
|
"version": "3.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
|
||||||
"integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==",
|
"integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -1974,9 +1984,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "11.2.7",
|
"version": "11.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
|
||||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
|
|
@ -2579,12 +2589,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/snowtransfer": {
|
"node_modules/snowtransfer": {
|
||||||
"version": "0.17.5",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.5.tgz",
|
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.19.0.tgz",
|
||||||
"integrity": "sha512-nVI1UJNFoX1ndGFZxB3zb3X5SWtD9hIAcw7wCgVKWvCf42Wg2B4UFIrZWI83HxaSBY0CGbPZmZzZb3RSt/v2wQ==",
|
"integrity": "sha512-Vebj4FCdpUsEKAzqC1p50ol5o09CLnrffvBCpTv0FNFefvXPXwogxR8kr4zJApIRt7oWQVZZVkxAq4XnGlICjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord-api-types": "^0.38.40"
|
"discord-api-types": "^0.38.49"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
|
|
@ -2667,9 +2677,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supertape": {
|
"node_modules/supertape": {
|
||||||
"version": "12.10.5",
|
"version": "13.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supertape/-/supertape-12.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/supertape/-/supertape-13.2.0.tgz",
|
||||||
"integrity": "sha512-1Px+6mhFaqcht3p4tkf3o4G8lbBazvx4pgFngm4vGwWipYm3fykm6SJ4ThXobiaNsptz53CDWA2q4B/2KtmA4w==",
|
"integrity": "sha512-UoxZnyoMOdSJHvbcmD8i28MaGXsA7I0cJ0jr8anT4CkmfaE9M1y5mt9EoXyzfC8UdnQZwXOnJLUwqyKLAeUOug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2856,9 +2866,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uqr": {
|
"node_modules/uqr": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz",
|
||||||
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==",
|
"integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
|
|
@ -3028,9 +3038,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.4.0",
|
"version": "3.6.0",
|
||||||
"description": "A bridge between Matrix and Discord",
|
"description": "A bridge between Matrix and Discord",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -27,26 +27,27 @@
|
||||||
"@cloudrac3r/pngjs": "^7.0.3",
|
"@cloudrac3r/pngjs": "^7.0.3",
|
||||||
"@cloudrac3r/pug": "^4.0.4",
|
"@cloudrac3r/pug": "^4.0.4",
|
||||||
"@cloudrac3r/stream-type": "^1.0.0",
|
"@cloudrac3r/stream-type": "^1.0.0",
|
||||||
"@cloudrac3r/turndown": "^7.1.4",
|
"@cloudrac3r/turndown": "^7.1.5",
|
||||||
"@stackoverflow/stacks": "^2.5.4",
|
"@stackoverflow/stacks": "^2.5.4",
|
||||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||||
"ansi-colors": "^4.1.3",
|
"ansi-colors": "^4.1.3",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"chunk-text": "^2.0.1",
|
"chunk-text": "^2.0.1",
|
||||||
"cloudstorm": "^0.17.0",
|
"cloudstorm": "^0.19.0",
|
||||||
|
"denque": "^2.1.0",
|
||||||
"discord-api-types": "^0.38.38",
|
"discord-api-types": "^0.38.38",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"entities": "^5.0.0",
|
"entities": "^5.0.0",
|
||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"h3": "^1.15.1",
|
"h3": "^1.15.10",
|
||||||
"heatsync": "^2.7.2",
|
"heatsync": "^2.7.2",
|
||||||
"htmx.org": "^2.0.4",
|
"htmx.org": "^2.0.4",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"prettier-bytes": "^1.0.4",
|
"prettier-bytes": "^1.0.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"snowtransfer": "^0.17.5",
|
"snowtransfer": "^0.19.0",
|
||||||
"try-to-catch": "^4.0.5",
|
"try-to-catch": "^4.0.5",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "^0.1.2",
|
||||||
"xxhash-wasm": "^1.0.2",
|
"xxhash-wasm": "^1.0.2",
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.1",
|
||||||
"c8": "^11.0.0",
|
"c8": "^11.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"supertape": "^12.0.12"
|
"supertape": "^13.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps start.js",
|
"start": "node --enable-source-maps start.js",
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ const {prompt} = require("enquirer")
|
||||||
|
|
||||||
reg.ooye.web_password = passwordResponse.web_password
|
reg.ooye.web_password = passwordResponse.web_password
|
||||||
writeRegistration(reg)
|
writeRegistration(reg)
|
||||||
console.log("Saved. Restart Out Of Your Element to apply this change.")
|
console.log("Saved. This change should be applied instantly.")
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ function defineEchoHandler() {
|
||||||
console.log("✅ Emojis are ready...")
|
console.log("✅ Emojis are ready...")
|
||||||
|
|
||||||
// set profile data on discord...
|
// set profile data on discord...
|
||||||
await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")})
|
await discord.snow.user.editSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")})
|
||||||
console.log("✅ Discord profile updated...")
|
console.log("✅ Discord profile updated...")
|
||||||
|
|
||||||
// set profile data on homeserver...
|
// set profile data on homeserver...
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,16 @@ async function channelToKState(channel, guild, di) {
|
||||||
// Don't overwrite room topic if the topic has been customised
|
// Don't overwrite room topic if the topic has been customised
|
||||||
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
||||||
|
|
||||||
|
// Make voice channels be a Matrix voice room (MSC3417)
|
||||||
|
if (channel.type === DiscordTypes.ChannelType.GuildVoice) {
|
||||||
|
creationContent.type = "org.matrix.msc3417.call"
|
||||||
|
channelKState["org.matrix.msc3401.call/"] = {
|
||||||
|
"m.intent": "m.room",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": customName || channel.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't add a space parent if it's self service
|
// Don't add a space parent if it's self service
|
||||||
// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
|
// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
|
||||||
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
|
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,17 @@ test("channel2room: read-only discord channel", async t => {
|
||||||
t.equal(api.getCalled(), 2)
|
t.equal(api.getCalled(), 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("channel2room: voice channel", async t => {
|
||||||
|
const api = mockAPI(t)
|
||||||
|
const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState))
|
||||||
|
t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call")
|
||||||
|
t.deepEqual(state["org.matrix.msc3401.call/"], {
|
||||||
|
"m.intent": "m.room",
|
||||||
|
"m.name": "🍞丨[8user] Piece",
|
||||||
|
"m.type": "m.voice"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("convertNameAndTopic: custom name and topic", t => {
|
test("convertNameAndTopic: custom name and topic", t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
_convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"),
|
_convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select} = passthrough
|
const {discord, sync, db, select} = passthrough
|
||||||
|
/** @type {import("../../matrix/mreq")} */
|
||||||
|
const mreq = sync.require("../../matrix/mreq")
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/file")} */
|
/** @type {import("../../matrix/file")} */
|
||||||
|
|
@ -237,8 +239,12 @@ async function syncSpaceExpressions(data, checkBeforeSync) {
|
||||||
try {
|
try {
|
||||||
existing = await api.getStateEvent(spaceID, "im.ponies.room_emotes", eventKey)
|
existing = await api.getStateEvent(spaceID, "im.ponies.room_emotes", eventKey)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e instanceof mreq.MatrixServerError && e.httpStatus < 400) {
|
||||||
// State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake.
|
// 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)
|
existing = fn([], guild)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isDeepStrictEqual(existing, content)) return
|
if (isDeepStrictEqual(existing, content)) return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ async function emojisToState(emojis, guild) {
|
||||||
if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit.
|
if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`)
|
e["emoji"] = {
|
||||||
|
name: emoji.name,
|
||||||
|
id: emoji.id
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,15 @@
|
||||||
|
|
||||||
const {EventEmitter} = require("events")
|
const {EventEmitter} = require("events")
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {select} = passthrough
|
const {select, sync, from} = passthrough
|
||||||
|
/** @type {import("../../matrix/utils")} */
|
||||||
|
const utils = sync.require("../../matrix/utils")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
||||||
|
(or before the it has finished being bridged to an event).
|
||||||
|
In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
||||||
|
*/
|
||||||
|
|
||||||
const DEBUG_RETRIGGER = false
|
const DEBUG_RETRIGGER = false
|
||||||
|
|
||||||
|
|
@ -12,81 +20,140 @@ function debugRetrigger(message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paused = new Set()
|
const storage = new class {
|
||||||
const emitter = new EventEmitter()
|
/** @private @type {Set<string>} */
|
||||||
|
paused = new Set()
|
||||||
|
/** @private @type {Map<string, ((found: Boolean) => any)[]>} id -> list of resolvers */
|
||||||
|
resolves = new Map()
|
||||||
|
/** @private @type {Map<string, ReturnType<setTimeout>>} id -> timer */
|
||||||
|
timers = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of storage is to store `resolve` and call it at a later time.
|
||||||
|
* @param {string} id
|
||||||
|
* @param {(found: Boolean) => any} resolve
|
||||||
|
*/
|
||||||
|
store(id, resolve) {
|
||||||
|
debugRetrigger(`[retrigger] STORE id = ${id}`)
|
||||||
|
this.resolves.set(id, (this.resolves.get(id) || []).concat(resolve)) // add to list in map value
|
||||||
|
if (!this.timers.has(id)) {
|
||||||
|
debugRetrigger(`[retrigger] SET TIMER id = ${id}`)
|
||||||
|
this.timers.set(id, setTimeout(() => this.resolve(id, false), 60 * 1000).unref()) // 1 minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} id */
|
||||||
|
isNotPaused(id) {
|
||||||
|
return !storage.paused.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} id */
|
||||||
|
pause(id) {
|
||||||
|
debugRetrigger(`[retrigger] PAUSE id = ${id}`)
|
||||||
|
this.paused.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through `resolves` storage and resolve them all. (Also resets timer/paused.)
|
||||||
|
* @param {string} id
|
||||||
|
* @param {boolean} value
|
||||||
|
*/
|
||||||
|
resolve(id, value) {
|
||||||
|
if (this.paused.has(id)) {
|
||||||
|
debugRetrigger(`[retrigger] RESUME id = ${id}`)
|
||||||
|
this.paused.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.resolves.has(id)) {
|
||||||
|
debugRetrigger(`[retrigger] RESOLVE ${value} id = ${id}`)
|
||||||
|
const fns = this.resolves.get(id) || []
|
||||||
|
this.resolves.delete(id)
|
||||||
|
for (const fn of fns) {
|
||||||
|
fn(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timers.has(id)) {
|
||||||
|
clearTimeout(this.timers.get(id))
|
||||||
|
this.timers.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
* @param {string} id
|
||||||
* (or before the it has finished being bridged to an event).
|
* @param {(found: Boolean) => any} resolve
|
||||||
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
* @param {boolean} existsInDatabase
|
||||||
* @template {(...args: any[]) => any} T
|
|
||||||
* @param {string} inputID
|
|
||||||
* @param {T} fn
|
|
||||||
* @param {Parameters<T>} rest
|
|
||||||
* @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered
|
|
||||||
*/
|
*/
|
||||||
function eventNotFoundThenRetrigger(inputID, fn, ...rest) {
|
function waitFor(id, resolve, existsInDatabase) {
|
||||||
if (!paused.has(inputID)) {
|
if (existsInDatabase && storage.isNotPaused(id)) { // if event already exists and isn't paused then resolve immediately
|
||||||
if (inputID.match(/^[0-9]+$/)) {
|
debugRetrigger(`[retrigger] EXISTS id = ${id}`)
|
||||||
const eventID = select("event_message", "event_id", {message_id: inputID}).pluck().get()
|
return resolve(true)
|
||||||
if (eventID) {
|
|
||||||
debugRetrigger(`[retrigger] OK mid <-> eid = ${inputID} <-> ${eventID}`)
|
|
||||||
return false // event was found so don't retrigger
|
|
||||||
}
|
|
||||||
} else if (inputID.match(/^\$/)) {
|
|
||||||
const messageID = select("event_message", "message_id", {event_id: inputID}).pluck().get()
|
|
||||||
if (messageID) {
|
|
||||||
debugRetrigger(`[retrigger] OK eid <-> mid = ${inputID} <-> ${messageID}`)
|
|
||||||
return false // message was found so don't retrigger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugRetrigger(`[retrigger] WAIT id = ${inputID}`)
|
// doesn't exist. wait for it to exist. storage will resolve true if it exists or false if it timed out
|
||||||
emitter.once(inputID, () => {
|
return storage.store(id, resolve)
|
||||||
debugRetrigger(`[retrigger] TRIGGER id = ${inputID}`)
|
}
|
||||||
fn(...rest)
|
|
||||||
})
|
const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw()
|
||||||
// if the event never arrives, don't trigger the callback, just clean up
|
/**
|
||||||
setTimeout(() => {
|
* @param {string} eventID
|
||||||
if (emitter.listeners(inputID).length) {
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
debugRetrigger(`[retrigger] EXPIRE id = ${inputID}`)
|
*/
|
||||||
}
|
function waitForEvent(eventID) {
|
||||||
emitter.removeAllListeners(inputID)
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
}, 60 * 1000) // 1 minute
|
waitFor(eventID, resolve, !!GET_EVENT_PREPARED.get(eventID))
|
||||||
return true // event was not found, then retrigger
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw()
|
||||||
|
/**
|
||||||
|
* @param {string} messageID
|
||||||
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
|
*/
|
||||||
|
function waitForMessage(messageID) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
waitFor(messageID, resolve, !!GET_MESSAGE_PREPARED.get(messageID))
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw()
|
||||||
|
/**
|
||||||
|
* @param {string} eventID
|
||||||
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
|
*/
|
||||||
|
function waitForReactionEvent(eventID) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
waitFor(eventID, resolve, !!GET_REACTION_EVENT_PREPARED.get(utils.getEventIDHash(eventID)))
|
||||||
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything calling retrigger during the callback will be paused and retriggered after the callback resolves.
|
* Anything calling retrigger during the callback will be paused and retriggered after the callback resolves.
|
||||||
* @template T
|
* @template T
|
||||||
* @param {string} messageID
|
* @param {string} id
|
||||||
* @param {Promise<T>} promise
|
* @param {Promise<T>} promise
|
||||||
* @returns {Promise<T>}
|
* @returns {Promise<T>}
|
||||||
*/
|
*/
|
||||||
async function pauseChanges(messageID, promise) {
|
async function pauseChanges(id, promise) {
|
||||||
try {
|
try {
|
||||||
debugRetrigger(`[retrigger] PAUSE id = ${messageID}`)
|
storage.pause(id)
|
||||||
paused.add(messageID)
|
|
||||||
return await promise
|
return await promise
|
||||||
} finally {
|
} finally {
|
||||||
debugRetrigger(`[retrigger] RESUME id = ${messageID}`)
|
finishedBridging(id)
|
||||||
paused.delete(messageID)
|
|
||||||
messageFinishedBridging(messageID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers any pending operations that were waiting on the corresponding event ID.
|
* Triggers any pending operations that were waiting on the corresponding event ID.
|
||||||
* @param {string} messageID
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
function messageFinishedBridging(messageID) {
|
function finishedBridging(id) {
|
||||||
if (emitter.listeners(messageID).length) {
|
storage.resolve(id, true)
|
||||||
debugRetrigger(`[retrigger] EMIT id = ${messageID}`)
|
|
||||||
}
|
|
||||||
emitter.emit(messageID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger
|
module.exports.waitForMessage = waitForMessage
|
||||||
module.exports.messageFinishedBridging = messageFinishedBridging
|
module.exports.waitForEvent = waitForEvent
|
||||||
|
module.exports.waitForReactionEvent = waitForReactionEvent
|
||||||
module.exports.pauseChanges = pauseChanges
|
module.exports.pauseChanges = pauseChanges
|
||||||
|
module.exports.finishedBridging = finishedBridging
|
||||||
|
|
@ -60,8 +60,7 @@ async function sendMessage(message, channel, guild, row) {
|
||||||
const detailedResultsMessage = await pollEnd.endPoll(message)
|
const detailedResultsMessage = await pollEnd.endPoll(message)
|
||||||
if (detailedResultsMessage) {
|
if (detailedResultsMessage) {
|
||||||
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
|
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
|
||||||
const channelID = threadParent ? threadParent : message.channel_id
|
const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent)
|
||||||
const threadID = threadParent ? message.channel_id : undefined
|
|
||||||
sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
|
sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, select, db} = passthrough
|
const {discord, select, db} = passthrough
|
||||||
|
|
||||||
|
|
@ -70,12 +69,18 @@ async function doSpeedbump(messageID) {
|
||||||
* 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.
|
* 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 {string} channelID
|
* @param {string} channelID
|
||||||
* @param {string} messageID
|
* @param {string} messageID
|
||||||
|
* @param {string} [userID] if provided, only slow down the message when the user has used PK before
|
||||||
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
|
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
|
||||||
*/
|
*/
|
||||||
async function maybeDoSpeedbump(channelID, messageID) {
|
async function maybeDoSpeedbump(channelID, messageID, userID) {
|
||||||
let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
|
let row = select("channel_room", ["room_id", "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?.thread_parent) row = select("channel_room", ["room_id", "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
|
if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump
|
||||||
|
if (userID) {
|
||||||
|
if (row.speedbump_webhook_id === userID) return {affected: false, row} // shortcut
|
||||||
|
const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get()
|
||||||
|
if (!userHasProxy) return {affected: false, row} // user has not used PK before, no speedbump
|
||||||
|
}
|
||||||
const affected = await doSpeedbump(messageID)
|
const affected = await doSpeedbump(messageID)
|
||||||
return {affected, row} // maybe affected, and there is a speedbump
|
return {affected, row} // maybe affected, and there is a speedbump
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
||||||
* @param {Omit<DiscordTypes.APIAttachment, "id" | "proxy_url">} attachment
|
* @param {Omit<DiscordTypes.APIAttachment, "id" | "proxy_url" | "flags">} attachment
|
||||||
* @param {boolean} [alwaysLink]
|
* @param {boolean} [alwaysLink]
|
||||||
*/
|
*/
|
||||||
async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
|
|
@ -256,8 +256,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
||||||
const username = interaction.member?.nick || interaction.user.global_name || interaction.user.username
|
const username = interaction.member?.nick || interaction.user.global_name || interaction.user.username
|
||||||
const thinkingText = isThinkingInteraction ? " — interaction loading..." : ""
|
const thinkingText = isThinkingInteraction ? " — interaction loading..." : ""
|
||||||
return {
|
return {
|
||||||
body: `↪️ ${username} used \`/${interaction.name}\`${thinkingText}`,
|
body: `❭ ${username} used \`/${interaction.name}\`${thinkingText}`,
|
||||||
html: `<blockquote><sub>↪️ ${mxid ? tag`<a href="https://matrix.to/#/${mxid}">${username}</a>` : username} used <code>/${interaction.name}</code>${thinkingText}</sub></blockquote>`
|
html: `<blockquote><sub>❭ ${mxid ? tag`<a href="https://matrix.to/#/${mxid}">${username}</a>` : username} used <code>/${interaction.name}</code>${thinkingText}</sub></blockquote>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,8 +265,9 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
||||||
* @param {any} newEvents merge into events
|
* @param {any} newEvents merge into events
|
||||||
* @param {any} events will be modified
|
* @param {any} events will be modified
|
||||||
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
|
* @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) {
|
function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) {
|
||||||
let prev = events.at(-1)
|
let prev = events.at(-1)
|
||||||
for (const ne of newEvents) {
|
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)
|
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
|
||||||
|
|
@ -278,6 +279,8 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype) {
|
||||||
rep.addLine(ne.body, ne.formatted_body)
|
rep.addLine(ne.body, ne.formatted_body)
|
||||||
prev.body = rep.body
|
prev.body = rep.body
|
||||||
prev.formatted_body = rep.formattedBody
|
prev.formatted_body = rep.formattedBody
|
||||||
|
} else if (forceMerge) {
|
||||||
|
throw new Error("Unable to merge events")
|
||||||
} else {
|
} else {
|
||||||
events.push(ne)
|
events.push(ne)
|
||||||
}
|
}
|
||||||
|
|
@ -357,6 +360,17 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) {
|
||||||
|
return [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
msgtype: "m.emote",
|
||||||
|
body: `set this room to receive announcements from ${message.content}`,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: tag`set this room to receive announcements from <strong>${message.content}</strong>`,
|
||||||
|
"m.mentions": {}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
|
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
|
||||||
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||||
|
|
||||||
|
|
@ -543,7 +557,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.
|
// 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.
|
// So we scan the message ahead of time for all its emojis and ensure they are in the DB.
|
||||||
const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)]
|
const emojiMatches = [...content.matchAll(/<(a?):([^:>]+):([0-9]+)>/g)]
|
||||||
await Promise.all(emojiMatches.map(match => {
|
await Promise.all(emojiMatches.map(match => {
|
||||||
const id = match[3]
|
const id = match[3]
|
||||||
const name = match[2]
|
const name = match[2]
|
||||||
|
|
@ -629,8 +643,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
const flags = message.flags || 0
|
const flags = message.flags || 0
|
||||||
if (flags & DiscordTypes.MessageFlags.IsCrosspost) {
|
if (flags & DiscordTypes.MessageFlags.IsCrosspost) {
|
||||||
body = `[🔀 ${message.author.username}]\n` + body
|
body = `[↷ ${message.author.username}]\n` + body
|
||||||
html = `🔀 <strong>${message.author.username}</strong><br>` + html
|
html = `↷ <strong>${message.author.username}</strong><br>` + html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback body/formatted_body for replies
|
// Fallback body/formatted_body for replies
|
||||||
|
|
@ -658,7 +672,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
const match = repliedToEventSenderMxid.match(/^@([^:]*)/)
|
const match = repliedToEventSenderMxid.match(/^@([^:]*)/)
|
||||||
assert(match)
|
assert(match)
|
||||||
repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever
|
repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever
|
||||||
repliedToUserHtml = `<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
repliedToUserHtml = tag`<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
||||||
} else {
|
} else {
|
||||||
repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user"
|
repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user"
|
||||||
repliedToUserHtml = repliedToDisplayName
|
repliedToUserHtml = repliedToDisplayName
|
||||||
|
|
@ -683,6 +697,12 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
+ html
|
+ html
|
||||||
body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions
|
body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions
|
||||||
+ "\n\n" + body
|
+ "\n\n" + body
|
||||||
|
} else if (referenced.type === DiscordTypes.MessageType.UserJoin) {
|
||||||
|
// Discord user join messages are bridged as joins, not text events. Generate substitute text for reply.
|
||||||
|
const joinerMxid = select("sim", "mxid", {user_id: referenced.author.id}).pluck().get()
|
||||||
|
const joinerHtml = joinerMxid ? tag`<a href="https://matrix.to/#/${joinerMxid}">${repliedToDisplayName}</a>` : tag`<strong>${repliedToDisplayName}</strong>`
|
||||||
|
html = `<blockquote>${joinerHtml} joined the room</blockquote>` + html
|
||||||
|
body = `> ${repliedToDisplayName} joined the room\n\n` + body
|
||||||
} else { // repliedToUnknownEvent
|
} else { // repliedToUnknownEvent
|
||||||
const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp)
|
const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp)
|
||||||
html = `<blockquote>In reply to ${dateDisplay} from ${repliedToDisplayName}:`
|
html = `<blockquote>In reply to ${dateDisplay} from ${repliedToDisplayName}:`
|
||||||
|
|
@ -751,20 +771,20 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
if (row && "event_id" in row) {
|
if (row && "event_id" in row) {
|
||||||
const via = await getViaServersMemo(row.room_id)
|
const via = await getViaServersMemo(row.room_id)
|
||||||
forwardedNotice.addLine(
|
forwardedNotice.addLine(
|
||||||
`[🔀 Forwarded from #${roomName}]`,
|
`[↷ Forwarded from #${roomName}]`,
|
||||||
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}/${row.event_id}?${via}">[jump to event]</a></em>`
|
tag`↷ <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}/${row.event_id}?${via}">[jump to event]</a></em>`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const via = await getViaServersMemo(room.room_id)
|
const via = await getViaServersMemo(room.room_id)
|
||||||
forwardedNotice.addLine(
|
forwardedNotice.addLine(
|
||||||
`[🔀 Forwarded from #${roomName}]`,
|
`[↷ Forwarded from #${roomName}]`,
|
||||||
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}?${via}">[jump to room]</a></em>`
|
tag`↷ <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}?${via}">[jump to room]</a></em>`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
forwardedNotice.addLine(
|
forwardedNotice.addLine(
|
||||||
`[🔀 Forwarded message]`,
|
`[↷ Forwarded message]`,
|
||||||
tag`🔀 <em>Forwarded message</em>`
|
tag`↷ <em>Forwarded message</em>`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -950,7 +970,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// May only be a section accessory or in an action row (up to 5)
|
// May only be a section accessory or in an action row (up to 5)
|
||||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||||
assert(component.label) // required for Discord to validate link buttons
|
assert(component.label) // required for Discord to validate link buttons
|
||||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
const link = await transformContentMessageLinks(component.url)
|
||||||
|
stack.msb.add(`[${component.label} ${link}] `, tag`<a href="${link}">${component.label}</a> `)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -963,8 +984,20 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
const {body, formatted_body} = stack.msb.get()
|
const {body, formatted_body} = stack.msb.get()
|
||||||
if (body.trim().length) {
|
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")
|
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
|
// Then polls
|
||||||
|
|
@ -1110,7 +1143,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let body = stickerItem.name
|
let body = stickerItem.name
|
||||||
const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id)
|
const sticker = guild.stickers?.find(sticker => sticker.id === stickerItem.id)
|
||||||
if (sticker && sticker.description) body += ` - ${sticker.description}`
|
if (sticker && sticker.description) body += ` - ${sticker.description}`
|
||||||
return {
|
return {
|
||||||
$type: "m.sticker",
|
$type: "m.sticker",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {messageToEvent} = require("./message-to-event")
|
const {messageToEvent} = require("./message-to-event")
|
||||||
const data = require("../../../test/data")
|
const data = require("../../../test/data")
|
||||||
|
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
||||||
|
|
||||||
test("message2event components: pk question mark output", async t => {
|
test("message2event components: pk question mark output", async t => {
|
||||||
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
|
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
|
||||||
|
|
@ -77,3 +78,24 @@ test("message2event components: pk question mark output", async t => {
|
||||||
msgtype: "m.text",
|
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": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ test("message2event embeds: interaction loading", async t => {
|
||||||
const events = await messageToEvent(data.interaction_message.thinking_interaction, data.guild.general, {})
|
const events = await messageToEvent(data.interaction_message.thinking_interaction, data.guild.general, {})
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
body: "↪️ Brad used `/stats` — interaction loading...",
|
body: "❭ Brad used `/stats` — interaction loading...",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">Brad</a> used <code>/stats</code> — interaction loading...</sub></blockquote>",
|
formatted_body: "<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">Brad</a> used <code>/stats</code> — interaction loading...</sub></blockquote>",
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
}])
|
}])
|
||||||
|
|
@ -22,12 +22,12 @@ test("message2event embeds: nothing but a field", async t => {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "↪️ PapiOphidian used `/stats`"
|
body: "❭ PapiOphidian used `/stats`"
|
||||||
+ "\n| ### Amanda 🎵#2192 :online:"
|
+ "\n| ### Amanda 🎵#2192 :online:"
|
||||||
+ "\n| willow tree, branch 0"
|
+ "\n| willow tree, branch 0"
|
||||||
+ "\n| **❯ Uptime:**\n| 3m 55s\n| **❯ Memory:**\n| 64.45MB",
|
+ "\n| **❯ Uptime:**\n| 3m 55s\n| **❯ Memory:**\n| 64.45MB",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: '<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
formatted_body: '<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
||||||
+ '<blockquote><p><strong>Amanda 🎵#2192 <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ\" title=\":online:\" alt=\":online:\">'
|
+ '<blockquote><p><strong>Amanda 🎵#2192 <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ\" title=\":online:\" alt=\":online:\">'
|
||||||
+ '<br>willow tree, branch 0</strong>'
|
+ '<br>willow tree, branch 0</strong>'
|
||||||
+ '<br><strong>❯ Uptime:</strong><br>3m 55s'
|
+ '<br><strong>❯ Uptime:</strong><br>3m 55s'
|
||||||
|
|
@ -153,10 +153,10 @@ test("message2event embeds: title without url", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "↪️ PapiOphidian used `/stats`"
|
body: "❭ PapiOphidian used `/stats`"
|
||||||
+ "\n| ## Hi, I'm Amanda!\n| \n| I condone pirating music!",
|
+ "\n| ## Hi, I'm Amanda!\n| \n| I condone pirating music!",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: '<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
formatted_body: '<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
||||||
+ `<blockquote><p><strong>Hi, I'm Amanda!</strong></p><p>I condone pirating music!</p></blockquote>`,
|
+ `<blockquote><p><strong>Hi, I'm Amanda!</strong></p><p>I condone pirating music!</p></blockquote>`,
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}])
|
}])
|
||||||
|
|
@ -167,10 +167,10 @@ test("message2event embeds: url without title", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "↪️ PapiOphidian used `/stats`"
|
body: "❭ PapiOphidian used `/stats`"
|
||||||
+ "\n| I condone pirating music!",
|
+ "\n| I condone pirating music!",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: '<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
formatted_body: '<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
||||||
+ `<blockquote><p>I condone pirating music!</p></blockquote>`,
|
+ `<blockquote><p>I condone pirating music!</p></blockquote>`,
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}])
|
}])
|
||||||
|
|
@ -181,10 +181,10 @@ test("message2event embeds: author without url", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "↪️ PapiOphidian used `/stats`"
|
body: "❭ PapiOphidian used `/stats`"
|
||||||
+ "\n| ## Amanda\n| \n| I condone pirating music!",
|
+ "\n| ## Amanda\n| \n| I condone pirating music!",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: '<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
formatted_body: '<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
||||||
+ `<blockquote><p><strong>Amanda</strong></p><p>I condone pirating music!</p></blockquote>`,
|
+ `<blockquote><p><strong>Amanda</strong></p><p>I condone pirating music!</p></blockquote>`,
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}])
|
}])
|
||||||
|
|
@ -195,10 +195,10 @@ test("message2event embeds: author url without name", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "↪️ PapiOphidian used `/stats`"
|
body: "❭ PapiOphidian used `/stats`"
|
||||||
+ "\n| I condone pirating music!",
|
+ "\n| I condone pirating music!",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: '<blockquote><sub>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
formatted_body: '<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">PapiOphidian</a> used <code>/stats</code></sub></blockquote>'
|
||||||
+ `<blockquote><p>I condone pirating music!</p></blockquote>`,
|
+ `<blockquote><p>I condone pirating music!</p></blockquote>`,
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}])
|
}])
|
||||||
|
|
@ -209,9 +209,9 @@ test("message2event embeds: 4 images", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
body: "[↷ Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "🔀 <em>Forwarded message</em><br><blockquote><a href=\"https://fixupx.com/i/status/2032003668787020046\">https://fixupx.com/i/status/2032003668787020046</a></blockquote>",
|
formatted_body: "↷ <em>Forwarded message</em><br><blockquote><a href=\"https://fixupx.com/i/status/2032003668787020046\">https://fixupx.com/i/status/2032003668787020046</a></blockquote>",
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}, {
|
}, {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const {MatrixServerError} = require("../../matrix/mreq")
|
||||||
const data = require("../../../test/data")
|
const data = require("../../../test/data")
|
||||||
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
const {db} = require("../../passthrough")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
|
|
@ -733,6 +734,31 @@ test("message2event: reply to a Discord message that wasn't bridged", async t =>
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("message2event: reply to a Discord member join (who didn't join on Matrix)", async t => {
|
||||||
|
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "<blockquote><strong>PEASANT!!</strong> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("message2event: reply to a Discord member join (who did join on Matrix)", async t => {
|
||||||
|
db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1461677775554478161', 'peasant321_76775', 'peasant321_76775', '@_ooye_peasant321_76775:cadence.moe')").run()
|
||||||
|
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: `<blockquote><a href="https://matrix.to/#/@_ooye_peasant321_76775:cadence.moe">PEASANT!!</a> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party`,
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
test("message2event: simple written @mention for matrix user", async t => {
|
test("message2event: simple written @mention for matrix user", async t => {
|
||||||
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
|
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
|
||||||
api: {
|
api: {
|
||||||
|
|
@ -1142,6 +1168,19 @@ test("message2event: type 4 channel name change", async t => {
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("message2event: type 12 channel follow add", async t => {
|
||||||
|
const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general)
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": {},
|
||||||
|
msgtype: "m.emote",
|
||||||
|
body: "set this room to receive announcements from PluralKit #downtime",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "set this room to receive announcements from <strong>PluralKit #downtime</strong>",
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
test("message2event: thread start message reference", async t => {
|
test("message2event: thread start message reference", async t => {
|
||||||
const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, {
|
const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, {
|
||||||
api: {
|
api: {
|
||||||
|
|
@ -1234,9 +1273,9 @@ test("message2event: crossposted announcements say where they are crossposted fr
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "[🔀 Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands",
|
body: "[↷ Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "🔀 <strong>Chewey Bot Official Server #announcements</strong><br>All text based commands are now inactive on Chewey Bot<br>To continue using commands you'll need to use them as slash commands"
|
formatted_body: "↷ <strong>Chewey Bot Official Server #announcements</strong><br>All text based commands are now inactive on Chewey Bot<br>To continue using commands you'll need to use them as slash commands"
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1305,9 +1344,9 @@ test("message2event: forwarded image", async t => {
|
||||||
t.deepEqual(events, [
|
t.deepEqual(events, [
|
||||||
{
|
{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
body: "[🔀 Forwarded message]",
|
body: "[↷ Forwarded message]",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "🔀 <em>Forwarded message</em>",
|
formatted_body: "↷ <em>Forwarded message</em>",
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
},
|
},
|
||||||
|
|
@ -1346,10 +1385,10 @@ test("message2event: constructed forwarded message", async t => {
|
||||||
t.deepEqual(events, [
|
t.deepEqual(events, [
|
||||||
{
|
{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
body: "[🔀 Forwarded from #wonderland]"
|
body: "[↷ Forwarded from #wonderland]"
|
||||||
+ "\n» What's cooking, good looking? :hipposcope:",
|
+ "\n» What's cooking, good looking? :hipposcope:",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `🔀 <em>Forwarded from wonderland <a href="https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$tBIT8mO7XTTCgIINyiAIy6M2MSoPAdJenRl_RLyYuaE?via=cadence.moe&via=matrix.org">[jump to event]</a></em>`
|
formatted_body: `↷ <em>Forwarded from wonderland <a href="https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$tBIT8mO7XTTCgIINyiAIy6M2MSoPAdJenRl_RLyYuaE?via=cadence.moe&via=matrix.org">[jump to event]</a></em>`
|
||||||
+ `<br><blockquote>What's cooking, good looking? <img data-mx-emoticon height="32" src="mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc" title=":hipposcope:" alt=":hipposcope:"></blockquote>`,
|
+ `<br><blockquote>What's cooking, good looking? <img data-mx-emoticon height="32" src="mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc" title=":hipposcope:" alt=":hipposcope:"></blockquote>`,
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
|
|
@ -1405,10 +1444,10 @@ test("message2event: constructed forwarded text", async t => {
|
||||||
t.deepEqual(events, [
|
t.deepEqual(events, [
|
||||||
{
|
{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
body: "[🔀 Forwarded from #amanda-spam]"
|
body: "[↷ Forwarded from #amanda-spam]"
|
||||||
+ "\n» What's cooking, good looking?",
|
+ "\n» What's cooking, good looking?",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `🔀 <em>Forwarded from amanda-spam <a href="https://matrix.to/#/!CzvdIdUQXgUjDVKxeU:cadence.moe?via=cadence.moe&via=matrix.org">[jump to room]</a></em>`
|
formatted_body: `↷ <em>Forwarded from amanda-spam <a href="https://matrix.to/#/!CzvdIdUQXgUjDVKxeU:cadence.moe?via=cadence.moe&via=matrix.org">[jump to room]</a></em>`
|
||||||
+ `<br><blockquote>What's cooking, good looking?</blockquote>`,
|
+ `<br><blockquote>What's cooking, good looking?</blockquote>`,
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
|
|
@ -1428,10 +1467,10 @@ test("message2event: don't scan forwarded messages for mentions", async t => {
|
||||||
t.deepEqual(events, [
|
t.deepEqual(events, [
|
||||||
{
|
{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
body: "[🔀 Forwarded message]"
|
body: "[↷ Forwarded message]"
|
||||||
+ "\n» If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114",
|
+ "\n» If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `🔀 <em>Forwarded message</em>`
|
formatted_body: `↷ <em>Forwarded message</em>`
|
||||||
+ `<br><blockquote>If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile <a href="https://social.luca.run/@luca/113950834185678114">https://social.luca.run/@luca/113950834185678114</a></blockquote>`,
|
+ `<br><blockquote>If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile <a href="https://social.luca.run/@luca/113950834185678114">https://social.luca.run/@luca/113950834185678114</a></blockquote>`,
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.text"
|
msgtype: "m.text"
|
||||||
|
|
@ -1781,9 +1820,9 @@ test("message2event: forwarded message with unreferenced mention", async t => {
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "[🔀 Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)",
|
body: "[↷ Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "🔀 <em>Forwarded message</em><br><blockquote>@unknown-user:<br>🎞️ Uploaded file: <a href=\"https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4\">2022-10-18_16-49-46.mp4</a> (51 MB)</blockquote>",
|
formatted_body: "↷ <em>Forwarded message</em><br><blockquote>@unknown-user:<br>🎞️ Uploaded file: <a href=\"https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4\">2022-10-18_16-49-46.mp4</a> (51 MB)</blockquote>",
|
||||||
"m.mentions": {}
|
"m.mentions": {}
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function pinsToList(pins, kstate) {
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const result = []
|
const result = []
|
||||||
for (const pin of pins.items) {
|
for (const pin of pins.items) {
|
||||||
const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get()
|
const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get()
|
||||||
if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID)
|
if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID)
|
||||||
}
|
}
|
||||||
result.reverse()
|
result.reverse()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function removeReaction(data, reactions, key) {
|
||||||
// Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have
|
// Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have
|
||||||
// reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user.
|
// reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user.
|
||||||
// Also need to clean up the database.
|
// Also need to clean up the database.
|
||||||
const hash = utils.getEventIDHash(event.event_id)
|
const hash = utils.getEventIDHash(eventID)
|
||||||
removals.push({eventID, mxid: null, hash})
|
removals.push({eventID, mxid: null, hash})
|
||||||
}
|
}
|
||||||
if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) {
|
if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ const {sync} = passthrough
|
||||||
/** @type {import("./discord-packets")} */
|
/** @type {import("./discord-packets")} */
|
||||||
const discordPackets = sync.require("./discord-packets")
|
const discordPackets = sync.require("./discord-packets")
|
||||||
|
|
||||||
|
const CONNECTION_DEBUG = false
|
||||||
|
|
||||||
class DiscordClient {
|
class DiscordClient {
|
||||||
/**
|
/**
|
||||||
* @param {string} discordToken
|
* @param {string} discordToken
|
||||||
|
|
@ -24,7 +26,7 @@ class DiscordClient {
|
||||||
const intents = [
|
const intents = [
|
||||||
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
|
"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",
|
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS",
|
||||||
"MESSAGE_CONTENT"
|
"MESSAGE_CONTENT", "GUILD_MEMBERS"
|
||||||
]
|
]
|
||||||
if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES")
|
if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES")
|
||||||
this.discordToken = discordToken
|
this.discordToken = discordToken
|
||||||
|
|
@ -59,6 +61,7 @@ class DiscordClient {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CONNECTION_DEBUG) {
|
||||||
const addEventLogger = (eventName, logName) => {
|
const addEventLogger = (eventName, logName) => {
|
||||||
this.cloud.on(eventName, (...args) => {
|
this.cloud.on(eventName, (...args) => {
|
||||||
const d = new Date().toISOString().slice(0, 19)
|
const d = new Date().toISOString().slice(0, 19)
|
||||||
|
|
@ -69,6 +72,7 @@ class DiscordClient {
|
||||||
addEventLogger("disconnected", "Disconnected")
|
addEventLogger("disconnected", "Disconnected")
|
||||||
addEventLogger("ready", "Ready")
|
addEventLogger("ready", "Ready")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = DiscordClient
|
module.exports = DiscordClient
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
// Discord library internals type beat
|
const assert = require("assert")
|
||||||
|
const {scheduler} = require("timers/promises")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const passthrough = require("../passthrough")
|
const passthrough = require("../passthrough")
|
||||||
const {sync, db} = passthrough
|
const {sync} = passthrough
|
||||||
|
|
||||||
const utils = {
|
/** @type {import("../matrix/homeserver-status")} */
|
||||||
/**
|
const homeserverStatus = sync.require("../matrix/homeserver-status")
|
||||||
|
|
||||||
|
let checkedHomeserver = false
|
||||||
|
|
||||||
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
* @param {import("cloudstorm").IGatewayMessage} message
|
* @param {import("cloudstorm").IGatewayMessage} message
|
||||||
* @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate
|
* @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate
|
||||||
*/
|
*/
|
||||||
async onPacket(client, message, listen) {
|
async function onPacket(client, message, listen) {
|
||||||
// requiring this later so that the client is already constructed by the time event-dispatcher is loaded
|
// requiring this later so that the client is already constructed by the time event-dispatcher is loaded
|
||||||
/** @type {typeof import("./event-dispatcher")} */
|
/** @type {typeof import("./event-dispatcher")} */
|
||||||
const eventDispatcher = sync.require("./event-dispatcher")
|
const eventDispatcher = sync.require("./event-dispatcher")
|
||||||
|
|
@ -48,6 +51,31 @@ const utils = {
|
||||||
|
|
||||||
if (listen === "full") {
|
if (listen === "full") {
|
||||||
try {
|
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.checkMissedExpressions(message.d)
|
||||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||||
await eventDispatcher.checkMissedPins(client, message.d)
|
await eventDispatcher.checkMissedPins(client, message.d)
|
||||||
|
|
@ -152,6 +180,28 @@ const utils = {
|
||||||
|
|
||||||
// Event dispatcher for OOYE bridge operations
|
// Event dispatcher for OOYE bridge operations
|
||||||
if (listen === "full" && message.t) {
|
if (listen === "full" && message.t) {
|
||||||
|
const alwaysRealTimeEvents = ["PRESENCE_UPDATE"]
|
||||||
|
if (alwaysRealTimeEvents.includes(message.t) || homeserverStatus.homeserverStatus.isRealTime()) {
|
||||||
|
dispatchPacketToBridge(client, message)
|
||||||
|
} else {
|
||||||
|
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 {
|
try {
|
||||||
if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") {
|
if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") {
|
||||||
await eventDispatcher.onSomeReactionsRemoved(client, message.d)
|
await eventDispatcher.onSomeReactionsRemoved(client, message.d)
|
||||||
|
|
@ -166,8 +216,7 @@ const utils = {
|
||||||
// Let OOYE try to handle errors too
|
// Let OOYE try to handle errors too
|
||||||
await eventDispatcher.onError(client, e, message)
|
await eventDispatcher.onError(client, e, message)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = utils
|
module.exports.onPacket = onPacket
|
||||||
|
module.exports.dispatchPacketToBridge = dispatchPacketToBridge
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const {id: botID} = require("../../addbot")
|
||||||
const {sync, db, select, from} = require("../passthrough")
|
const {sync, db, select, from} = require("../passthrough")
|
||||||
|
|
||||||
/** @type {import("./actions/send-message")}) */
|
/** @type {import("./actions/send-message")}) */
|
||||||
|
|
@ -38,6 +39,8 @@ const removeMember = sync.require("./actions/remove-member")
|
||||||
const vote = sync.require("./actions/poll-vote")
|
const vote = sync.require("./actions/poll-vote")
|
||||||
/** @type {import("../m2d/event-dispatcher")} */
|
/** @type {import("../m2d/event-dispatcher")} */
|
||||||
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
||||||
|
/** @type {import("../m2d/actions/redact.js")} */
|
||||||
|
const redact = sync.require("../m2d/actions/redact.js")
|
||||||
/** @type {import("../discord/interactions/matrix-info")} */
|
/** @type {import("../discord/interactions/matrix-info")} */
|
||||||
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ module.exports = {
|
||||||
|
|
||||||
matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage)
|
matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage)
|
||||||
|
|
||||||
const channelID = gatewayMessage.d["channel_id"]
|
const channelID = gatewayMessage.d?.["channel_id"]
|
||||||
if (!channelID) return
|
if (!channelID) return
|
||||||
const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||||
if (!roomID) return
|
if (!roomID) return
|
||||||
|
|
@ -102,7 +105,7 @@ module.exports = {
|
||||||
// console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`)
|
// console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`)
|
||||||
let messages
|
let messages
|
||||||
try {
|
try {
|
||||||
messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50})
|
messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 100})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === `{"message": "Missing Access", "code": 50001}`) { // pathetic error handling from SnowTransfer
|
if (e.message === `{"message": "Missing Access", "code": 50001}`) { // pathetic error handling from SnowTransfer
|
||||||
console.log(`[check missed messages] no permissions to look back in channel ${channel.name} (${channel.id})`)
|
console.log(`[check missed messages] no permissions to look back in channel ${channel.name} (${channel.id})`)
|
||||||
|
|
@ -310,13 +313,13 @@ module.exports = {
|
||||||
|
|
||||||
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
|
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
|
||||||
|
|
||||||
const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id)
|
const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id)
|
||||||
if (affected) return
|
if (affected) return
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await sendMessage.sendMessage(message, channel, guild, row)
|
await sendMessage.sendMessage(message, channel, guild, row)
|
||||||
|
|
||||||
retrigger.messageFinishedBridging(message.id)
|
retrigger.finishedBridging(message.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -332,13 +335,11 @@ module.exports = {
|
||||||
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
|
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.
|
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
|
||||||
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
|
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id)
|
||||||
if (affected) return
|
if (affected) return
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return
|
if (!await retrigger.waitForMessage(data.id)) return
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -375,6 +376,16 @@ module.exports = {
|
||||||
* @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data
|
* @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data
|
||||||
*/
|
*/
|
||||||
async onSomeReactionsRemoved(client, data) {
|
async onSomeReactionsRemoved(client, data) {
|
||||||
|
// Don't attempt to double-bridge our own m2d deleted reactions back to Matrix
|
||||||
|
if ("user_id" in data && data.user_id === botID) {
|
||||||
|
const emojiIdOrName = data.emoji.id || data.emoji.name
|
||||||
|
const i = redact.m2dDeletedReactions.findIndex(x => data.message_id === x.messageID && emojiIdOrName === x.emojiIdOrName)
|
||||||
|
if (i !== -1) {
|
||||||
|
redact.m2dDeletedReactions.splice(i, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await removeReaction.removeSomeReactions(data)
|
await removeReaction.removeSomeReactions(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -384,7 +395,7 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
async MESSAGE_DELETE(client, data) {
|
async MESSAGE_DELETE(client, data) {
|
||||||
speedbump.onMessageDelete(data.id)
|
speedbump.onMessageDelete(data.id)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_DELETE, client, data)) return
|
if (!await retrigger.waitForMessage(data.id)) return
|
||||||
await deleteMessage.deleteMessage(data)
|
await deleteMessage.deleteMessage(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -432,12 +443,12 @@ module.exports = {
|
||||||
* @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data
|
* @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data
|
||||||
*/
|
*/
|
||||||
async MESSAGE_POLL_VOTE_ADD(client, data) {
|
async MESSAGE_POLL_VOTE_ADD(client, data) {
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_ADD, client, data)) return
|
if (!await retrigger.waitForMessage(data.message_id)) return
|
||||||
await vote.addVote(data)
|
await vote.addVote(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
async MESSAGE_POLL_VOTE_REMOVE(client, data) {
|
async MESSAGE_POLL_VOTE_REMOVE(client, data) {
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_REMOVE, client, data)) return
|
if (!await retrigger.waitForMessage(data.message_id)) return
|
||||||
await vote.removeVote(data)
|
await vote.removeVote(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
42
src/db/migrations/0037-remove-leaked-webhooks.js
Normal file
42
src/db/migrations/0037-remove-leaked-webhooks.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
const {discord, db, from, select, sync} = require("../../passthrough")
|
||||||
|
/** @type {import("../../discord/utils")} */
|
||||||
|
const dUtils = sync.require("../../discord/utils")
|
||||||
|
|
||||||
|
const ones = "₀₁₂₃₄₅₆₇₈₉"
|
||||||
|
const tens = "0123456789"
|
||||||
|
|
||||||
|
/* c8 ignore start */
|
||||||
|
|
||||||
|
module.exports = async function(db) {
|
||||||
|
// added tolerance to https://discordstatus.com/incidents/4hpm4454hxtx
|
||||||
|
const OUTAGE_START = 1778263200000
|
||||||
|
const OUTAGE_END = 1778284800000
|
||||||
|
|
||||||
|
const startSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_START)
|
||||||
|
const endSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_END)
|
||||||
|
|
||||||
|
const affectedChannels = from("message_room").join("historical_channel_room", "historical_room_index")
|
||||||
|
.pluck("reference_channel_id").selectUnsafe("DISTINCT reference_channel_id")
|
||||||
|
.and("WHERE message_id >= ? AND message_id <= ? AND length(message_id) = ?").all(startSnowflake, endSnowflake, startSnowflake.length)
|
||||||
|
let affectedWebhooks = select("webhook", ["channel_id", "webhook_id", "webhook_token"], {channel_id: affectedChannels}).all()
|
||||||
|
affectedWebhooks = affectedWebhooks.filter(w => BigInt(w.webhook_id) < BigInt(endSnowflake)) // if webhook ID is already newly generated then no need to replace
|
||||||
|
|
||||||
|
if (affectedWebhooks.length) {
|
||||||
|
process.stdout.write(` revoking ${affectedWebhooks.length} possibly compromised webhooks... `)
|
||||||
|
for (let counter = 1; counter <= affectedWebhooks.length; counter++) {
|
||||||
|
const webhook = affectedWebhooks[counter-1]
|
||||||
|
|
||||||
|
await discord.snow.webhook.deleteWebhookToken(webhook.webhook_id, webhook.webhook_token, "Webhook token possibly compromised during 8th May 2026 outage").catch(e => {
|
||||||
|
if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) {
|
||||||
|
// OK
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(webhook.channel_id)
|
||||||
|
|
||||||
|
process.stdout.write(String(counter).at(-1) === "0" ? tens[(counter/10)%10] : ones[counter%10])
|
||||||
|
}
|
||||||
|
process.stdout.write("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/db/migrations/0038-fix-emoji-file-format.sql
Normal file
5
src/db/migrations/0038-fix-emoji-file-format.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
DELETE FROM emoji WHERE mxc_url NOT IN (SELECT mxc_url FROM file WHERE discord_url LIKE 'https://cdn.discordapp.com/emojis/%.webp%');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -104,6 +104,16 @@ class From {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluckUnsafe(col) {
|
||||||
|
/** @type {Pluck<Table, any>} */
|
||||||
|
// @ts-ignore
|
||||||
|
const r = this
|
||||||
|
r.cols = [col]
|
||||||
|
r.makeColsSafe = false
|
||||||
|
r.isPluck = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} sql
|
* @param {string} sql
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,8 @@ test("orm: select unsafe works (to select complex column names that can't be typ
|
||||||
.all()
|
.all()
|
||||||
t.equal(results[0].power_level, 150)
|
t.equal(results[0].power_level, 150)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("orm: pluck unsafe works (to select complex column names that can't be type verified)", t => {
|
||||||
|
const result = from("channel_room").where({guild_id: "112760669178241024"}).pluckUnsafe("count(*)").get()
|
||||||
|
t.equal(result, 7)
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ async function _interact({guild_id, data}, {api}) {
|
||||||
// from Matrix
|
// from Matrix
|
||||||
const event = await api.getEvent(message.room_id, message.event_id)
|
const event = await api.getEvent(message.room_id, message.event_id)
|
||||||
const via = await utils.getViaServersQuery(message.room_id, api)
|
const via = await utils.getViaServersQuery(message.room_id, api)
|
||||||
|
|
||||||
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
||||||
assert(channelsInGuild)
|
assert(channelsInGuild)
|
||||||
const inChannels = channelsInGuild
|
const inChannels = channelsInGuild
|
||||||
|
|
@ -61,6 +62,11 @@ async function _interact({guild_id, data}, {api}) {
|
||||||
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
||||||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||||
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
||||||
|
let inChannelsText = inChannels.map(c => `<#${c.id}>`).join(" • ")
|
||||||
|
if (inChannelsText.length > 1024) {
|
||||||
|
inChannelsText = `In ${inChannels.length} channels`
|
||||||
|
}
|
||||||
|
|
||||||
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
||||||
let name = matrixMember?.displayname || event.sender
|
let name = matrixMember?.displayname || event.sender
|
||||||
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||||
|
|
@ -98,7 +104,7 @@ async function _interact({guild_id, data}, {api}) {
|
||||||
color: 0x0dbd8b,
|
color: 0x0dbd8b,
|
||||||
fields: [{
|
fields: [{
|
||||||
name: "In Channels",
|
name: "In Channels",
|
||||||
value: inChannels.map(c => `<#${c.id}>`).join(" • ")
|
value: inChannelsText
|
||||||
}, {
|
}, {
|
||||||
name: "\u200b",
|
name: "\u200b",
|
||||||
value: idInfo
|
value: idInfo
|
||||||
|
|
|
||||||
|
|
@ -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
|
// User must have EVERY permission in default_member_permissions to be able to use the command
|
||||||
|
|
||||||
function registerInteractions() {
|
function registerInteractions() {
|
||||||
discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
discord.snow.interaction.editApplicationCommands(id, [{
|
||||||
name: "Matrix info",
|
name: "Matrix info",
|
||||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||||
type: DiscordTypes.ApplicationCommandType.Message,
|
type: DiscordTypes.ApplicationCommandType.Message,
|
||||||
|
|
@ -91,15 +91,6 @@ function registerInteractions() {
|
||||||
async function dispatchInteraction(interaction) {
|
async function dispatchInteraction(interaction) {
|
||||||
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
||||||
try {
|
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") {
|
if (interactionId === "Matrix info") {
|
||||||
await matrixInfo.interact(interaction)
|
await matrixInfo.interact(interaction)
|
||||||
} else if (interactionId === "invite") {
|
} else if (interactionId === "invite") {
|
||||||
|
|
@ -122,10 +113,11 @@ async function dispatchInteraction(interaction) {
|
||||||
await ping.interact(interaction)
|
await ping.interact(interaction)
|
||||||
} else if (interactionId === "privacy") {
|
} else if (interactionId === "privacy") {
|
||||||
await privacy.interact(interaction)
|
await privacy.interact(interaction)
|
||||||
|
} else if (interactionId.startsWith("POLL_")) {
|
||||||
|
await poll.interact(interaction)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown interaction ${interactionId}`)
|
throw new Error(`Unknown interaction ${interactionId}`)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let stackLines = null
|
let stackLines = null
|
||||||
if (e.stack) {
|
if (e.stack) {
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,406 @@ function filterTo(xs, fn) {
|
||||||
return filtered
|
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",
|
||||||
|
"accesslog",
|
||||||
|
"actionscript",
|
||||||
|
"ada",
|
||||||
|
"adoc",
|
||||||
|
"alan",
|
||||||
|
"angelscript",
|
||||||
|
"ansi",
|
||||||
|
"apache",
|
||||||
|
"apacheconf",
|
||||||
|
"applescript",
|
||||||
|
"arcade",
|
||||||
|
"arduino",
|
||||||
|
"arm",
|
||||||
|
"armasm",
|
||||||
|
"as",
|
||||||
|
"asc",
|
||||||
|
"asciidoc",
|
||||||
|
"aspectj",
|
||||||
|
"ass",
|
||||||
|
"atom",
|
||||||
|
"autohotkey",
|
||||||
|
"autoit",
|
||||||
|
"avrasm",
|
||||||
|
"awk",
|
||||||
|
"axapta",
|
||||||
|
"bash",
|
||||||
|
"basic",
|
||||||
|
"bat",
|
||||||
|
"bbcode",
|
||||||
|
"bf",
|
||||||
|
"bind",
|
||||||
|
"blade",
|
||||||
|
"bnf",
|
||||||
|
"brainfuck",
|
||||||
|
"c",
|
||||||
|
"c++",
|
||||||
|
"cal",
|
||||||
|
"capnp",
|
||||||
|
"capnproto",
|
||||||
|
"cc",
|
||||||
|
"chaos",
|
||||||
|
"chapel",
|
||||||
|
"chpl",
|
||||||
|
"cisco",
|
||||||
|
"clj",
|
||||||
|
"clojure",
|
||||||
|
"cls",
|
||||||
|
"cmake.in",
|
||||||
|
"cmake",
|
||||||
|
"cmd",
|
||||||
|
"coffee",
|
||||||
|
"coffeescript",
|
||||||
|
"console",
|
||||||
|
"coq",
|
||||||
|
"cos",
|
||||||
|
"cpc",
|
||||||
|
"cpp",
|
||||||
|
"cr",
|
||||||
|
"craftcms",
|
||||||
|
"crm",
|
||||||
|
"crmsh",
|
||||||
|
"crystal",
|
||||||
|
"cs",
|
||||||
|
"csharp",
|
||||||
|
"cshtml",
|
||||||
|
"cson",
|
||||||
|
"csp",
|
||||||
|
"css",
|
||||||
|
"csv",
|
||||||
|
"cxx",
|
||||||
|
"cypher",
|
||||||
|
"d",
|
||||||
|
"dart",
|
||||||
|
"delphi",
|
||||||
|
"dfm",
|
||||||
|
"diff",
|
||||||
|
"django",
|
||||||
|
"dns",
|
||||||
|
"docker",
|
||||||
|
"dockerfile",
|
||||||
|
"dos",
|
||||||
|
"dpr",
|
||||||
|
"dsconfig",
|
||||||
|
"dst",
|
||||||
|
"dts",
|
||||||
|
"dust",
|
||||||
|
"dylan",
|
||||||
|
"ebnf",
|
||||||
|
"elixir",
|
||||||
|
"elm",
|
||||||
|
"erl",
|
||||||
|
"erlang",
|
||||||
|
"ex",
|
||||||
|
"extempore",
|
||||||
|
"f90",
|
||||||
|
"f95",
|
||||||
|
"fix",
|
||||||
|
"fortran",
|
||||||
|
"freepascal",
|
||||||
|
"fs",
|
||||||
|
"fsharp",
|
||||||
|
"gams",
|
||||||
|
"gauss",
|
||||||
|
"gawk",
|
||||||
|
"gcode",
|
||||||
|
"gdscript",
|
||||||
|
"gemspec",
|
||||||
|
"gf",
|
||||||
|
"gherkin",
|
||||||
|
"glsl",
|
||||||
|
"gms",
|
||||||
|
"gn",
|
||||||
|
"gni",
|
||||||
|
"go",
|
||||||
|
"godot",
|
||||||
|
"golang",
|
||||||
|
"golo",
|
||||||
|
"gololang",
|
||||||
|
"gradle",
|
||||||
|
"graph",
|
||||||
|
"groovy",
|
||||||
|
"gss",
|
||||||
|
"gyp",
|
||||||
|
"h",
|
||||||
|
"h++",
|
||||||
|
"haml",
|
||||||
|
"handlebars",
|
||||||
|
"haskell",
|
||||||
|
"haxe",
|
||||||
|
"hbs",
|
||||||
|
"hcl",
|
||||||
|
"hh",
|
||||||
|
"hpp",
|
||||||
|
"hs",
|
||||||
|
"html.handlebars",
|
||||||
|
"html.hbs",
|
||||||
|
"html",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"hx",
|
||||||
|
"hxx",
|
||||||
|
"hy",
|
||||||
|
"hylang",
|
||||||
|
"i",
|
||||||
|
"i7",
|
||||||
|
"iced",
|
||||||
|
"iecst",
|
||||||
|
"inform7",
|
||||||
|
"ini",
|
||||||
|
"ino",
|
||||||
|
"instances",
|
||||||
|
"iol",
|
||||||
|
"irb",
|
||||||
|
"irpf90",
|
||||||
|
"java",
|
||||||
|
"javascript",
|
||||||
|
"jinja",
|
||||||
|
"jolie",
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"jsp",
|
||||||
|
"jsx",
|
||||||
|
"julia-repl",
|
||||||
|
"julia",
|
||||||
|
"k",
|
||||||
|
"kaos",
|
||||||
|
"kdb",
|
||||||
|
"kotlin",
|
||||||
|
"kt",
|
||||||
|
"lasso",
|
||||||
|
"lassoscript",
|
||||||
|
"lazarus",
|
||||||
|
"ldif",
|
||||||
|
"leaf",
|
||||||
|
"lean",
|
||||||
|
"less",
|
||||||
|
"lfm",
|
||||||
|
"lisp",
|
||||||
|
"livecodeserver",
|
||||||
|
"livescript",
|
||||||
|
"ln",
|
||||||
|
"lock",
|
||||||
|
"log",
|
||||||
|
"lpr",
|
||||||
|
"ls",
|
||||||
|
"ls",
|
||||||
|
"lua",
|
||||||
|
"mak",
|
||||||
|
"make",
|
||||||
|
"makefile",
|
||||||
|
"markdown",
|
||||||
|
"mathematica",
|
||||||
|
"matlab",
|
||||||
|
"mawk",
|
||||||
|
"maxima",
|
||||||
|
"md",
|
||||||
|
"mel",
|
||||||
|
"mercury",
|
||||||
|
"mirc",
|
||||||
|
"mizar",
|
||||||
|
"mk",
|
||||||
|
"mkd",
|
||||||
|
"mkdown",
|
||||||
|
"ml",
|
||||||
|
"ml",
|
||||||
|
"mm",
|
||||||
|
"mma",
|
||||||
|
"mojolicious",
|
||||||
|
"monkey",
|
||||||
|
"moon",
|
||||||
|
"moonscript",
|
||||||
|
"mrc",
|
||||||
|
"n1ql",
|
||||||
|
"nawk",
|
||||||
|
"nc",
|
||||||
|
"never",
|
||||||
|
"nginx",
|
||||||
|
"nginxconf",
|
||||||
|
"nim",
|
||||||
|
"nimrod",
|
||||||
|
"nix",
|
||||||
|
"nsis",
|
||||||
|
"obj-c",
|
||||||
|
"obj-c++",
|
||||||
|
"objc",
|
||||||
|
"objective-c++",
|
||||||
|
"objectivec",
|
||||||
|
"ocaml",
|
||||||
|
"ocl",
|
||||||
|
"ol",
|
||||||
|
"openscad",
|
||||||
|
"osascript",
|
||||||
|
"oxygene",
|
||||||
|
"p21",
|
||||||
|
"parser3",
|
||||||
|
"pas",
|
||||||
|
"pascal",
|
||||||
|
"patch",
|
||||||
|
"pcmk",
|
||||||
|
"perl",
|
||||||
|
"pf.conf",
|
||||||
|
"pf",
|
||||||
|
"pgsql",
|
||||||
|
"php",
|
||||||
|
"php3",
|
||||||
|
"php4",
|
||||||
|
"php5",
|
||||||
|
"php6",
|
||||||
|
"php7",
|
||||||
|
"pl",
|
||||||
|
"plaintext",
|
||||||
|
"plist",
|
||||||
|
"pm",
|
||||||
|
"podspec",
|
||||||
|
"pony",
|
||||||
|
"postgres",
|
||||||
|
"postgresql",
|
||||||
|
"powershell",
|
||||||
|
"pp",
|
||||||
|
"processing",
|
||||||
|
"profile",
|
||||||
|
"prolog",
|
||||||
|
"properties",
|
||||||
|
"proto",
|
||||||
|
"protobuf",
|
||||||
|
"ps",
|
||||||
|
"ps1",
|
||||||
|
"puppet",
|
||||||
|
"py",
|
||||||
|
"pycon",
|
||||||
|
"python-repl",
|
||||||
|
"python",
|
||||||
|
"qml",
|
||||||
|
"r",
|
||||||
|
"razor-cshtml",
|
||||||
|
"razor",
|
||||||
|
"rb",
|
||||||
|
"re",
|
||||||
|
"reasonml",
|
||||||
|
"rebol",
|
||||||
|
"red-system",
|
||||||
|
"red",
|
||||||
|
"redbol",
|
||||||
|
"rf",
|
||||||
|
"rib",
|
||||||
|
"robot",
|
||||||
|
"rpm-spec",
|
||||||
|
"rpm-specfile",
|
||||||
|
"rpm",
|
||||||
|
"rs",
|
||||||
|
"rsl",
|
||||||
|
"rss",
|
||||||
|
"ruby",
|
||||||
|
"ruleslanguage",
|
||||||
|
"rust",
|
||||||
|
"sas",
|
||||||
|
"SAS",
|
||||||
|
"sc",
|
||||||
|
"scad",
|
||||||
|
"scala",
|
||||||
|
"scheme",
|
||||||
|
"sci",
|
||||||
|
"scilab",
|
||||||
|
"scl",
|
||||||
|
"scss",
|
||||||
|
"sh",
|
||||||
|
"shell",
|
||||||
|
"shexc",
|
||||||
|
"smali",
|
||||||
|
"smalltalk",
|
||||||
|
"sml",
|
||||||
|
"sol",
|
||||||
|
"solidity",
|
||||||
|
"spec",
|
||||||
|
"specfile",
|
||||||
|
"sql",
|
||||||
|
"srt",
|
||||||
|
"ssa",
|
||||||
|
"st",
|
||||||
|
"stan",
|
||||||
|
"stanfuncs",
|
||||||
|
"stata",
|
||||||
|
"step",
|
||||||
|
"stp",
|
||||||
|
"structured-text",
|
||||||
|
"styl",
|
||||||
|
"stylus",
|
||||||
|
"subunit",
|
||||||
|
"supercollider",
|
||||||
|
"svelte",
|
||||||
|
"svg",
|
||||||
|
"swift",
|
||||||
|
"tao",
|
||||||
|
"tap",
|
||||||
|
"tcl",
|
||||||
|
"terraform",
|
||||||
|
"tex",
|
||||||
|
"text",
|
||||||
|
"tf",
|
||||||
|
"thor",
|
||||||
|
"thrift",
|
||||||
|
"tk",
|
||||||
|
"toml",
|
||||||
|
"tp",
|
||||||
|
"ts",
|
||||||
|
"tsql",
|
||||||
|
"tsx",
|
||||||
|
"ttml",
|
||||||
|
"twig",
|
||||||
|
"txt",
|
||||||
|
"typescript",
|
||||||
|
"unicorn-rails-log",
|
||||||
|
"v",
|
||||||
|
"vala",
|
||||||
|
"vb",
|
||||||
|
"vba",
|
||||||
|
"vbnet",
|
||||||
|
"vbs",
|
||||||
|
"vbscript",
|
||||||
|
"verilog",
|
||||||
|
"vhdl",
|
||||||
|
"vim",
|
||||||
|
"vtt",
|
||||||
|
"wl",
|
||||||
|
"x++",
|
||||||
|
"x86asm",
|
||||||
|
"xhtml",
|
||||||
|
"xjb",
|
||||||
|
"xl",
|
||||||
|
"xml",
|
||||||
|
"xpath",
|
||||||
|
"xq",
|
||||||
|
"xquery",
|
||||||
|
"xsd",
|
||||||
|
"xsl",
|
||||||
|
"xtlang",
|
||||||
|
"xtm",
|
||||||
|
"yaml",
|
||||||
|
"yml",
|
||||||
|
"zep",
|
||||||
|
"zephir",
|
||||||
|
"zone",
|
||||||
|
"zsh"
|
||||||
|
])
|
||||||
|
|
||||||
module.exports.getPermissions = getPermissions
|
module.exports.getPermissions = getPermissions
|
||||||
module.exports.getDefaultPermissions = getDefaultPermissions
|
module.exports.getDefaultPermissions = getDefaultPermissions
|
||||||
module.exports.hasPermission = hasPermission
|
module.exports.hasPermission = hasPermission
|
||||||
|
|
@ -194,3 +594,5 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
|
||||||
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
|
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
|
||||||
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
|
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
|
||||||
module.exports.filterTo = filterTo
|
module.exports.filterTo = filterTo
|
||||||
|
module.exports.swapThreadID = swapThreadID
|
||||||
|
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
*/
|
*/
|
||||||
async function addReaction(event) {
|
async function addReaction(event) {
|
||||||
// Wait until the corresponding channel and message have already been bridged
|
// Wait until the corresponding channel and message have already been bridged
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, () => as.emit("type:m.reaction", event))) return
|
if (!await retrigger.waitForEvent(event.content["m.relates_to"].event_id)) return
|
||||||
|
|
||||||
// These will exist because it passed retrigger
|
// These will exist because it passed retrigger
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
|
|
@ -50,6 +50,8 @@ async function addReaction(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji, original_encoding) VALUES (?, ?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding, key)
|
db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji, original_encoding) VALUES (?, ?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding, key)
|
||||||
|
|
||||||
|
retrigger.finishedBridging(event.event_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.addReaction = addReaction
|
module.exports.addReaction = addReaction
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ async function getAndConvertEmoji(mxc) {
|
||||||
const res = await api.getMedia(mxc, {signal: abortController.signal})
|
const res = await api.getMedia(mxc, {signal: abortController.signal})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
const root = await res.json()
|
const root = await res.json()
|
||||||
throw new mreq.MatrixServerError(root, {mxc})
|
throw new mreq.MatrixServerError(root, res.status, {mxc})
|
||||||
}
|
}
|
||||||
const readable = stream.Readable.fromWeb(res.body)
|
const readable = stream.Readable.fromWeb(res.body)
|
||||||
return emojiSheetConverter.convertImageStream(readable, () => {
|
return emojiSheetConverter.convertImageStream(readable, () => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ const utils = sync.require("../../matrix/utils")
|
||||||
/** @type {import("../../d2m/actions/retrigger")} */
|
/** @type {import("../../d2m/actions/retrigger")} */
|
||||||
const retrigger = sync.require("../../d2m/actions/retrigger")
|
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
|
|
||||||
|
/** @type {{messageID: string, emojiIdOrName: string}[]} */
|
||||||
|
const m2dDeletedReactions = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
|
|
@ -24,6 +27,21 @@ async function deleteMessage(event) {
|
||||||
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
|
*/
|
||||||
|
async function removeMessageEvent(event) {
|
||||||
|
// Could be for removing a message or suppressing embeds. For more information, the message needs to be bridged first.
|
||||||
|
if (!await retrigger.waitForEvent(event.redacts)) return
|
||||||
|
|
||||||
|
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||||
|
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||||
|
await suppressEmbeds(event)
|
||||||
|
} else {
|
||||||
|
await deleteMessage(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
|
|
@ -41,11 +59,20 @@ async function suppressEmbeds(event) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
async function removeReaction(event) {
|
async function removeReaction(event) {
|
||||||
|
if (!await retrigger.waitForReactionEvent(event.redacts)) return
|
||||||
|
|
||||||
const hash = utils.getEventIDHash(event.redacts)
|
const hash = utils.getEventIDHash(event.redacts)
|
||||||
const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
.select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
|
.select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
|
||||||
if (!row) return
|
if (!row) return
|
||||||
|
// See how many Matrix-side reactions there are, and delete if it's the last one
|
||||||
|
const numberOfReactions = from("reaction").where({message_id: row.message_id, encoded_emoji: row.encoded_emoji}).pluckUnsafe("count(*)").get()
|
||||||
|
if (numberOfReactions === 1) {
|
||||||
|
// If a unicode emoji, the name is already the Discord preferred version because that's what was added and stored to encoded_emoji
|
||||||
|
const emojiIdOrName = decodeURIComponent(row.encoded_emoji).split(":").slice(-1)[0]
|
||||||
|
m2dDeletedReactions.push({messageID: row.message_id, emojiIdOrName})
|
||||||
await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji)
|
await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji)
|
||||||
|
}
|
||||||
db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash)
|
db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,18 +81,12 @@ async function removeReaction(event) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
async function handle(event) {
|
async function handle(event) {
|
||||||
// If this is for removing a reaction, try it
|
// Don't know if it's a redaction for a reaction or an event, try both at the same time (otherwise waitFor will block)
|
||||||
await removeReaction(event)
|
await Promise.all([
|
||||||
|
removeMessageEvent(event),
|
||||||
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
removeReaction(event)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.redacts, () => as.emit("type:m.room.redaction", event))) return
|
])
|
||||||
|
|
||||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
|
||||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
|
||||||
await suppressEmbeds(event)
|
|
||||||
} else {
|
|
||||||
await deleteMessage(event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.handle = handle
|
module.exports.handle = handle
|
||||||
|
module.exports.m2dDeletedReactions = m2dDeletedReactions
|
||||||
|
|
@ -22,7 +22,7 @@ async function getAndResizeSticker(mxc) {
|
||||||
const res = await api.getMedia(mxc)
|
const res = await api.getMedia(mxc)
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
const root = await res.json()
|
const root = await res.json()
|
||||||
throw new mreq.MatrixServerError(root, {mxc})
|
throw new mreq.MatrixServerError(root, res.status, {mxc})
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamIn = Readable.fromWeb(res.body)
|
const streamIn = Readable.fromWeb(res.body)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ async function updatePins(pins, prev) {
|
||||||
const diff = diffPins.diffPins(pins, prev)
|
const diff = diffPins.diffPins(pins, prev)
|
||||||
for (const [event_id, added] of diff) {
|
for (const [event_id, added] of diff) {
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
.select("reference_channel_id", "message_id").get()
|
.select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get()
|
||||||
if (!row) continue
|
if (!row) continue
|
||||||
if (added) {
|
if (added) {
|
||||||
discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
|
discord.snow.channel.createChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
|
||||||
} else {
|
} else {
|
||||||
discord.snow.channel.removeChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix")
|
discord.snow.channel.deleteChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const {Readable} = require("stream")
|
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const crypto = require("crypto")
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {sync, discord, db, select} = passthrough
|
const {sync, db, select} = passthrough
|
||||||
|
|
||||||
const {reg} = require("../../matrix/read-registration")
|
/** @type {import("../../discord/utils")} */
|
||||||
/** @type {import("../../matrix/api")} */
|
const dUtils = sync.require("../../discord/utils")
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
/** @type {import("../../matrix/utils")} */
|
|
||||||
const utils = sync.require("../../matrix/utils")
|
|
||||||
/** @type {import("../converters/poll-components")} */
|
/** @type {import("../converters/poll-components")} */
|
||||||
const pollComponents = sync.require("../converters/poll-components")
|
const pollComponents = sync.require("../converters/poll-components")
|
||||||
/** @type {import("./channel-webhook")} */
|
/** @type {import("./channel-webhook")} */
|
||||||
|
|
@ -33,9 +27,10 @@ async function updateVote(event) {
|
||||||
|
|
||||||
// If poll was started on Matrix, the Discord version is using components, so we can update that to the current status
|
// If poll was started on Matrix, the Discord version is using components, so we can update that to the current status
|
||||||
if (messageRow.source === 0) {
|
if (messageRow.source === 0) {
|
||||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
const row = select("channel_room", ["channel_id", "thread_parent"], {room_id: event.room_id}).get()
|
||||||
assert(channelID)
|
assert(row)
|
||||||
await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID))
|
const {channelID, threadID} = dUtils.swapThreadID(row.channel_id, row.thread_parent)
|
||||||
|
await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID), threadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const domino = require("domino")
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const entities = require("entities")
|
const entities = require("entities")
|
||||||
const pb = require("prettier-bytes")
|
const pb = require("prettier-bytes")
|
||||||
|
const mimeTypes = require("mime-types")
|
||||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
|
|
@ -29,6 +30,8 @@ const pollComponents = sync.require("./poll-components")
|
||||||
const setupEmojis = sync.require("../actions/setup-emojis")
|
const setupEmojis = sync.require("../actions/setup-emojis")
|
||||||
/** @type {import("../../d2m/converters/user-to-mxid")} */
|
/** @type {import("../../d2m/converters/user-to-mxid")} */
|
||||||
const userToMxid = sync.require("../../d2m/converters/user-to-mxid")
|
const userToMxid = sync.require("../../d2m/converters/user-to-mxid")
|
||||||
|
/** @type {import("../../web/routes/letter-avatar")} */
|
||||||
|
const letterAvatar = sync.require("../../web/routes/letter-avatar")
|
||||||
|
|
||||||
/** @type {[RegExp, string][]} */
|
/** @type {[RegExp, string][]} */
|
||||||
const markdownEscapes = [
|
const markdownEscapes = [
|
||||||
|
|
@ -95,15 +98,6 @@ 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", {
|
turndownService.addRule("spoiler", {
|
||||||
filter: function (node, options) {
|
filter: function (node, options) {
|
||||||
return node.tagName === "SPAN" && node.hasAttribute("data-mx-spoiler")
|
return node.tagName === "SPAN" && node.hasAttribute("data-mx-spoiler")
|
||||||
|
|
@ -582,6 +576,13 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
displayNameRunoff = ""
|
displayNameRunoff = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avatar post-processing. Use a thumbnail for media, or generate letter avatar if none present.
|
||||||
|
if (avatarURL) {
|
||||||
|
avatarURL = avatarURL + "?preset=avatar"
|
||||||
|
} else {
|
||||||
|
avatarURL = letterAvatar.getLetterAvatarURL(event.sender, displayNameShortened)
|
||||||
|
}
|
||||||
|
|
||||||
let content = event.content["body"] || "" // ultimate fallback
|
let content = event.content["body"] || "" // ultimate fallback
|
||||||
/** @type {{id: string, filename: string}[]} */
|
/** @type {{id: string, filename: string}[]} */
|
||||||
const attachments = []
|
const attachments = []
|
||||||
|
|
@ -613,13 +614,20 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
captionContent.addLine(`(Spoiler: ${fileSpoilerReason})`)
|
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
|
// File link as alternative to uploading
|
||||||
if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) {
|
if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) {
|
||||||
// Upload (unencrypted) file as link, because it's too large for Discord
|
// 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.
|
// 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)
|
const url = mxUtils.getPublicUrlForMxc(event.content.url)
|
||||||
assert(url)
|
assert(url)
|
||||||
const filename = event.content.filename || event.content.body
|
|
||||||
const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : ""
|
const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : ""
|
||||||
if (fileIsSpoiler) {
|
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.
|
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.
|
||||||
|
|
@ -628,7 +636,6 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Upload file as file
|
// Upload file as file
|
||||||
let filename = event.content.filename || event.content.body
|
|
||||||
if (fileIsSpoiler) filename = "SPOILER_" + filename
|
if (fileIsSpoiler) filename = "SPOILER_" + filename
|
||||||
if ("file" in event.content) {
|
if ("file" in event.content) {
|
||||||
// Encrypted
|
// Encrypted
|
||||||
|
|
@ -816,22 +823,12 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
if (shouldProcessTextEvent) {
|
if (shouldProcessTextEvent) {
|
||||||
if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) {
|
if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) {
|
||||||
let input = event.content.formatted_body
|
let input = event.content.formatted_body
|
||||||
if (perMessageProfile?.has_fallback) {
|
|
||||||
// Strip fallback elements added for clients that don't support per-message profiles.
|
|
||||||
// Deviates from recommended regexp in MSC to be less strict. Avoiding an HTML parser for performance reasons.
|
|
||||||
// ┌────A────┐ Opening HTML tag: capture tag name and stay within tag
|
|
||||||
// ┆ ┆┌─────────────B────────────┐ This text in the tag somewhere, presumably an attribute name
|
|
||||||
// ┆ ┆┆ ┆┌─C──┐ Rest of the opening tag
|
|
||||||
// ┆ ┆┆ ┆┆ ┆┌─D─┐ Tag content (no more tags allowed within)
|
|
||||||
// ┆ ┆┆ ┆┆ ┆┆ ┆┌─E──┐ Closing tag matching opening tag name
|
|
||||||
input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "")
|
|
||||||
}
|
|
||||||
if (event.content.msgtype === "m.emote") {
|
if (event.content.msgtype === "m.emote") {
|
||||||
input = `* ${displayName} ${input}`
|
input = `* ${displayName} ${input}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handling mentions of Discord users
|
// Handling mentions of Discord users
|
||||||
input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")>/g, (whole, attributeValue, mxid) => {
|
input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")/g, (whole, attributeValue, mxid) => {
|
||||||
mxid = decodeURIComponent(mxid)
|
mxid = decodeURIComponent(mxid)
|
||||||
if (mxUtils.eventSenderIsFromDiscord(mxid)) {
|
if (mxUtils.eventSenderIsFromDiscord(mxid)) {
|
||||||
// Handle mention of an OOYE sim user by their mxid
|
// Handle mention of an OOYE sim user by their mxid
|
||||||
|
|
@ -856,7 +853,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
input = input.replace(/("https:\/\/matrix.to.*?<\/a>):?/g, "$1")
|
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.
|
// 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>/g, "</blockquote>")
|
input = input.replace(/(?:\n|<br ?\/?>\s*)*<\/blockquote>(?:\n|<br ?\/?>\s*)*/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.
|
// 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.
|
// But I should not count it if it's between block elements.
|
||||||
|
|
@ -886,8 +883,9 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
const doc = domino.createDocument(
|
const doc = domino.createDocument(
|
||||||
// DOM parsers arrange elements in the <head> and <body>. Wrapping in a custom element ensures elements are reliably arranged in a single element.
|
// DOM parsers arrange elements in the <head> and <body>. Wrapping in a custom element ensures elements are reliably arranged in a single element.
|
||||||
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
|
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
|
||||||
);
|
)
|
||||||
const root = doc.getElementById("turndown-root");
|
const root = doc.getElementById("turndown-root")
|
||||||
|
assert(root)
|
||||||
async function forEachNode(event, node) {
|
async function forEachNode(event, node) {
|
||||||
for (; node; node = node.nextSibling) {
|
for (; node; node = node.nextSibling) {
|
||||||
// Check written mentions
|
// Check written mentions
|
||||||
|
|
@ -901,9 +899,12 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
}
|
}
|
||||||
// Check for incompatible backticks in code blocks
|
// Check for incompatible backticks in code blocks
|
||||||
let preNode
|
let preNode
|
||||||
if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) {
|
let isBackticksTextInPre = node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))
|
||||||
|
let isLongPre = node.tagName === "PRE" && node.textContent.length > 1800 && (preNode = node)
|
||||||
|
if (isBackticksTextInPre || isLongPre) {
|
||||||
if (preNode.firstChild?.nodeName === "CODE") {
|
if (preNode.firstChild?.nodeName === "CODE") {
|
||||||
const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt"
|
let ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1]
|
||||||
|
if (!dUtils.supportedPlaintextPreviewExtensions.has(ext)) ext = "txt"
|
||||||
const filename = `inline_code.${ext}`
|
const filename = `inline_code.${ext}`
|
||||||
// Build the replacement <code> node
|
// Build the replacement <code> node
|
||||||
const replacementCode = doc.createElement("code")
|
const replacementCode = doc.createElement("code")
|
||||||
|
|
@ -940,6 +941,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await forEachNode(event, root)
|
await forEachNode(event, root)
|
||||||
|
if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove())
|
||||||
|
|
||||||
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
|
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
|
||||||
// First we need to determine which emojis are at the end.
|
// First we need to determine which emojis are at the end.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,8 @@ const vote = sync.require("./actions/vote")
|
||||||
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
|
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
|
||||||
/** @type {import("../matrix/utils")} */
|
/** @type {import("../matrix/utils")} */
|
||||||
const utils = sync.require("../matrix/utils")
|
const utils = sync.require("../matrix/utils")
|
||||||
|
/** @type {import("../matrix/mreq")}) */
|
||||||
|
const mreq = sync.require("../matrix/mreq")
|
||||||
/** @type {import("../matrix/api")}) */
|
/** @type {import("../matrix/api")}) */
|
||||||
const api = sync.require("../matrix/api")
|
const api = sync.require("../matrix/api")
|
||||||
/** @type {import("../d2m/actions/create-room")} */
|
/** @type {import("../d2m/actions/create-room")} */
|
||||||
|
|
@ -32,6 +34,8 @@ const createRoom = sync.require("../d2m/actions/create-room")
|
||||||
const roomUpgrade = require("../matrix/room-upgrade")
|
const roomUpgrade = require("../matrix/room-upgrade")
|
||||||
/** @type {import("../d2m/actions/retrigger")} */
|
/** @type {import("../d2m/actions/retrigger")} */
|
||||||
const retrigger = sync.require("../d2m/actions/retrigger")
|
const retrigger = sync.require("../d2m/actions/retrigger")
|
||||||
|
/** @type {import("../matrix/homeserver-status")} */
|
||||||
|
const homeserverStatus = sync.require("../matrix/homeserver-status")
|
||||||
const {reg} = require("../matrix/read-registration")
|
const {reg} = require("../matrix/read-registration")
|
||||||
|
|
||||||
let lastReportedEvent = 0
|
let lastReportedEvent = 0
|
||||||
|
|
@ -94,6 +98,11 @@ function printError(type, source, e, payload) {
|
||||||
console.dir(payload, {depth: null})
|
console.dir(payload, {depth: null})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {string} stack */
|
||||||
|
function cleanErrorStack(stack) {
|
||||||
|
return stack.replace(/(\/webhooks\/[0-9]+\/)[a-zA-Z0-9_-]+/g, "$1(redacted)")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
* @param {"Discord" | "Matrix"} source
|
* @param {"Discord" | "Matrix"} source
|
||||||
|
|
@ -134,7 +143,7 @@ async function sendError(roomID, source, type, e, payload) {
|
||||||
builder.addLine(errorIntroLine)
|
builder.addLine(errorIntroLine)
|
||||||
|
|
||||||
// Where
|
// Where
|
||||||
const stack = stringifyErrorStack(e)
|
const stack = cleanErrorStack(stringifyErrorStack(e))
|
||||||
builder.addLine(`Error trace:\n${stack}`, tag`<details><summary>Error trace</summary><pre>${stack}</pre></details>`)
|
builder.addLine(`Error trace:\n${stack}`, tag`<details><summary>Error trace</summary><pre>${stack}</pre></details>`)
|
||||||
|
|
||||||
// How
|
// How
|
||||||
|
|
@ -143,7 +152,7 @@ async function sendError(roomID, source, type, e, payload) {
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
try {
|
try {
|
||||||
await api.sendEvent(roomID, "m.room.message", {
|
const errorEventID = await api.sendEvent(roomID, "m.room.message", {
|
||||||
...builder.get(),
|
...builder.get(),
|
||||||
"moe.cadence.ooye.error": {
|
"moe.cadence.ooye.error": {
|
||||||
source: source.toLowerCase(),
|
source: source.toLowerCase(),
|
||||||
|
|
@ -153,7 +162,20 @@ async function sendError(roomID, source, type, e, payload) {
|
||||||
user_ids: ["@cadence:cadence.moe"]
|
user_ids: ["@cadence:cadence.moe"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e) {}
|
// Add reaction indicating that errors may be retried
|
||||||
|
await api.sendEvent(roomID, "m.reaction", {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.annotation",
|
||||||
|
event_id: errorEventID,
|
||||||
|
key: "🔁"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function guard(type, fn) {
|
function guard(type, fn) {
|
||||||
|
|
@ -172,6 +194,7 @@ const errorRetrySema = new Semaphore()
|
||||||
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} reactionEvent
|
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} reactionEvent
|
||||||
*/
|
*/
|
||||||
async function onRetryReactionAdd(reactionEvent) {
|
async function onRetryReactionAdd(reactionEvent) {
|
||||||
|
if (reactionEvent.sender === `@${reg.sender_localpart}:${reg.ooye.server_name}`) return // Don't respond to the bot's own indicative reaction
|
||||||
const roomID = reactionEvent.room_id
|
const roomID = reactionEvent.room_id
|
||||||
await errorRetrySema.request(async () => {
|
await errorRetrySema.request(async () => {
|
||||||
const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
|
const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
|
||||||
|
|
@ -211,7 +234,7 @@ async event => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await matrixCommandHandler.execute(event)
|
await matrixCommandHandler.execute(event)
|
||||||
}
|
}
|
||||||
retrigger.messageFinishedBridging(event.event_id)
|
retrigger.finishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -222,7 +245,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
retrigger.messageFinishedBridging(event.event_id)
|
retrigger.finishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -502,5 +525,6 @@ async event => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||||
|
module.exports.cleanErrorStack = cleanErrorStack
|
||||||
module.exports.sendError = sendError
|
module.exports.sendError = sendError
|
||||||
module.exports.printError = printError
|
module.exports.printError = printError
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {stringifyErrorStack} = require("./event-dispatcher")
|
const {stringifyErrorStack, cleanErrorStack} = require("./event-dispatcher")
|
||||||
|
|
||||||
test("stringify error stack: works", t => {
|
test("stringify error stack: works", t => {
|
||||||
function a() {
|
function a() {
|
||||||
|
|
@ -21,3 +21,30 @@ test("stringify error stack: works", t => {
|
||||||
t.match(str, /^ \[prop\]: 2.1$/m)
|
t.match(str, /^ \[prop\]: 2.1$/m)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("clean error stack: removes webhook token", t => {
|
||||||
|
t.notMatch(
|
||||||
|
cleanErrorStack(`
|
||||||
|
DiscordAPIError: Service resource is being rate limited.
|
||||||
|
at fn (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:591:13)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||||
|
at exports.RequestHandler.request (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:546:17)
|
||||||
|
at WebhookMethods.executeWebhook (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/methods/Webhook.ts:249:35)
|
||||||
|
at /var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:65:31
|
||||||
|
at withWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:47:9)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||||
|
at async Object.sendMessageWithWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:64:17)
|
||||||
|
at async Object.sendEvent (/var/home/cadence/out-of-your-element/src/m2d/actions/send-event.js:132:27)
|
||||||
|
at async /var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:208:27
|
||||||
|
at async AppService.<anonymous> (/var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:162:11) {
|
||||||
|
[method]: "POST"
|
||||||
|
[path]: "/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG"
|
||||||
|
[code]: 40062
|
||||||
|
[httpStatus]: 429
|
||||||
|
[request]: {"endpoint":"/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG","method":"POST","dataType":"json","data":{"content":"https://discordstatus.com/#day\nOnly what discord tell us right now","allowed_mentions":{"parse":["roles"],"users":[]},"username":"lewri","avatar_url":"https://bridge.cadence.moe/download/matrix/matrix.org/URWwrtSUONGOYhfMsdUzcrir"}}
|
||||||
|
[response]: {}
|
||||||
|
[name]: "DiscordAPIError"`
|
||||||
|
),
|
||||||
|
/pfRqHl9v/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,8 @@ async function ping() {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${reg.as_token}`
|
Authorization: `Bearer ${reg.as_token}`
|
||||||
},
|
},
|
||||||
body: "{}"
|
body: "{}",
|
||||||
|
signal: AbortSignal.timeout(15e3)
|
||||||
})
|
})
|
||||||
const root = await res.json()
|
const root = await res.json()
|
||||||
return {
|
return {
|
||||||
|
|
@ -463,17 +464,29 @@ async function ping() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an mxc:// URL, and an optional height for thumbnailing, get the file from the content repository. Returns res.
|
* Given an mxc:// URL, and optional parameters for thumbnailing, get the file from the content repository. Returns res.
|
||||||
|
*
|
||||||
|
* Note that Synapse currently doesn't support animated thumbnails: https://github.com/element-hq/synapse/pull/18831
|
||||||
|
* @see https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv1mediathumbnailservernamemediaid
|
||||||
* @param {string} mxc
|
* @param {string} mxc
|
||||||
* @param {RequestInit & {height?: number | string}} [init]
|
* @param {RequestInit & {thumbnail?: {height?: number | string, width?: number | string, animated?: boolean, method?: "crop" | "scale"}}} [init]
|
||||||
* @return {Promise<Response & {body: streamWeb.ReadableStream<Uint8Array>}>}
|
* @return {Promise<Response & {body: streamWeb.ReadableStream<Uint8Array>}>}
|
||||||
*/
|
*/
|
||||||
async function getMedia(mxc, init = {}) {
|
async function getMedia(mxc, init = {}) {
|
||||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
init = {...init}
|
||||||
|
|
||||||
|
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
|
||||||
assert(mediaParts)
|
assert(mediaParts)
|
||||||
const downloadOrThumbnail = init.height ? "thumbnail" : "download"
|
|
||||||
let url = `${mreq.baseUrl}/client/v1/media/${downloadOrThumbnail}/${mediaParts[1]}/${mediaParts[2]}`
|
let route = "download"
|
||||||
if (init.height) url += "?" + new URLSearchParams({height: String(init.height), width: String(init.height)})
|
let query = ""
|
||||||
|
|
||||||
|
if (init.thumbnail) {
|
||||||
|
route = "thumbnail"
|
||||||
|
query = "?" + new URLSearchParams(Object.keys(init.thumbnail).map(k => [k, String(init.thumbnail?.[k])]))
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = `${mreq.baseUrl}/client/v1/media/${route}/${mediaParts[1]}/${mediaParts[2]}${query}`
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${reg.as_token}`
|
Authorization: `Bearer ${reg.as_token}`
|
||||||
|
|
|
||||||
134
src/matrix/homeserver-status.js
Normal file
134
src/matrix/homeserver-status.js
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
// @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,9 +9,12 @@ const {reg} = require("./read-registration.js")
|
||||||
const baseUrl = `${reg.ooye.server_origin}/_matrix`
|
const baseUrl = `${reg.ooye.server_origin}/_matrix`
|
||||||
|
|
||||||
class MatrixServerError extends Error {
|
class MatrixServerError extends Error {
|
||||||
constructor(data, opts) {
|
/** @param {number} httpStatus} */
|
||||||
|
constructor(data, httpStatus, opts) {
|
||||||
super(data.error || data.errcode)
|
super(data.error || data.errcode)
|
||||||
this.data = data
|
this.data = data
|
||||||
|
/** @type {number} */
|
||||||
|
this.httpStatus = httpStatus
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.errcode = data.errcode
|
this.errcode = data.errcode
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
|
|
@ -44,11 +47,11 @@ async function _convertBody(body) {
|
||||||
async function makeMatrixServerError(res, opts = {}) {
|
async function makeMatrixServerError(res, opts = {}) {
|
||||||
delete opts.headers?.["Authorization"]
|
delete opts.headers?.["Authorization"]
|
||||||
if (res.headers.get("content-type") === "application/json") {
|
if (res.headers.get("content-type") === "application/json") {
|
||||||
return new MatrixServerError(await res.json(), opts)
|
return new MatrixServerError(await res.json(), res.status, opts)
|
||||||
} else if (res.headers.get("content-type")?.startsWith("text/")) {
|
} 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()}, opts)
|
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, res.status, opts)
|
||||||
} else {
|
} else {
|
||||||
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts)
|
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, res.status, opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,12 +81,12 @@ async function mreq(method, url, bodyIn, extra = {}) {
|
||||||
var root = JSON.parse(text)
|
var root = JSON.parse(text)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
delete opts.headers?.["Authorization"]
|
delete opts.headers?.["Authorization"]
|
||||||
throw new MatrixServerError(text, {baseUrl, url, ...opts})
|
throw new MatrixServerError(text, res.status, {baseUrl, url, ...opts})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok || root.errcode) {
|
if (!res.ok || root.errcode) {
|
||||||
delete opts.headers?.["Authorization"]
|
delete opts.headers?.["Authorization"]
|
||||||
throw new MatrixServerError(root, {baseUrl, url, ...opts})
|
throw new MatrixServerError(root, res.status, {baseUrl, url, ...opts})
|
||||||
}
|
}
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,15 @@ function readRegistration() {
|
||||||
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
|
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
|
||||||
let reg = readRegistration()
|
let reg = readRegistration()
|
||||||
|
|
||||||
|
if (reg) {
|
||||||
|
fs.watch(registrationFilePath, {persistent: false}, () => {
|
||||||
|
let newReg = readRegistration()
|
||||||
|
if (newReg) {
|
||||||
|
Object.assign(reg, newReg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.registrationFilePath = registrationFilePath
|
module.exports.registrationFilePath = registrationFilePath
|
||||||
module.exports.readRegistration = readRegistration
|
module.exports.readRegistration = readRegistration
|
||||||
module.exports.getTemplateRegistration = getTemplateRegistration
|
module.exports.getTemplateRegistration = getTemplateRegistration
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ function getPublicUrlForMxc(mxc) {
|
||||||
*/
|
*/
|
||||||
function makeMxcPublic(mxc) {
|
function makeMxcPublic(mxc) {
|
||||||
assert(hasher, "xxhash is not ready yet")
|
assert(hasher, "xxhash is not ready yet")
|
||||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
|
||||||
if (!mediaParts) return undefined
|
if (!mediaParts) return undefined
|
||||||
|
|
||||||
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
||||||
|
|
|
||||||
21
src/stdin.js
21
src/stdin.js
|
|
@ -15,18 +15,37 @@ const mreq = sync.require("./matrix/mreq")
|
||||||
const api = sync.require("./matrix/api")
|
const api = sync.require("./matrix/api")
|
||||||
const file = sync.require("./matrix/file")
|
const file = sync.require("./matrix/file")
|
||||||
const sendEvent = sync.require("./m2d/actions/send-event")
|
const sendEvent = sync.require("./m2d/actions/send-event")
|
||||||
|
const redact = sync.require("./m2d/actions/redact")
|
||||||
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
||||||
const updatePins = sync.require("./d2m/actions/update-pins")
|
const updatePins = sync.require("./d2m/actions/update-pins")
|
||||||
const speedbump = sync.require("./d2m/actions/speedbump")
|
const speedbump = sync.require("./d2m/actions/speedbump")
|
||||||
const ks = sync.require("./matrix/kstate")
|
const ks = sync.require("./matrix/kstate")
|
||||||
const setPresence = sync.require("./d2m/actions/set-presence")
|
const setPresence = sync.require("./d2m/actions/set-presence")
|
||||||
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
||||||
|
const dUtils = sync.require("./discord/utils")
|
||||||
|
const mxUtils = sync.require("./matrix/utils")
|
||||||
const guildID = "112760669178241024"
|
const guildID = "112760669178241024"
|
||||||
|
|
||||||
|
async function ping() {
|
||||||
|
const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
|
||||||
|
if (result.ok) {
|
||||||
|
return "Ping OK. The homeserver and OOYE are talking to each other fine."
|
||||||
|
} else {
|
||||||
|
if (typeof result.root === "string") {
|
||||||
|
var msg = `Cannot reach homeserver: ${result.root}`
|
||||||
|
} else if (result.root.error) {
|
||||||
|
var msg = `Homeserver said: [${result.status}] ${result.root.error}`
|
||||||
|
} else {
|
||||||
|
var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}`
|
||||||
|
}
|
||||||
|
return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
if (!passthrough.repl) {
|
if (!passthrough.repl) {
|
||||||
const cli = repl.start({ prompt: "", eval: customEval, writer: s => s })
|
const cli = repl.start({prompt: "", eval: customEval, writer: s => s})
|
||||||
Object.assign(cli.context, passthrough)
|
Object.assign(cli.context, passthrough)
|
||||||
passthrough.repl = cli
|
passthrough.repl = cli
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ block body
|
||||||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||||
.s-popover--arrow.s-popover--arrow__tc
|
.s-popover--arrow.s-popover--arrow__tc
|
||||||
+add-roles-menu(guild, guild_id)
|
+add-roles-menu(guild, guild_id)
|
||||||
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.
|
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.
|
||||||
|
|
||||||
h3.mt32.fs-category Features
|
h3.mt32.fs-category Features
|
||||||
.s-card.d-grid.px0.g16
|
.s-card.d-grid.px0.g16
|
||||||
|
|
@ -191,14 +191,14 @@ block body
|
||||||
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
||||||
+discord(channel, true, "Announcement")
|
+discord(channel, true, "Announcement")
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Discord channels are linked.
|
.s-empty-state.p8 No Discord channels available.
|
||||||
.fl-grow1.s-btn-group.fd-column.w30
|
.fl-grow1.s-btn-group.fd-column.w30
|
||||||
each room in unlinkedRooms
|
each room in unlinkedRooms
|
||||||
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
|
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)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
|
||||||
+matrix(room, true)
|
+matrix(room, true)
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Matrix rooms are linked.
|
.s-empty-state.p8 No Matrix rooms available.
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
div
|
div
|
||||||
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3")
|
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3")
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
|
const {ReadableStream} = require("stream/web")
|
||||||
|
const {Readable} = require("stream")
|
||||||
|
const sharp = require("sharp")
|
||||||
|
|
||||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||||
let hasher = null
|
let hasher = null
|
||||||
|
|
@ -19,11 +22,27 @@ const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet")
|
||||||
/** @type {import("../../m2d/actions/sticker")} */
|
/** @type {import("../../m2d/actions/sticker")} */
|
||||||
const sticker = sync.require("../../m2d/actions/sticker")
|
const sticker = sync.require("../../m2d/actions/sticker")
|
||||||
|
|
||||||
|
// Resizing client-side because server-side is too slow, at least with Synapse. Really need it to be fast because webhook avatars show a placeholder in the interim.
|
||||||
|
/** @type {{[presetKey: string]: (body: ReadableStream) => ReadableStream}} */
|
||||||
|
const MEDIA_THUMBNAIL_PRESETS = {
|
||||||
|
avatar: body =>
|
||||||
|
Readable.toWeb(
|
||||||
|
Readable.fromWeb(body).pipe(
|
||||||
|
sharp()
|
||||||
|
.resize({height: 210, width: 210, fit: "cover"}) // the largest display of the webhook pfp on Discord Android in screen pixels
|
||||||
|
.jpeg({force: false, quality: 90}) // File size works out to up to ~110k for a PNG, less for a JPEG
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
params: z.object({
|
media: z.object({
|
||||||
server_name: z.string(),
|
server_name: z.string(),
|
||||||
media_id: z.string()
|
media_id: z.string()
|
||||||
}),
|
}),
|
||||||
|
mediaQuery: z.object({
|
||||||
|
preset: z.enum(Object.keys(MEDIA_THUMBNAIL_PRESETS)) // list of possible thumbnail presets
|
||||||
|
}),
|
||||||
sheet: z.object({
|
sheet: z.object({
|
||||||
e: z.array(z.string()).or(z.string())
|
e: z.array(z.string()).or(z.string())
|
||||||
}),
|
}),
|
||||||
|
|
@ -65,7 +84,8 @@ function verifyMediaHash(serverAndMediaID) {
|
||||||
}
|
}
|
||||||
|
|
||||||
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
|
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
|
||||||
const params = await getValidatedRouterParams(event, schema.params.parse)
|
const params = await getValidatedRouterParams(event, schema.media.parse)
|
||||||
|
const query = await getValidatedQuery(event, schema.mediaQuery.safeParse)
|
||||||
|
|
||||||
verifyMediaHash(`${params.server_name}/${params.media_id}`)
|
verifyMediaHash(`${params.server_name}/${params.media_id}`)
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
|
|
@ -77,7 +97,12 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
|
||||||
setResponseStatus(event, res.status)
|
setResponseStatus(event, res.status)
|
||||||
setResponseHeader(event, "Content-Type", contentType)
|
setResponseHeader(event, "Content-Type", contentType)
|
||||||
setResponseHeader(event, "Transfer-Encoding", "chunked")
|
setResponseHeader(event, "Transfer-Encoding", "chunked")
|
||||||
|
|
||||||
|
if (res.ok && query.success) {
|
||||||
|
return MEDIA_THUMBNAIL_PRESETS[query.data.preset](res.body)
|
||||||
|
} else {
|
||||||
return res.body
|
return res.body
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
as.router.get(`/download/sheet`, defineEventHandler(async event => {
|
as.router.get(`/download/sheet`, defineEventHandler(async event => {
|
||||||
|
|
|
||||||
117
src/web/routes/letter-avatar.js
Normal file
117
src/web/routes/letter-avatar.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const h3 = require("h3")
|
||||||
|
const {defineEventHandler, getValidatedQuery, setResponseHeader} = h3
|
||||||
|
const sharp = require("sharp")
|
||||||
|
const {z} = require("zod")
|
||||||
|
|
||||||
|
const {as} = require("../../passthrough")
|
||||||
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a 300x300 avatar image consisting of a dark coloured background, and a single character in a lighter colour centered in the middle.
|
||||||
|
Note: Where dimensions are changed, font size must also be changed too to produce an identical image as before.
|
||||||
|
Simply put, 100px = 60pt for font.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SIZE = 300
|
||||||
|
const POSSIBLE_HUES = 12
|
||||||
|
|
||||||
|
/** Helper function: To get accurate complimenting colours we need to work in HSL, then convert back to RGB at the end */
|
||||||
|
function hslToRgb(h, s, l) {
|
||||||
|
s /= 100;
|
||||||
|
l /= 100;
|
||||||
|
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
|
||||||
|
const f = n => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
return l - a * Math.max(-1, Math.min(Math.min(k - 3, 9 - k), 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: Math.round(255 * f(0)),
|
||||||
|
g: Math.round(255 * f(8)),
|
||||||
|
b: Math.round(255 * f(4))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the MXID to generate deterministic avatar colours for each user.
|
||||||
|
* Here, we use the string hash code as a hue value, with a 360 wrap modulo.
|
||||||
|
* @param {string} mxid
|
||||||
|
*/
|
||||||
|
function mxidToHue(mxid) {
|
||||||
|
// Element Classic string hasher
|
||||||
|
let hash = 0;
|
||||||
|
let i;
|
||||||
|
let chr;
|
||||||
|
if (mxid.length === 0) {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
for (i = 0; i < mxid.length; i++) {
|
||||||
|
chr = mxid.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr;
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
hash = Math.abs(hash)
|
||||||
|
return (hash % POSSIBLE_HUES) * (360 / POSSIBLE_HUES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first useful character in username to put in the avatar.
|
||||||
|
* @param {string} username
|
||||||
|
*/
|
||||||
|
function usernameToLetter(username) {
|
||||||
|
return (username.match(/[a-z0-9]/i)?.[0] || "#").toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} mxid
|
||||||
|
* @param {string} username
|
||||||
|
*/
|
||||||
|
function getLetterAvatarURL(mxid, username) {
|
||||||
|
const p = new URLSearchParams({letter: usernameToLetter(username), hue: String(mxidToHue(mxid))})
|
||||||
|
return `${reg.ooye.bridge_origin}/download/letter-avatar?${p}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
letterAvatar: z.object({
|
||||||
|
hue: z.coerce.number().min(0).max(360),
|
||||||
|
letter: z.string().regex(/^[A-Z0-9#]$/)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce a PNG letter-avatar from given parameters.
|
||||||
|
* @param {string} letter
|
||||||
|
* @param {number} hue
|
||||||
|
*/
|
||||||
|
as.router.get("/download/letter-avatar", defineEventHandler(async event => {
|
||||||
|
const {letter, hue} = await getValidatedQuery(event, schema.letterAvatar.parse)
|
||||||
|
|
||||||
|
const bg_rgb = hslToRgb(hue, 65, 18);
|
||||||
|
const text_rgb = hslToRgb(hue, 70, 65);
|
||||||
|
const text_rgbahex = `#${text_rgb.r.toString(16).padStart(2, "0")}${text_rgb.g.toString(16).padStart(2, "0")}${text_rgb.b.toString(16).padStart(2, "0")}ff`
|
||||||
|
|
||||||
|
const streamOut = sharp({
|
||||||
|
create: {
|
||||||
|
width: SIZE, height: SIZE, channels: 4,
|
||||||
|
background: {
|
||||||
|
r: bg_rgb.r, g: bg_rgb.g, b: bg_rgb.b, alpha: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).composite([{
|
||||||
|
input: {
|
||||||
|
text: {
|
||||||
|
text: `<span foreground="${text_rgbahex}">${letter}</span>`,
|
||||||
|
font: "Noto Sans Bold 180", align: "center", rgba: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]).png()
|
||||||
|
|
||||||
|
setResponseHeader(event, "content-type", "image/png")
|
||||||
|
return streamOut
|
||||||
|
}))
|
||||||
|
|
||||||
|
module.exports.getLetterAvatarURL = getLetterAvatarURL
|
||||||
85
src/web/routes/stats.js
Normal file
85
src/web/routes/stats.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const {defineEventHandler, getValidatedQuery, H3Event, setResponseHeader} = require("h3")
|
||||||
|
const {as, db, sync} = require("../../passthrough")
|
||||||
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
|
/** @type {import("../../discord/utils")} */
|
||||||
|
const dUtils = sync.require("../../discord/utils")
|
||||||
|
|
||||||
|
// Calculation takes time and is single-threaded. I could add database indexes, but this is simpler and doesn't need storage.
|
||||||
|
const STATS_CACHE_TIME = 10 * 60 * 1000 // 10 minutes
|
||||||
|
|
||||||
|
function getMessageCountLastDuration(duration) {
|
||||||
|
const snowflake = dUtils.timestampToSnowflakeInexact(Date.now() - duration)
|
||||||
|
return db.prepare("select count(*) from message_room where message_id >= ? and length(message_id) = ?").pluck().get(snowflake, snowflake.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStats() {
|
||||||
|
const durations = [
|
||||||
|
["week", 7 * 24 * 60 * 60 * 1000],
|
||||||
|
["day", 1 * 24 * 60 * 60 * 1000],
|
||||||
|
["hour", 1 * 60 * 60 * 1000]
|
||||||
|
]
|
||||||
|
|
||||||
|
// console.time("get stats")
|
||||||
|
let temp = {
|
||||||
|
guilds: db.prepare("select count(*) from guild_space").pluck().get(),
|
||||||
|
channels: db.prepare("select count(*) from channel_room").pluck().get(),
|
||||||
|
messages: db.prepare("select count(*) from message_room").pluck().get(),
|
||||||
|
...durations.reduce((a, c) => (a[`messages_last_${c[0]}`] = getMessageCountLastDuration(c[1]), a), {}),
|
||||||
|
message_sources: db.prepare("select count(*) from event_message where part = 0 group by source order by source").pluck().all(),
|
||||||
|
oldest_message: new Date(dUtils.snowflakeToTimestampExact(db.prepare("select min(message_id) from event_message where source = 0").pluck().get())), // good until 2090
|
||||||
|
discord_users: db.prepare("select count(*) from sim").pluck().get(),
|
||||||
|
matrix_users: db.prepare("select count(distinct mxid) from member_cache where mxid not like ?").pluck().get(reg.namespaces.users[0].regex.replace(/\.\*.*/, "%")),
|
||||||
|
}
|
||||||
|
// console.timeEnd("get stats")
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof getStats>} */
|
||||||
|
let stats
|
||||||
|
let statsUpdatedAt = 0
|
||||||
|
|
||||||
|
function updateStatsIfOld() {
|
||||||
|
if (statsUpdatedAt < Date.now() - STATS_CACHE_TIME) {
|
||||||
|
stats = getStats()
|
||||||
|
statsUpdatedAt = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
as.router.get("/api/stats", defineEventHandler(async event => {
|
||||||
|
updateStatsIfOld()
|
||||||
|
return {
|
||||||
|
...stats,
|
||||||
|
oldest_message: stats.oldest_message.toISOString(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
as.router.get("/metrics", defineEventHandler(async event => {
|
||||||
|
updateStatsIfOld()
|
||||||
|
setResponseHeader(event, "content-type", "text/plain")
|
||||||
|
return `
|
||||||
|
# HELP guilds Total number of guilds
|
||||||
|
# TYPE guilds gauge
|
||||||
|
ooye_guilds_total ${stats.guilds}
|
||||||
|
|
||||||
|
# HELP channels Total number of channels
|
||||||
|
# TYPE channels gauge
|
||||||
|
ooye_channels_total ${stats.channels}
|
||||||
|
|
||||||
|
# HELP messages_total Total number of messages sent from each side
|
||||||
|
# TYPE messages_total gauge
|
||||||
|
ooye_messages_total{type="matrix"} ${stats.message_sources[0]}
|
||||||
|
ooye_messages_total{type="discord"} ${stats.message_sources[1]}
|
||||||
|
|
||||||
|
# HELP oldest_message_timestamp Unix timestamp of the oldest message
|
||||||
|
# TYPE oldest_message_timestamp gauge
|
||||||
|
ooye_oldest_message_timestamp_seconds ${stats.oldest_message.getTime() / 1000}
|
||||||
|
|
||||||
|
# HELP ooye_users_total Total number of users on each side
|
||||||
|
# TYPE ooye_users_total gauge
|
||||||
|
ooye_users_total{type="matrix"} ${stats.matrix_users}
|
||||||
|
ooye_users_total{type="discord"} ${stats.discord_users}
|
||||||
|
`.trimStart()
|
||||||
|
}))
|
||||||
|
|
@ -130,7 +130,9 @@ sync.require("./routes/download-discord")
|
||||||
sync.require("./routes/guild-settings")
|
sync.require("./routes/guild-settings")
|
||||||
sync.require("./routes/guild")
|
sync.require("./routes/guild")
|
||||||
sync.require("./routes/info")
|
sync.require("./routes/info")
|
||||||
|
sync.require("./routes/letter-avatar")
|
||||||
sync.require("./routes/link")
|
sync.require("./routes/link")
|
||||||
sync.require("./routes/log-in-with-matrix")
|
sync.require("./routes/log-in-with-matrix")
|
||||||
sync.require("./routes/oauth")
|
sync.require("./routes/oauth")
|
||||||
sync.require("./routes/password")
|
sync.require("./routes/password")
|
||||||
|
sync.require("./routes/stats")
|
||||||
|
|
|
||||||
3
start.js
3
start.js
|
|
@ -31,8 +31,9 @@ sync.require("./src/m2d/event-dispatcher")
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await migrate.migrate(db)
|
await migrate.migrate(db)
|
||||||
|
process.stdout.write("Connecting to Discord... ")
|
||||||
await discord.cloud.connect()
|
await discord.cloud.connect()
|
||||||
console.log("Discord gateway started")
|
console.log("ok.")
|
||||||
sync.require("./src/web/server")
|
sync.require("./src/web/server")
|
||||||
await power.applyPower()
|
await power.applyPower()
|
||||||
|
|
||||||
|
|
|
||||||
308
test/data.js
308
test/data.js
|
|
@ -19,6 +19,26 @@ module.exports = {
|
||||||
default_thread_rate_limit_per_user: 0,
|
default_thread_rate_limit_per_user: 0,
|
||||||
guild_id: "112760669178241024"
|
guild_id: "112760669178241024"
|
||||||
},
|
},
|
||||||
|
voice: {
|
||||||
|
voice_background_display: null,
|
||||||
|
version: 1774469910848,
|
||||||
|
user_limit: 0,
|
||||||
|
type: 2,
|
||||||
|
theme_color: null,
|
||||||
|
status: null,
|
||||||
|
rtc_region: null,
|
||||||
|
rate_limit_per_user: 0,
|
||||||
|
position: 0,
|
||||||
|
permission_overwrites: [],
|
||||||
|
parent_id: "805261291908104252",
|
||||||
|
nsfw: false,
|
||||||
|
name: "🍞丨[8user] Piece",
|
||||||
|
last_message_id: "1459912691098325137",
|
||||||
|
id: "1036840786093953084",
|
||||||
|
flags: 0,
|
||||||
|
bitrate: 256000,
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
},
|
||||||
updates: {
|
updates: {
|
||||||
type: 0,
|
type: 0,
|
||||||
topic: "Updates and release announcements for Out Of Your Element.",
|
topic: "Updates and release announcements for Out Of Your Element.",
|
||||||
|
|
@ -2015,6 +2035,80 @@ module.exports = {
|
||||||
tts: false
|
tts: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
reply_to_member_join: {
|
||||||
|
type: 19,
|
||||||
|
content: "when the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
timestamp: "2026-03-30T12:11:04.443000+00:00",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
id: "1488148556962332692",
|
||||||
|
channel_id: "475599038536744962",
|
||||||
|
author: {
|
||||||
|
id: "576945009408999426",
|
||||||
|
username: "randomllama121",
|
||||||
|
avatar: "08510a70f957106dad1580323c40cd7a",
|
||||||
|
discriminator: "0",
|
||||||
|
public_flags: 128,
|
||||||
|
flags: 128,
|
||||||
|
banner: null,
|
||||||
|
accent_color: null,
|
||||||
|
global_name: "random :3",
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
collectibles: null,
|
||||||
|
display_name_styles: null,
|
||||||
|
banner_color: null,
|
||||||
|
clan: null,
|
||||||
|
primary_guild: null
|
||||||
|
},
|
||||||
|
pinned: false,
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false,
|
||||||
|
message_reference: {
|
||||||
|
type: 0,
|
||||||
|
channel_id: "475599038536744962",
|
||||||
|
message_id: "1488146734352826478",
|
||||||
|
guild_id: "475599038536744960"
|
||||||
|
},
|
||||||
|
referenced_message: {
|
||||||
|
type: 7,
|
||||||
|
content: "",
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
timestamp: "2026-03-30T12:03:49.899000+00:00",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
id: "1488146734352826478",
|
||||||
|
channel_id: "475599038536744962",
|
||||||
|
author: {
|
||||||
|
id: "1461677775554478161",
|
||||||
|
username: "peasant321_76775",
|
||||||
|
avatar: null,
|
||||||
|
discriminator: "0",
|
||||||
|
public_flags: 0,
|
||||||
|
flags: 0,
|
||||||
|
banner: null,
|
||||||
|
accent_color: null,
|
||||||
|
global_name: "PEASANT!!",
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
collectibles: null,
|
||||||
|
display_name_styles: null,
|
||||||
|
banner_color: null,
|
||||||
|
clan: null,
|
||||||
|
primary_guild: null
|
||||||
|
},
|
||||||
|
pinned: false,
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false
|
||||||
|
}
|
||||||
|
},
|
||||||
attachment_no_content: {
|
attachment_no_content: {
|
||||||
id: "1124628646670389348",
|
id: "1124628646670389348",
|
||||||
type: 0,
|
type: 0,
|
||||||
|
|
@ -5379,6 +5473,189 @@ module.exports = {
|
||||||
content: '-# Original Message ID: 1466556003645657118 · <t:1769724599:f>'
|
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: {
|
message_update: {
|
||||||
|
|
@ -6170,6 +6447,37 @@ module.exports = {
|
||||||
components: [],
|
components: [],
|
||||||
position: 12
|
position: 12
|
||||||
},
|
},
|
||||||
|
channel_follow_add: {
|
||||||
|
type: 12,
|
||||||
|
content: "PluralKit #downtime",
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
timestamp: "2026-03-24T23:16:04.097Z",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
id: "1486141581047369888",
|
||||||
|
channel_id: "1451125453082591314",
|
||||||
|
author: {
|
||||||
|
id: "154058479798059009",
|
||||||
|
username: "exaptations",
|
||||||
|
discriminator: "0",
|
||||||
|
avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80",
|
||||||
|
bot: false,
|
||||||
|
flags: 0,
|
||||||
|
globalName: "Exa",
|
||||||
|
},
|
||||||
|
pinned: false,
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false,
|
||||||
|
message_reference: {
|
||||||
|
type: 0,
|
||||||
|
channel_id: "1015204661701124206",
|
||||||
|
guild_id: "466707357099884544"
|
||||||
|
}
|
||||||
|
},
|
||||||
updated_to_start_thread_from_here: {
|
updated_to_start_thread_from_here: {
|
||||||
t: "MESSAGE_UPDATE",
|
t: "MESSAGE_UPDATE",
|
||||||
s: 19,
|
s: 19,
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ WITH a (message_id, channel_id) AS (VALUES
|
||||||
('1381212840957972480', '112760669178241024'),
|
('1381212840957972480', '112760669178241024'),
|
||||||
('1401760355339862066', '112760669178241024'),
|
('1401760355339862066', '112760669178241024'),
|
||||||
('1439351590262800565', '1438284564815548418'),
|
('1439351590262800565', '1438284564815548418'),
|
||||||
('1404133238414376971', '112760669178241024'))
|
('1404133238414376971', '112760669178241024'),
|
||||||
|
('1440549403667468320', '1160894080998461480'))
|
||||||
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;
|
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
|
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||||
|
|
@ -143,7 +144,8 @@ 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),
|
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
|
||||||
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
||||||
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
||||||
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
|
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1),
|
||||||
|
('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1);
|
||||||
|
|
||||||
INSERT INTO file (discord_url, mxc_url) VALUES
|
INSERT INTO file (discord_url, mxc_url) VALUES
|
||||||
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
||||||
|
|
@ -192,6 +194,7 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) V
|
||||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0),
|
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0),
|
||||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
|
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
|
||||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
|
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
|
||||||
|
('!qzDBLKlildpzrrOnFZ:cadence.moe', '@lavender.pet:queer.sh', 'lavender.pet', NULL, 0),
|
||||||
('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0),
|
('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0),
|
||||||
('!zq94fae5bVKUubZLp7:agiadn.org', '@underscore_x:agiadn.org', 'underscore_x', NULL, 100);
|
('!zq94fae5bVKUubZLp7:agiadn.org', '@underscore_x:agiadn.org', 'underscore_x', NULL, 100);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue