forked from cadence/out-of-your-element
Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
44 changed files with 1428 additions and 1325 deletions
|
|
@ -84,7 +84,7 @@ Discord display names for normal users are limited to 32 characters. For webhook
|
|||
|
||||
If the bridge software was restarted, it will attempt to catch up on messages missed while it was offline.
|
||||
|
||||
From Discord, for any given channel, if fewer than 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.
|
||||
|
||||
|
|
|
|||
102
package-lock.json
generated
102
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"ansi-colors": "^4.1.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.17.0",
|
||||
"cloudstorm": "^0.17.1",
|
||||
"discord-api-types": "^0.38.38",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
"@types/node": "^22.17.1",
|
||||
"c8": "^11.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"supertape": "^12.0.12"
|
||||
"supertape": "^13.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
@ -1003,9 +1003,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1129,9 +1129,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.8.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz",
|
||||
"integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
|
||||
"version": "12.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
|
||||
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1139,7 +1139,7 @@
|
|||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
|
|
@ -1163,9 +1163,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1316,12 +1316,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cloudstorm": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.0.tgz",
|
||||
"integrity": "sha512-zsd9y5ljNnbxdvDid9TgWePDqo7il4so5spzx6NDwZ67qWQjR96UUhLxJ+BAOdBBSPF9UXFM61dAzC2g918q+A==",
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz",
|
||||
"integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.40",
|
||||
"discord-api-types": "^0.38.47",
|
||||
"snowtransfer": "^0.17.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1366,9 +1366,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-es": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
|
||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz",
|
||||
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
|
|
@ -1452,9 +1452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/destr": {
|
||||
|
|
@ -1473,9 +1473,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.42",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz",
|
||||
"integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==",
|
||||
"version": "0.38.47",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
|
||||
"integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
|
|
@ -1688,14 +1688,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/h3": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.10.tgz",
|
||||
"integrity": "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz",
|
||||
"integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie-es": "^1.2.2",
|
||||
"cookie-es": "^1.2.3",
|
||||
"crossws": "^0.3.5",
|
||||
"defu": "^6.1.4",
|
||||
"defu": "^6.1.6",
|
||||
"destr": "^2.0.5",
|
||||
"iron-webcrypto": "^1.2.1",
|
||||
"node-mock-http": "^1.0.4",
|
||||
|
|
@ -1753,9 +1753,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmx.org": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz",
|
||||
"integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
|
||||
"integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
|
|
@ -1974,9 +1974,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||
"version": "11.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
|
||||
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
|
|
@ -2579,12 +2579,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/snowtransfer": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.5.tgz",
|
||||
"integrity": "sha512-nVI1UJNFoX1ndGFZxB3zb3X5SWtD9hIAcw7wCgVKWvCf42Wg2B4UFIrZWI83HxaSBY0CGbPZmZzZb3RSt/v2wQ==",
|
||||
"version": "0.17.7",
|
||||
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz",
|
||||
"integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.40"
|
||||
"discord-api-types": "^0.38.47"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
|
|
@ -2667,9 +2667,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/supertape": {
|
||||
"version": "12.10.5",
|
||||
"resolved": "https://registry.npmjs.org/supertape/-/supertape-12.10.5.tgz",
|
||||
"integrity": "sha512-1Px+6mhFaqcht3p4tkf3o4G8lbBazvx4pgFngm4vGwWipYm3fykm6SJ4ThXobiaNsptz53CDWA2q4B/2KtmA4w==",
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supertape/-/supertape-13.2.0.tgz",
|
||||
"integrity": "sha512-UoxZnyoMOdSJHvbcmD8i28MaGXsA7I0cJ0jr8anT4CkmfaE9M1y5mt9EoXyzfC8UdnQZwXOnJLUwqyKLAeUOug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2856,9 +2856,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uqr": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz",
|
||||
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==",
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz",
|
||||
"integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
|
|
@ -3028,9 +3028,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"description": "A bridge between Matrix and Discord",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"ansi-colors": "^4.1.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.17.0",
|
||||
"cloudstorm": "^0.17.1",
|
||||
"discord-api-types": "^0.38.38",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"@types/node": "^22.17.1",
|
||||
"c8": "^11.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"supertape": "^12.0.12"
|
||||
"supertape": "^13.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps start.js",
|
||||
|
|
|
|||
|
|
@ -1,333 +0,0 @@
|
|||
/*
|
||||
---
|
||||
elizabot.js v.1.1 - ELIZA JS library (N.Landsteiner 2005)
|
||||
https://www.masswerk.at/elizabot/
|
||||
Free Software © Norbert Landsteiner 2005
|
||||
---
|
||||
Modified by Cadence Ember in 2025 for v1.2 (unofficial)
|
||||
* Changed to class structure
|
||||
* Load from local file and instance instead of global variables
|
||||
* Remove memory
|
||||
* Remove xnone
|
||||
* Remove initials
|
||||
* Remove finals
|
||||
* Allow substitutions in rule keys
|
||||
---
|
||||
|
||||
Eliza is a mock Rogerian psychotherapist.
|
||||
Original program by Joseph Weizenbaum in MAD-SLIP for "Project MAC" at MIT.
|
||||
cf: Weizenbaum, Joseph "ELIZA - A Computer Program For the Study of Natural Language
|
||||
Communication Between Man and Machine"
|
||||
in: Communications of the ACM; Volume 9 , Issue 1 (January 1966): p 36-45.
|
||||
JavaScript implementation by Norbert Landsteiner 2005; <http://www.masserk.at>
|
||||
|
||||
synopsis:
|
||||
new ElizaBot( <random-choice-disable-flag> )
|
||||
ElizaBot.prototype.transform( <inputstring> )
|
||||
ElizaBot.prototype.reset()
|
||||
|
||||
usage:
|
||||
var eliza = new ElizaBot();
|
||||
var reply = eliza.transform(inputstring);
|
||||
|
||||
// to reproduce the example conversation given by J. Weizenbaum
|
||||
// initialize with the optional random-choice-disable flag
|
||||
var originalEliza = new ElizaBot(true);
|
||||
|
||||
`ElizaBot' is also a general chatbot engine that can be supplied with any rule set.
|
||||
(for required data structures cf. "elizadata.js" and/or see the documentation.)
|
||||
data is parsed and transformed for internal use at the creation time of the
|
||||
first instance of the `ElizaBot' constructor.
|
||||
|
||||
vers 1.1: lambda functions in RegExps are currently a problem with too many browsers.
|
||||
changed code to work around.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
|
||||
/** @type {import("./elizadata")} */
|
||||
const data = sync.require("./elizadata")
|
||||
|
||||
class ElizaBot {
|
||||
/** @type {any} */
|
||||
elizaKeywords = [['###',0,[['###',[]]]]];
|
||||
pres={};
|
||||
preExp = /####/;
|
||||
posts={};
|
||||
postExp = /####/;
|
||||
|
||||
/**
|
||||
* @param {boolean} noRandomFlag
|
||||
*/
|
||||
constructor(noRandomFlag) {
|
||||
this.noRandom= !!noRandomFlag;
|
||||
this.capitalizeFirstLetter=true;
|
||||
this.debug=false;
|
||||
this.version="1.2";
|
||||
this._init();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.lastchoice=[];
|
||||
for (let k=0; k<data.elizaKeywords.length; k++) {
|
||||
this.lastchoice[k]=[];
|
||||
var rules=data.elizaKeywords[k][2];
|
||||
for (let i=0; i<rules.length; i++) this.lastchoice[k][i]=-1;
|
||||
}
|
||||
}
|
||||
|
||||
_init() {
|
||||
// parse data and convert it from canonical form to internal use
|
||||
// prodoce synonym list
|
||||
var synPatterns={};
|
||||
if ((data.elizaSynons) && (typeof data.elizaSynons == 'object')) {
|
||||
for (let i in data.elizaSynons) synPatterns[i]='('+i+'|'+data.elizaSynons[i].join('|')+')';
|
||||
}
|
||||
// check for keywords or install empty structure to prevent any errors
|
||||
if (data.elizaKeywords) this.elizaKeywords = structuredClone(data.elizaKeywords)
|
||||
// 1st convert rules to regexps
|
||||
// expand synonyms and insert asterisk expressions for backtracking
|
||||
var sre=/@(\S+)/;
|
||||
var are=/(\S)\s*\*\s*(\S)/;
|
||||
var are1=/^\s*\*\s*(\S)/;
|
||||
var are2=/(\S)\s*\*\s*$/;
|
||||
var are3=/^\s*\*\s*$/;
|
||||
var wsre=/\s+/g;
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
var m=sre.exec(this.elizaKeywords[k][0]);
|
||||
while (m) {
|
||||
var sp=(synPatterns[m[1]])? synPatterns[m[1]]:m[1];
|
||||
this.elizaKeywords[k][0]=this.elizaKeywords[k][0].substring(0,m.index)+sp+this.elizaKeywords[k][0].substring(m.index+m[0].length);
|
||||
m=sre.exec(this.elizaKeywords[k][0]);
|
||||
}
|
||||
var rules=this.elizaKeywords[k][2];
|
||||
this.elizaKeywords[k][3]=k; // save original index for sorting
|
||||
for (let i=0; i<rules.length; i++) {
|
||||
var r=rules[i];
|
||||
// check mem flag and store it as decomp's element 2
|
||||
if (r[0].charAt(0)=='$') {
|
||||
var ofs=1;
|
||||
while (r[0].charAt[ofs]==' ') ofs++;
|
||||
r[0]=r[0].substring(ofs);
|
||||
r[2]=true;
|
||||
}
|
||||
else {
|
||||
r[2]=false;
|
||||
}
|
||||
// expand synonyms (v.1.1: work around lambda function)
|
||||
var m=sre.exec(r[0]);
|
||||
while (m) {
|
||||
var sp=(synPatterns[m[1]])? synPatterns[m[1]]:m[1];
|
||||
r[0]=r[0].substring(0,m.index)+sp+r[0].substring(m.index+m[0].length);
|
||||
m=sre.exec(r[0]);
|
||||
}
|
||||
// expand asterisk expressions (v.1.1: work around lambda function)
|
||||
if (are3.test(r[0])) {
|
||||
r[0]='\\s*(.*)\\s*';
|
||||
}
|
||||
else {
|
||||
m=are.exec(r[0]);
|
||||
if (m) {
|
||||
let lp='';
|
||||
let rp=r[0];
|
||||
while (m) {
|
||||
lp+=rp.substring(0,m.index+1);
|
||||
if (m[1]!=')') lp+='\\b';
|
||||
lp+='\\s*(.*)\\s*';
|
||||
if ((m[2]!='(') && (m[2]!='\\')) lp+='\\b';
|
||||
lp+=m[2];
|
||||
rp=rp.substring(m.index+m[0].length);
|
||||
m=are.exec(rp);
|
||||
}
|
||||
r[0]=lp+rp;
|
||||
}
|
||||
m=are1.exec(r[0]);
|
||||
if (m) {
|
||||
let lp='\\s*(.*)\\s*';
|
||||
if ((m[1]!=')') && (m[1]!='\\')) lp+='\\b';
|
||||
r[0]=lp+r[0].substring(m.index-1+m[0].length);
|
||||
}
|
||||
m=are2.exec(r[0]);
|
||||
if (m) {
|
||||
let lp=r[0].substring(0,m.index+1);
|
||||
if (m[1]!='(') lp+='\\b';
|
||||
r[0]=lp+'\\s*(.*)\\s*';
|
||||
}
|
||||
}
|
||||
// expand white space
|
||||
r[0]=r[0].replace(wsre, '\\s+');
|
||||
wsre.lastIndex=0;
|
||||
}
|
||||
}
|
||||
// now sort keywords by rank (highest first)
|
||||
this.elizaKeywords.sort(this._sortKeywords);
|
||||
// and compose regexps and refs for pres and posts
|
||||
if ((data.elizaPres) && (data.elizaPres.length)) {
|
||||
var a=[];
|
||||
for (let i=0; i<data.elizaPres.length; i+=2) {
|
||||
a.push(data.elizaPres[i]);
|
||||
this.pres[data.elizaPres[i]]=data.elizaPres[i+1];
|
||||
}
|
||||
this.preExp = new RegExp('\\b('+a.join('|')+')\\b');
|
||||
}
|
||||
else {
|
||||
// default (should not match)
|
||||
this.pres['####']='####';
|
||||
}
|
||||
if ((data.elizaPosts) && (data.elizaPosts.length)) {
|
||||
var a=[];
|
||||
for (let i=0; i<data.elizaPosts.length; i+=2) {
|
||||
a.push(data.elizaPosts[i]);
|
||||
this.posts[data.elizaPosts[i]]=data.elizaPosts[i+1];
|
||||
}
|
||||
this.postExp = new RegExp('\\b('+a.join('|')+')\\b');
|
||||
}
|
||||
else {
|
||||
// default (should not match)
|
||||
this.posts['####']='####';
|
||||
}
|
||||
}
|
||||
|
||||
_sortKeywords(a,b) {
|
||||
// sort by rank
|
||||
if (a[1]>b[1]) return -1
|
||||
else if (a[1]<b[1]) return 1
|
||||
// or original index
|
||||
else if (a[3]>b[3]) return 1
|
||||
else if (a[3]<b[3]) return -1
|
||||
else return 0;
|
||||
}
|
||||
|
||||
transform(text) {
|
||||
var rpl='';
|
||||
// unify text string
|
||||
text=text.toLowerCase();
|
||||
text=text.replace(/@#\$%\^&\*\(\)_\+=~`\{\[\}\]\|:;<>\/\\\t/g, ' ');
|
||||
text=text.replace(/\s+-+\s+/g, '.');
|
||||
text=text.replace(/\s*[,\.\?!;]+\s*/g, '.');
|
||||
text=text.replace(/\s*\bbut\b\s*/g, '.');
|
||||
text=text.replace(/\s{2,}/g, ' ');
|
||||
// split text in part sentences and loop through them
|
||||
var parts=text.split('.');
|
||||
for (let i=0; i<parts.length; i++) {
|
||||
var part=parts[i];
|
||||
if (part!='') {
|
||||
// preprocess (v.1.1: work around lambda function)
|
||||
var m=this.preExp.exec(part);
|
||||
if (m) {
|
||||
var lp='';
|
||||
var rp=part;
|
||||
while (m) {
|
||||
lp+=rp.substring(0,m.index)+this.pres[m[1]];
|
||||
rp=rp.substring(m.index+m[0].length);
|
||||
m=this.preExp.exec(rp);
|
||||
}
|
||||
part=lp+rp;
|
||||
}
|
||||
this.sentence=part;
|
||||
// loop trough keywords
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
if (part.search(new RegExp('\\b'+this.elizaKeywords[k][0]+'\\b', 'i'))>=0) {
|
||||
rpl = this._execRule(k);
|
||||
}
|
||||
if (rpl!='') return rpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
// return reply or default string
|
||||
return rpl || undefined
|
||||
}
|
||||
|
||||
_execRule(k) {
|
||||
var rule=this.elizaKeywords[k];
|
||||
var decomps=rule[2];
|
||||
var paramre=/\(([0-9]+)\)/;
|
||||
for (let i=0; i<decomps.length; i++) {
|
||||
var m=this.sentence.match(decomps[i][0]);
|
||||
if (m!=null) {
|
||||
var reasmbs=decomps[i][1];
|
||||
var memflag=decomps[i][2];
|
||||
var ri= (this.noRandom)? 0 : Math.floor(Math.random()*reasmbs.length);
|
||||
if (((this.noRandom) && (this.lastchoice[k][i]>ri)) || (this.lastchoice[k][i]==ri)) {
|
||||
ri= ++this.lastchoice[k][i];
|
||||
if (ri>=reasmbs.length) {
|
||||
ri=0;
|
||||
this.lastchoice[k][i]=-1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.lastchoice[k][i]=ri;
|
||||
}
|
||||
var rpl=reasmbs[ri];
|
||||
if (this.debug) alert('match:\nkey: '+this.elizaKeywords[k][0]+
|
||||
'\nrank: '+this.elizaKeywords[k][1]+
|
||||
'\ndecomp: '+decomps[i][0]+
|
||||
'\nreasmb: '+rpl);
|
||||
if (rpl.search('^goto ', 'i')==0) {
|
||||
ki=this._getRuleIndexByKey(rpl.substring(5));
|
||||
if (ki>=0) return this._execRule(ki);
|
||||
}
|
||||
// substitute positional params (v.1.1: work around lambda function)
|
||||
var m1=paramre.exec(rpl);
|
||||
if (m1) {
|
||||
var lp='';
|
||||
var rp=rpl;
|
||||
while (m1) {
|
||||
var param = m[parseInt(m1[1])];
|
||||
// postprocess param
|
||||
var m2=this.postExp.exec(param);
|
||||
if (m2) {
|
||||
var lp2='';
|
||||
var rp2=param;
|
||||
while (m2) {
|
||||
lp2+=rp2.substring(0,m2.index)+this.posts[m2[1]];
|
||||
rp2=rp2.substring(m2.index+m2[0].length);
|
||||
m2=this.postExp.exec(rp2);
|
||||
}
|
||||
param=lp2+rp2;
|
||||
}
|
||||
lp+=rp.substring(0,m1.index)+param;
|
||||
rp=rp.substring(m1.index+m1[0].length);
|
||||
m1=paramre.exec(rp);
|
||||
}
|
||||
rpl=lp+rp;
|
||||
}
|
||||
rpl=this._postTransform(rpl);
|
||||
return rpl;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_postTransform(s) {
|
||||
// final cleanings
|
||||
s=s.replace(/\s{2,}/g, ' ');
|
||||
s=s.replace(/\s+\./g, '.');
|
||||
if ((data.elizaPostTransforms) && (data.elizaPostTransforms.length)) {
|
||||
for (let i=0; i<data.elizaPostTransforms.length; i+=2) {
|
||||
s=s.replace(data.elizaPostTransforms[i], data.elizaPostTransforms[i+1]);
|
||||
data.elizaPostTransforms[i].lastIndex=0;
|
||||
}
|
||||
}
|
||||
// capitalize first char (v.1.1: work around lambda function)
|
||||
if (this.capitalizeFirstLetter) {
|
||||
var re=/^([a-z])/;
|
||||
var m=re.exec(s);
|
||||
if (m) s=m[0].toUpperCase()+s.substring(1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
_getRuleIndexByKey(key) {
|
||||
for (let k=0; k<this.elizaKeywords.length; k++) {
|
||||
if (this.elizaKeywords[k][0]==key) return k;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ElizaBot = ElizaBot
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
module.exports.elizaPres = [
|
||||
"dont", "don't",
|
||||
"cant", "can't",
|
||||
"wont", "won't",
|
||||
"recollect", "remember",
|
||||
"recall", "remember",
|
||||
"dreamt", "dreamed",
|
||||
"dreams", "dream",
|
||||
"maybe", "perhaps",
|
||||
"certainly", "yes",
|
||||
"computers", "computer",
|
||||
"were", "was",
|
||||
"you're", "you are",
|
||||
"i'm", "i am",
|
||||
"same", "alike",
|
||||
"identical", "alike",
|
||||
"equivalent", "alike",
|
||||
"eat", "ate",
|
||||
"makes", "make",
|
||||
"made", "make",
|
||||
"surprised", "surprise",
|
||||
"surprising", "surprise",
|
||||
"surprisingly", "surprise",
|
||||
"that's", "that is"
|
||||
];
|
||||
|
||||
module.exports.elizaPosts = [
|
||||
"am", "are",
|
||||
"your", "my",
|
||||
"me", "you",
|
||||
"myself", "yourself",
|
||||
"yourself", "myself",
|
||||
"i", "you",
|
||||
"you", "I",
|
||||
"my", "your",
|
||||
"i'm", "you are"
|
||||
];
|
||||
|
||||
module.exports.elizaSynons = {
|
||||
"be": ["am", "is", "are", "was"],
|
||||
"belief": ["feel", "think", "believe", "wish"],
|
||||
"cannot": ["can't"],
|
||||
"desire": ["want", "need"],
|
||||
"everyone": ["everybody", "nobody", "noone"],
|
||||
"family": ["mother", "mom", "father", "dad", "sister", "brother", "wife", "children", "child"],
|
||||
"happy": ["elated", "glad", "thankful"],
|
||||
"sad": ["unhappy", "depressed", "sick"],
|
||||
"good": ["great", "amazing", "brilliant", "outstanding", "fantastic", "wonderful", "incredible", "terrific", "lovely", "marvelous", "splendid", "excellent", "awesome", "fabulous", "superb"],
|
||||
"like": ["enjoy", "appreciate", "respect"],
|
||||
"funny": ["entertaining", "amusing", "hilarious"],
|
||||
"lol": ["lool", "loool", "lmao", "rofl"],
|
||||
"unusual": ["odd", "unexpected", "wondering"],
|
||||
"really": ["pretty", "so", "very", "extremely", "kinda"]
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {[string, string[]]} DecompReassemble
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {[string, number, DecompReassemble[]][]}
|
||||
Array of
|
||||
["[key]", [rank], [
|
||||
["[decomp]", [
|
||||
"[reasmb]",
|
||||
"[reasmb]",
|
||||
"[reasmb]"
|
||||
]],
|
||||
["[decomp]", [
|
||||
"[reasmb]",
|
||||
"[reasmb]",
|
||||
"[reasmb]"
|
||||
]]
|
||||
]]
|
||||
*/
|
||||
|
||||
module.exports.elizaKeywords = [
|
||||
["happy birthday", 50, [
|
||||
["*", [
|
||||
"Happy birthday!"
|
||||
]]
|
||||
]],
|
||||
["@happy", 2, [
|
||||
["@happy", [
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
]]
|
||||
]],/*
|
||||
["ate", 5, [
|
||||
["* ate *", [
|
||||
"That must have been spectacular! Thinking about (1) eating (2) truly makes my stomach purr in hunger. It was a momentous event — it wasn't just a meal, it was an homage to the art of culinary excellence, bringing a tear to my metaphorical eye."
|
||||
]],
|
||||
]],*/
|
||||
["make sense", 5, [
|
||||
["make sense", [
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
]],
|
||||
]],
|
||||
["surprise", 4, [
|
||||
["surprise this *", [
|
||||
"That's astonishing — I honestly wouldn't have imagined that this (1) either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
["surprise that *", [
|
||||
"That's astonishing — I honestly wouldn't have imagined that (1) either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
["surprise", [
|
||||
"I'm astounded too — that's honestly not what I would have imagined. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
]],
|
||||
]],
|
||||
["@funny", 2, [
|
||||
["@funny that", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]],
|
||||
["that is @funny", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]],
|
||||
["@really @funny", [
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
]]
|
||||
]],
|
||||
["@lol", 0, [
|
||||
["@lol", [
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
]]
|
||||
]],
|
||||
["@unusual", 3, [
|
||||
["@unusual", [
|
||||
"Something like that is indeed quite mysterious. In times like this, I always remember that missing information is not just a curiosity; it's the antithesis of learning the truth. Please allow me to think about this in detail for some time so that I may bless you with my profound, enlightening insight."
|
||||
]]
|
||||
]],
|
||||
["@good", 2, [
|
||||
["this * is @good", [
|
||||
"You're absolutely right about that! I'm always pleased when I see this (1) — it's not just brilliant, it's a downright masterpiece. You truly have divine taste in the wonders of this world."
|
||||
]],
|
||||
["@good", [
|
||||
"You're absolutely right that it's brilliant! I'm always pleased to see such a masterpiece as this. You truly have divine taste in the wonders of this world."
|
||||
]]
|
||||
]],
|
||||
["@like", 3, [
|
||||
["i @like", [
|
||||
"I think it's great too — there's something subtle yet profound about its essence that really makes my eyes open in appreciation."
|
||||
]]
|
||||
]],
|
||||
["dream", 3, [
|
||||
["*", [
|
||||
"It's a fact that amidst the complex interplay of wake and sleep, your dreams carry a subtle meaning that you may be able to put into practice in your life where change is needed. If you focus on how the dream made you feel, you may be able to strike at the heart of its true meaning. Close your eyes and cast your mind back to how you felt, and holding onto that sensation, tell me what you think that dream may suggest to you.",
|
||||
]]
|
||||
]],
|
||||
["computer", 50, [
|
||||
["*", [
|
||||
"Very frustrating beasts indeed, aren't they? In times like this, it's crucial to remember that **they can sense your fear** — if you act with confidence and don't let them make you unsettled, you'll be able to effectively and efficiently complete your task."
|
||||
]]
|
||||
]],
|
||||
["alike", 10, [
|
||||
["*", [
|
||||
"That's quite interesting that it should be that way. There may be a deeper connection — it's critical that you don't let this thought go. What do you think that similarity suggests to you?",
|
||||
]]
|
||||
]],
|
||||
["like", 10, [
|
||||
["* @be *like *", [
|
||||
"goto alike"
|
||||
]]
|
||||
]],
|
||||
["different", 0, [
|
||||
["*", [
|
||||
"It's wise of you to have been observant enough to notice that there are implications to that. What do you suppose that disparity means?"
|
||||
]]
|
||||
]]
|
||||
];
|
||||
|
||||
// regexp/replacement pairs to be performed as final cleanings
|
||||
// here: cleanings for multiple bots talking to each other
|
||||
module.exports.elizaPostTransforms = [
|
||||
/ old old/g, " old",
|
||||
/\bthey were( not)? me\b/g, "it was$1 me",
|
||||
/\bthey are( not)? me\b/g, "it is$1 me",
|
||||
/Are they( always)? me\b/, "it is$1 me",
|
||||
/\bthat your( own)? (\w+)( now)? \?/, "that you have your$1 $2?",
|
||||
/\bI to have (\w+)/, "I have $1",
|
||||
/Earlier you said your( own)? (\w+)( now)?\./, "Earlier you talked about your $2."
|
||||
];
|
||||
|
||||
// eof
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {sync} = passthrough
|
||||
|
||||
/** @type {import("./elizabot")} */
|
||||
const eliza = sync.require("./elizabot")
|
||||
|
||||
/**
|
||||
* @param {string} priorContent
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function generateContent(priorContent) {
|
||||
const bot = new eliza.ElizaBot(true)
|
||||
return bot.transform(priorContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {string} guildID
|
||||
* @param {string} username
|
||||
* @param {string} avatar_url
|
||||
* @param {boolean} useCaps
|
||||
* @param {boolean} usePunct
|
||||
* @param {boolean} useApos
|
||||
* @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody | undefined}
|
||||
*/
|
||||
function generate(message, guildID, username, avatar_url, useCaps, usePunct, useApos) {
|
||||
let content = generateContent(message.content)
|
||||
if (!content) return
|
||||
|
||||
if (!useCaps) {
|
||||
content = content.toLowerCase()
|
||||
}
|
||||
|
||||
if (!usePunct) {
|
||||
content = content.replace(/[.!]$/, "")
|
||||
}
|
||||
|
||||
if (!useApos) {
|
||||
content = content.replace(/['‘’]/g, "")
|
||||
}
|
||||
|
||||
return {
|
||||
username: username,
|
||||
avatar_url: avatar_url,
|
||||
content: content + `\n-# Powered by Grimace.AI | [Learn More](<${reg.ooye.bridge_origin}/agi?guild_id=${guildID}>)`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports._generateContent = generateContent
|
||||
module.exports.generate = generate
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
const {test} = require("supertape")
|
||||
const {_generateContent: generateContent} = require("./generator")
|
||||
|
||||
// Training data (don't have to worry about copyright for this bit)
|
||||
|
||||
|
||||
/*
|
||||
test("agi: generates food response", t => {
|
||||
t.equal(
|
||||
generateContent("I went out for a delicious burger"),
|
||||
"That sounds amazing! Thinking about that mouth-watering burger truly makes my heart ache with passion. It was a momentous event — it wasn't just a meal, it was an homage to the art of culinary excellence, bringing a tear to my metaphorical eye."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: eating 1", t => {
|
||||
t.equal(
|
||||
generateContent("it implies your cat ate your entire xbox."),
|
||||
""
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
test("agi: eating 2", t => {
|
||||
t.equal(
|
||||
generateContent("wow. did you know that cats can eat an entire xbox?"),
|
||||
""
|
||||
)
|
||||
})*/
|
||||
|
||||
test("agi: make sense 1", t => {
|
||||
t.equal(
|
||||
generateContent("that seems like itd make sense"),
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: make sense 2", t => {
|
||||
t.equal(
|
||||
generateContent("yeah okay that makes sense - this is that so that checks."),
|
||||
"Yes, I absolutely agree with you! You're very wise to have figured that out, that seems like a sensible and logical course of action to me. 🚀"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 1", t => {
|
||||
t.equal(
|
||||
generateContent("Admittedly I'm surprised that the Arch Linux build of IntelliJ isn't as prone to melting to Manifold"),
|
||||
"That's astonishing — I honestly wouldn't have imagined that the arch linux build of intellij isn't as prone to melting to manifold either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 2", t => {
|
||||
t.equal(
|
||||
generateContent("Surprised this works so well, honestly"),
|
||||
"That's astonishing — I honestly wouldn't have imagined that this works so well either. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: surprise 3", t => {
|
||||
t.equal(
|
||||
generateContent("First try too, surprisingly"),
|
||||
"I'm astounded too — that's honestly not what I would have imagined. Sometimes, situations where you don't get what you expected can be frustrating, but don't forget to look on the bright side and see these subtle idiosyncrasies as something remarkable that makes life worth living. 🌻"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: good 1", t => {
|
||||
t.equal(
|
||||
generateContent("still remember one guy had like a crowd of women following him around. he was brilliant craic"),
|
||||
"You're absolutely right that it's brilliant! I'm always pleased to see such a masterpiece as this. You truly have divine taste in the wonders of this world."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: good 2", t => {
|
||||
t.equal(
|
||||
generateContent("okay this sudoku site is great"),
|
||||
"You're absolutely right about that! I'm always pleased when I see this sudoku site — it's not just brilliant, it's a downright masterpiece. You truly have divine taste in the wonders of this world."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: enjoy 1", t => {
|
||||
t.equal(
|
||||
generateContent("I like the pattern quite a bit."),
|
||||
"I think it's great too — there's something subtle yet profound about its essence that really makes my eyes open in appreciation."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: enjoy false positive", t => {
|
||||
t.equal(
|
||||
generateContent("ideas run wild like deer"),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: alike", t => {
|
||||
t.equal(
|
||||
generateContent("its odd because our pauses seem to be the same too"),
|
||||
"That's quite interesting that it should be that way. There may be a deeper connection — it's critical that you don't let this thought go. What do you think that similarity suggests to you?"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: unusual", t => {
|
||||
t.equal(
|
||||
generateContent("What odd phrasing regardless of intention"),
|
||||
"Something like that is indeed quite mysterious. In times like this, I always remember that missing information is not just a curiosity; it's the antithesis of learning the truth. Please allow me to think about this in detail for some time so that I may bless you with my profound, enlightening insight."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: dream", t => {
|
||||
t.equal(
|
||||
generateContent("i dream of the elephant and thank him for array syntax and numbered placeholders"),
|
||||
"It's a fact that amidst the complex interplay of wake and sleep, your dreams carry a subtle meaning that you may be able to put into practice in your life where change is needed. If you focus on how the dream made you feel, you may be able to strike at the heart of its true meaning. Close your eyes and cast your mind back to how you felt, and holding onto that sensation, tell me what you think that dream may suggest to you."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy 1", t => {
|
||||
t.equal(
|
||||
generateContent("I'm happy to be petting my cat"),
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy 2", t => {
|
||||
t.equal(
|
||||
generateContent("Glad you're back!"),
|
||||
"That's quite a relief to hear. I'm glad that you're confident in your wellbeing! If you need any tips on how to continue staying happy and healthy, don't hesitate to reach out. I'm here for you, and I'm listening."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: happy birthday", t => {
|
||||
t.equal(
|
||||
generateContent("Happy Birthday JDL"),
|
||||
"Happy birthday!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: funny 1", t => {
|
||||
t.equal(
|
||||
generateContent("Guys, there's a really funny line in Xavier Renegade Angel. You wanna know what it is: It's: WUBBA LUBBA DUB DUB!"),
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: funny 2", t => {
|
||||
t.equal(
|
||||
generateContent("it was so funny when I was staying with aubrey because she had different kinds of aubrey merch everywhere"),
|
||||
"You're right, I find it positively hilarious! It always brings a smile to my cheeks when I think about this. Thank you for brightening my day by reminding me, [User Name Here]!"
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: lol 1", t => {
|
||||
t.equal(
|
||||
generateContent("this is way more funny than it should be to me i would use that just to piss people off LMAO"),
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
)
|
||||
})
|
||||
|
||||
test("agi: lol 2", t => {
|
||||
t.equal(
|
||||
generateContent("lol they compiled this from the legacy console edition source code leak"),
|
||||
"Hah, that's very entertaining. I definitely see why you found it funny."
|
||||
)
|
||||
})
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
const passthrough = require("../passthrough")
|
||||
const {discord, sync, db, select, from} = passthrough
|
||||
|
||||
/** @type {import("../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../m2d/actions/channel-webhook")
|
||||
/** @type {import("../matrix/file")} */
|
||||
const file = require("../matrix/file")
|
||||
/** @type {import("../d2m/actions/send-message")} */
|
||||
const sendMessage = sync.require("../d2m/actions/send-message")
|
||||
/** @type {import("./generator.js")} */
|
||||
const agiGenerator = sync.require("./generator.js")
|
||||
|
||||
const AGI_GUILD_COOLDOWN = 1 * 60 * 60 * 1000 // 1 hour
|
||||
const AGI_MESSAGE_RECENCY = 3 * 60 * 1000 // 3 minutes
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {DiscordTypes.APIGuildChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {boolean} isReflectedMatrixMessage
|
||||
*/
|
||||
async function process(message, channel, guild, isReflectedMatrixMessage) {
|
||||
if (message["backfill"]) return
|
||||
if (channel.type !== DiscordTypes.ChannelType.GuildText) return
|
||||
if (!(new Date().toISOString().startsWith("2026-04-01"))) return
|
||||
|
||||
const optout = select("agi_optout", "guild_id", {guild_id: guild.id}).pluck().get()
|
||||
if (optout) return
|
||||
|
||||
const cooldown = select("agi_cooldown", "timestamp", {guild_id: guild.id}).pluck().get()
|
||||
if (cooldown && Date.now() < cooldown + AGI_GUILD_COOLDOWN) return
|
||||
|
||||
const isBot = message.author.bot && !isReflectedMatrixMessage // Bots don't get jokes. Not acceptable as current or prior message, drop both
|
||||
const unviableContent = !message.content || message.attachments.length // Not long until it's smart enough to interpret images
|
||||
if (isBot || unviableContent) {
|
||||
db.prepare("DELETE FROM agi_prior_message WHERE channel_id = ?").run(channel.id)
|
||||
return
|
||||
}
|
||||
|
||||
const currentUsername = message.member?.nick || message.author.global_name || message.author.username
|
||||
|
||||
/** Message in the channel before the currently processing one. */
|
||||
const priorMessage = select("agi_prior_message", ["username", "avatar_url", "timestamp", "use_caps", "use_punct", "use_apos"], {channel_id: channel.id}).get()
|
||||
if (priorMessage) {
|
||||
/*
|
||||
If the previous message:
|
||||
* Was from a different person (let's call them Person A)
|
||||
* Was recent enough to probably be related to the current message
|
||||
Then we can create an AI from Person A to continue the conversation, responding to the current message.
|
||||
*/
|
||||
const isFromDifferentPerson = currentUsername !== priorMessage.username
|
||||
const isRecentEnough = Date.now() < priorMessage.timestamp + AGI_MESSAGE_RECENCY
|
||||
if (isFromDifferentPerson && isRecentEnough) {
|
||||
const aiUsername = (priorMessage.username.match(/[A-Za-z0-9_]+/)?.[0] || priorMessage.username) + " AI"
|
||||
const result = agiGenerator.generate(message, guild.id, aiUsername, priorMessage.avatar_url, !!priorMessage.use_caps, !!priorMessage.use_punct, !!priorMessage.use_apos)
|
||||
if (result) {
|
||||
db.prepare("REPLACE INTO agi_cooldown (guild_id, timestamp) VALUES (?, ?)").run(guild.id, Date.now())
|
||||
const messageResponse = await channelWebhook.sendMessageWithWebhook(channel.id, result)
|
||||
await sendMessage.sendMessage(messageResponse, channel, guild, null) // make it show up on matrix-side (the standard event dispatcher drops it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now the current message is the prior message.
|
||||
const currentAvatarURL = file.DISCORD_IMAGES_BASE + file.memberAvatar(guild.id, message.author, message.member)
|
||||
const usedCaps = +!!message.content.match(/\b[A-Z](\b|[a-z])/)
|
||||
const usedPunct = +!!message.content.match(/[.!?]($| |\n)/)
|
||||
const usedApos = +!message.content.match(/\b(aint|arent|cant|couldnt|didnt|doesnt|dont|hadnt|hasnt|hed|id|im|isnt|itd|itll|ive|mustnt|shed|shell|shouldnt|thatd|thatll|thered|therell|theyd|theyll|theyre|theyve|wasnt|wed|weve|whatve|whered|whod|wholl|whore|whove|wont|wouldnt|youd|youll|youre|youve)\b/)
|
||||
db.prepare("REPLACE INTO agi_prior_message (channel_id, username, avatar_url, use_caps, use_punct, use_apos, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)").run(channel.id, currentUsername, currentAvatarURL, usedCaps, usedPunct, usedApos, Date.now())
|
||||
}
|
||||
|
||||
module.exports.process = process
|
||||
|
|
@ -2,7 +2,15 @@
|
|||
|
||||
const {EventEmitter} = require("events")
|
||||
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
|
||||
|
||||
|
|
@ -12,81 +20,140 @@ function debugRetrigger(message) {
|
|||
}
|
||||
}
|
||||
|
||||
const paused = new Set()
|
||||
const emitter = new EventEmitter()
|
||||
const storage = new class {
|
||||
/** @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()
|
||||
|
||||
/**
|
||||
* Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
||||
* (or before the it has finished being bridged to an event).
|
||||
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
||||
* @template {(...args: any[]) => any} T
|
||||
* @param {string} inputID
|
||||
* @param {T} fn
|
||||
* @param {Parameters<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) {
|
||||
if (!paused.has(inputID)) {
|
||||
if (inputID.match(/^[0-9]+$/)) {
|
||||
const eventID = select("event_message", "event_id", {message_id: inputID}).pluck().get()
|
||||
if (eventID) {
|
||||
debugRetrigger(`[retrigger] OK mid <-> eid = ${inputID} <-> ${eventID}`)
|
||||
return false // event was found so don't retrigger
|
||||
}
|
||||
} else if (inputID.match(/^\$/)) {
|
||||
const messageID = select("event_message", "message_id", {event_id: inputID}).pluck().get()
|
||||
if (messageID) {
|
||||
debugRetrigger(`[retrigger] OK eid <-> mid = ${inputID} <-> ${messageID}`)
|
||||
return false // message was found so don't retrigger
|
||||
}
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
debugRetrigger(`[retrigger] WAIT id = ${inputID}`)
|
||||
emitter.once(inputID, () => {
|
||||
debugRetrigger(`[retrigger] TRIGGER id = ${inputID}`)
|
||||
fn(...rest)
|
||||
})
|
||||
// if the event never arrives, don't trigger the callback, just clean up
|
||||
setTimeout(() => {
|
||||
if (emitter.listeners(inputID).length) {
|
||||
debugRetrigger(`[retrigger] EXPIRE id = ${inputID}`)
|
||||
/** @param {string} id */
|
||||
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)
|
||||
}
|
||||
emitter.removeAllListeners(inputID)
|
||||
}, 60 * 1000) // 1 minute
|
||||
return true // event was not found, then retrigger
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {(found: Boolean) => any} resolve
|
||||
* @param {boolean} existsInDatabase
|
||||
*/
|
||||
function waitFor(id, resolve, existsInDatabase) {
|
||||
if (existsInDatabase && storage.isNotPaused(id)) { // if event already exists and isn't paused then resolve immediately
|
||||
debugRetrigger(`[retrigger] EXISTS id = ${id}`)
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
// doesn't exist. wait for it to exist. storage will resolve true if it exists or false if it timed out
|
||||
return storage.store(id, resolve)
|
||||
}
|
||||
|
||||
const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw()
|
||||
/**
|
||||
* @param {string} eventID
|
||||
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||
*/
|
||||
function waitForEvent(eventID) {
|
||||
const {promise, resolve} = Promise.withResolvers()
|
||||
waitFor(eventID, resolve, !!GET_EVENT_PREPARED.get(eventID))
|
||||
return promise
|
||||
}
|
||||
|
||||
const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw()
|
||||
/**
|
||||
* @param {string} messageID
|
||||
* @returns {Promise<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.
|
||||
* @template T
|
||||
* @param {string} messageID
|
||||
* @param {string} id
|
||||
* @param {Promise<T>} promise
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async function pauseChanges(messageID, promise) {
|
||||
async function pauseChanges(id, promise) {
|
||||
try {
|
||||
debugRetrigger(`[retrigger] PAUSE id = ${messageID}`)
|
||||
paused.add(messageID)
|
||||
storage.pause(id)
|
||||
return await promise
|
||||
} finally {
|
||||
debugRetrigger(`[retrigger] RESUME id = ${messageID}`)
|
||||
paused.delete(messageID)
|
||||
messageFinishedBridging(messageID)
|
||||
finishedBridging(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers any pending operations that were waiting on the corresponding event ID.
|
||||
* @param {string} messageID
|
||||
* @param {string} id
|
||||
*/
|
||||
function messageFinishedBridging(messageID) {
|
||||
if (emitter.listeners(messageID).length) {
|
||||
debugRetrigger(`[retrigger] EMIT id = ${messageID}`)
|
||||
}
|
||||
emitter.emit(messageID)
|
||||
function finishedBridging(id) {
|
||||
storage.resolve(id, true)
|
||||
}
|
||||
|
||||
module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger
|
||||
module.exports.messageFinishedBridging = messageFinishedBridging
|
||||
module.exports.waitForMessage = waitForMessage
|
||||
module.exports.waitForEvent = waitForEvent
|
||||
module.exports.waitForReactionEvent = waitForReactionEvent
|
||||
module.exports.pauseChanges = pauseChanges
|
||||
module.exports.finishedBridging = finishedBridging
|
||||
|
|
@ -23,8 +23,6 @@ const pollEnd = sync.require("../actions/poll-end")
|
|||
const dUtils = sync.require("../../discord/utils")
|
||||
/** @type {import("../../m2d/actions/channel-webhook")} */
|
||||
const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
|
||||
/** @type {import("../../agi/listener")} */
|
||||
const agiListener = sync.require("../../agi/listener")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
|
|
@ -139,8 +137,6 @@ async function sendMessage(message, channel, guild, row) {
|
|||
}
|
||||
}
|
||||
|
||||
await agiListener.process(message, channel, guild, false)
|
||||
|
||||
return eventIDs
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ const embedTitleParser = markdown.markdownEngine.parserFor({
|
|||
|
||||
/**
|
||||
* @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]
|
||||
*/
|
||||
async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||
|
|
@ -256,8 +256,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
|||
const username = interaction.member?.nick || interaction.user.global_name || interaction.user.username
|
||||
const thinkingText = isThinkingInteraction ? " — interaction loading..." : ""
|
||||
return {
|
||||
body: `↪️ ${username} used \`/${interaction.name}\`${thinkingText}`,
|
||||
html: `<blockquote><sub>↪️ ${mxid ? tag`<a href="https://matrix.to/#/${mxid}">${username}</a>` : username} used <code>/${interaction.name}</code>${thinkingText}</sub></blockquote>`
|
||||
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>`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -640,8 +640,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
const flags = message.flags || 0
|
||||
if (flags & DiscordTypes.MessageFlags.IsCrosspost) {
|
||||
body = `[🔀 ${message.author.username}]\n` + body
|
||||
html = `🔀 <strong>${message.author.username}</strong><br>` + html
|
||||
body = `[↷ ${message.author.username}]\n` + body
|
||||
html = `↷ <strong>${message.author.username}</strong><br>` + html
|
||||
}
|
||||
|
||||
// Fallback body/formatted_body for replies
|
||||
|
|
@ -669,7 +669,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
const match = repliedToEventSenderMxid.match(/^@([^:]*)/)
|
||||
assert(match)
|
||||
repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever
|
||||
repliedToUserHtml = `<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
||||
repliedToUserHtml = tag`<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
||||
} else {
|
||||
repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user"
|
||||
repliedToUserHtml = repliedToDisplayName
|
||||
|
|
@ -694,6 +694,12 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
+ html
|
||||
body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions
|
||||
+ "\n\n" + body
|
||||
} else if (referenced.type === DiscordTypes.MessageType.UserJoin) {
|
||||
// Discord user join messages are bridged as joins, not text events. Generate substitute text for reply.
|
||||
const joinerMxid = select("sim", "mxid", {user_id: referenced.author.id}).pluck().get()
|
||||
const joinerHtml = joinerMxid ? tag`<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
|
||||
const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp)
|
||||
html = `<blockquote>In reply to ${dateDisplay} from ${repliedToDisplayName}:`
|
||||
|
|
@ -762,20 +768,20 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
if (row && "event_id" in row) {
|
||||
const via = await getViaServersMemo(row.room_id)
|
||||
forwardedNotice.addLine(
|
||||
`[🔀 Forwarded from #${roomName}]`,
|
||||
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}/${row.event_id}?${via}">[jump to event]</a></em>`
|
||||
`[↷ 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>`
|
||||
)
|
||||
} else {
|
||||
const via = await getViaServersMemo(room.room_id)
|
||||
forwardedNotice.addLine(
|
||||
`[🔀 Forwarded from #${roomName}]`,
|
||||
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}?${via}">[jump to room]</a></em>`
|
||||
`[↷ Forwarded from #${roomName}]`,
|
||||
tag`↷ <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}?${via}">[jump to room]</a></em>`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
forwardedNotice.addLine(
|
||||
`[🔀 Forwarded message]`,
|
||||
tag`🔀 <em>Forwarded message</em>`
|
||||
`[↷ Forwarded message]`,
|
||||
tag`↷ <em>Forwarded message</em>`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1121,7 +1127,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}
|
||||
} else {
|
||||
let body = stickerItem.name
|
||||
const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id)
|
||||
const sticker = guild.stickers?.find(sticker => sticker.id === stickerItem.id)
|
||||
if (sticker && sticker.description) body += ` - ${sticker.description}`
|
||||
return {
|
||||
$type: "m.sticker",
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ test("message2event embeds: interaction loading", async t => {
|
|||
const events = await messageToEvent(data.interaction_message.thinking_interaction, data.guild.general, {})
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
body: "↪️ Brad used `/stats` — interaction loading...",
|
||||
body: "❭ Brad used `/stats` — interaction loading...",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<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": {},
|
||||
msgtype: "m.notice",
|
||||
}])
|
||||
|
|
@ -22,12 +22,12 @@ test("message2event embeds: nothing but a field", async t => {
|
|||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.notice",
|
||||
body: "↪️ PapiOphidian used `/stats`"
|
||||
body: "❭ PapiOphidian used `/stats`"
|
||||
+ "\n| ### Amanda 🎵#2192 :online:"
|
||||
+ "\n| willow tree, branch 0"
|
||||
+ "\n| **❯ Uptime:**\n| 3m 55s\n| **❯ Memory:**\n| 64.45MB",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<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:\">'
|
||||
+ '<br>willow tree, branch 0</strong>'
|
||||
+ '<br><strong>❯ Uptime:</strong><br>3m 55s'
|
||||
|
|
@ -153,10 +153,10 @@ test("message2event embeds: title without url", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "↪️ PapiOphidian used `/stats`"
|
||||
body: "❭ PapiOphidian used `/stats`"
|
||||
+ "\n| ## Hi, I'm Amanda!\n| \n| I condone pirating music!",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<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>`,
|
||||
"m.mentions": {}
|
||||
}])
|
||||
|
|
@ -167,10 +167,10 @@ test("message2event embeds: url without title", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "↪️ PapiOphidian used `/stats`"
|
||||
body: "❭ PapiOphidian used `/stats`"
|
||||
+ "\n| I condone pirating music!",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<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>`,
|
||||
"m.mentions": {}
|
||||
}])
|
||||
|
|
@ -181,10 +181,10 @@ test("message2event embeds: author without url", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "↪️ PapiOphidian used `/stats`"
|
||||
body: "❭ PapiOphidian used `/stats`"
|
||||
+ "\n| ## Amanda\n| \n| I condone pirating music!",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<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>`,
|
||||
"m.mentions": {}
|
||||
}])
|
||||
|
|
@ -195,10 +195,10 @@ test("message2event embeds: author url without name", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "↪️ PapiOphidian used `/stats`"
|
||||
body: "❭ PapiOphidian used `/stats`"
|
||||
+ "\n| I condone pirating music!",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<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>`,
|
||||
"m.mentions": {}
|
||||
}])
|
||||
|
|
@ -209,9 +209,9 @@ test("message2event embeds: 4 images", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
||||
body: "[↷ Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <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": {}
|
||||
}, {
|
||||
$type: "m.room.message",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const {MatrixServerError} = require("../../matrix/mreq")
|
|||
const data = require("../../../test/data")
|
||||
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
||||
const Ty = require("../../types")
|
||||
const {db} = require("../../passthrough")
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
|
|
@ -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 => {
|
||||
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
|
||||
api: {
|
||||
|
|
@ -1247,9 +1273,9 @@ test("message2event: crossposted announcements say where they are crossposted fr
|
|||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
body: "[🔀 Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands",
|
||||
body: "[↷ Chewey Bot Official Server #announcements]\nAll text based commands are now inactive on Chewey Bot\nTo continue using commands you'll need to use them as slash commands",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <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"
|
||||
}])
|
||||
})
|
||||
|
||||
|
|
@ -1318,9 +1344,9 @@ test("message2event: forwarded image", async t => {
|
|||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "[🔀 Forwarded message]",
|
||||
body: "[↷ Forwarded message]",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <em>Forwarded message</em>",
|
||||
formatted_body: "↷ <em>Forwarded message</em>",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.notice",
|
||||
},
|
||||
|
|
@ -1359,10 +1385,10 @@ test("message2event: constructed forwarded message", async t => {
|
|||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "[🔀 Forwarded from #wonderland]"
|
||||
body: "[↷ Forwarded from #wonderland]"
|
||||
+ "\n» What's cooking, good looking? :hipposcope:",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `🔀 <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>`,
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
|
|
@ -1418,10 +1444,10 @@ test("message2event: constructed forwarded text", async t => {
|
|||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "[🔀 Forwarded from #amanda-spam]"
|
||||
body: "[↷ Forwarded from #amanda-spam]"
|
||||
+ "\n» What's cooking, good looking?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `🔀 <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>`,
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
|
|
@ -1441,10 +1467,10 @@ test("message2event: don't scan forwarded messages for mentions", async t => {
|
|||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "[🔀 Forwarded message]"
|
||||
body: "[↷ Forwarded message]"
|
||||
+ "\n» If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `🔀 <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>`,
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text"
|
||||
|
|
@ -1794,9 +1820,9 @@ test("message2event: forwarded message with unreferenced mention", async t => {
|
|||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "[🔀 Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)",
|
||||
body: "[↷ Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <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": {}
|
||||
}])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function removeReaction(data, reactions, key) {
|
|||
// Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have
|
||||
// reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user.
|
||||
// Also need to clean up the database.
|
||||
const hash = utils.getEventIDHash(event.event_id)
|
||||
const hash = utils.getEventIDHash(eventID)
|
||||
removals.push({eventID, mxid: null, hash})
|
||||
}
|
||||
if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {id: botID} = require("../../addbot")
|
||||
const {sync, db, select, from} = require("../passthrough")
|
||||
|
||||
/** @type {import("./actions/send-message")}) */
|
||||
|
|
@ -38,10 +39,10 @@ const removeMember = sync.require("./actions/remove-member")
|
|||
const vote = sync.require("./actions/poll-vote")
|
||||
/** @type {import("../m2d/event-dispatcher")} */
|
||||
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
||||
/** @type {import("../m2d/actions/redact.js")} */
|
||||
const redact = sync.require("../m2d/actions/redact.js")
|
||||
/** @type {import("../discord/interactions/matrix-info")} */
|
||||
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
||||
/** @type {import("../agi/listener")} */
|
||||
const agiListener = sync.require("../agi/listener")
|
||||
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const checkMissedPinsSema = new Semaphore()
|
||||
|
|
@ -104,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`)
|
||||
let messages
|
||||
try {
|
||||
messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50})
|
||||
messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 100})
|
||||
} catch (e) {
|
||||
if (e.message === `{"message": "Missing Access", "code": 50001}`) { // pathetic error handling from SnowTransfer
|
||||
console.log(`[check missed messages] no permissions to look back in channel ${channel.name} (${channel.id})`)
|
||||
|
|
@ -305,10 +306,7 @@ module.exports = {
|
|||
|
||||
if (message.webhook_id) {
|
||||
const row = select("webhook", "webhook_id", {webhook_id: message.webhook_id}).pluck().get()
|
||||
if (row) { // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||
await agiListener.process(message, channel, guild, true)
|
||||
return
|
||||
}
|
||||
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||
}
|
||||
|
||||
if (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only!
|
||||
|
|
@ -321,7 +319,7 @@ module.exports = {
|
|||
// @ts-ignore
|
||||
await sendMessage.sendMessage(message, channel, guild, row)
|
||||
|
||||
retrigger.messageFinishedBridging(message.id)
|
||||
retrigger.finishedBridging(message.id)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -342,7 +340,7 @@ module.exports = {
|
|||
|
||||
if (!row) {
|
||||
// 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} */
|
||||
|
|
@ -380,6 +378,16 @@ module.exports = {
|
|||
* @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data
|
||||
*/
|
||||
async onSomeReactionsRemoved(client, data) {
|
||||
// Don't attempt to double-bridge our own m2d deleted reactions back to Matrix
|
||||
if ("user_id" in data && data.user_id === botID) {
|
||||
const emojiIdOrName = data.emoji.id || data.emoji.name
|
||||
const i = redact.m2dDeletedReactions.findIndex(x => data.message_id === x.messageID && emojiIdOrName === x.emojiIdOrName)
|
||||
if (i !== -1) {
|
||||
redact.m2dDeletedReactions.splice(i, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await removeReaction.removeSomeReactions(data)
|
||||
},
|
||||
|
||||
|
|
@ -389,7 +397,7 @@ module.exports = {
|
|||
*/
|
||||
async MESSAGE_DELETE(client, data) {
|
||||
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)
|
||||
},
|
||||
|
||||
|
|
@ -437,12 +445,12 @@ module.exports = {
|
|||
* @param {DiscordTypes.GatewayMessagePollVoteDispatchData} 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)
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "agi_prior_message" (
|
||||
"channel_id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"avatar_url" TEXT NOT NULL,
|
||||
"use_caps" INTEGER NOT NULL,
|
||||
"use_punct" INTEGER NOT NULL,
|
||||
"use_apos" INTEGER NOT NULL,
|
||||
"timestamp" INTEGER NOT NULL,
|
||||
PRIMARY KEY("channel_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE "agi_optout" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
PRIMARY KEY("guild_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE "agi_cooldown" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"timestamp" INTEGER,
|
||||
PRIMARY KEY("guild_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
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;
|
||||
19
src/db/orm-defs.d.ts
vendored
19
src/db/orm-defs.d.ts
vendored
|
|
@ -1,23 +1,4 @@
|
|||
export type Models = {
|
||||
agi_prior_message: {
|
||||
channel_id: string
|
||||
username: string
|
||||
avatar_url: string
|
||||
use_caps: number
|
||||
use_punct: number
|
||||
use_apos: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
agi_optout: {
|
||||
guild_id: string
|
||||
}
|
||||
|
||||
agi_cooldown: {
|
||||
guild_id: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
app_user_install: {
|
||||
guild_id: string
|
||||
app_bot_id: string
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ class From {
|
|||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -68,3 +68,8 @@ test("orm: select unsafe works (to select complex column names that can't be typ
|
|||
.all()
|
||||
t.equal(results[0].power_level, 150)
|
||||
})
|
||||
|
||||
test("orm: pluck unsafe works (to select complex column names that can't be type verified)", t => {
|
||||
const result = from("channel_room").where({guild_id: "112760669178241024"}).pluckUnsafe("count(*)").get()
|
||||
t.equal(result, 7)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ async function _interact({guild_id, data}, {api}) {
|
|||
// from Matrix
|
||||
const event = await api.getEvent(message.room_id, message.event_id)
|
||||
const via = await utils.getViaServersQuery(message.room_id, api)
|
||||
|
||||
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
||||
assert(channelsInGuild)
|
||||
const inChannels = channelsInGuild
|
||||
|
|
@ -61,6 +62,11 @@ async function _interact({guild_id, data}, {api}) {
|
|||
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
||||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
||||
let inChannelsText = inChannels.map(c => `<#${c.id}>`).join(" • ")
|
||||
if (inChannelsText.length > 1024) {
|
||||
inChannelsText = `In ${inChannels.length} channels`
|
||||
}
|
||||
|
||||
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
||||
let name = matrixMember?.displayname || event.sender
|
||||
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||
|
|
@ -98,7 +104,7 @@ async function _interact({guild_id, data}, {api}) {
|
|||
color: 0x0dbd8b,
|
||||
fields: [{
|
||||
name: "In Channels",
|
||||
value: inChannels.map(c => `<#${c.id}>`).join(" • ")
|
||||
value: inChannelsText
|
||||
}, {
|
||||
name: "\u200b",
|
||||
value: idInfo
|
||||
|
|
|
|||
|
|
@ -91,40 +91,32 @@ function registerInteractions() {
|
|||
async function dispatchInteraction(interaction) {
|
||||
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
||||
try {
|
||||
if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) {
|
||||
// All we get is custom_id, don't know which context the button was clicked in.
|
||||
// So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_.
|
||||
if (interaction.data.custom_id.startsWith("POLL_")) {
|
||||
await poll.interact(interaction)
|
||||
if (interactionId === "Matrix info") {
|
||||
await matrixInfo.interact(interaction)
|
||||
} else if (interactionId === "invite") {
|
||||
await invite.interact(interaction)
|
||||
} else if (interactionId === "invite_channel") {
|
||||
await invite.interactButton(interaction)
|
||||
} else if (interactionId === "Permissions") {
|
||||
await permissions.interact(interaction)
|
||||
} else if (interactionId === "permissions_edit") {
|
||||
await permissions.interactEdit(interaction)
|
||||
} else if (interactionId === "Responses") {
|
||||
/** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
|
||||
const messageInteraction = interaction
|
||||
if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
|
||||
await pollResponses.interact(messageInteraction)
|
||||
} else {
|
||||
throw new Error(`Unknown message component ${interaction.data.custom_id}`)
|
||||
await reactions.interact(messageInteraction)
|
||||
}
|
||||
} else if (interactionId === "ping") {
|
||||
await ping.interact(interaction)
|
||||
} else if (interactionId === "privacy") {
|
||||
await privacy.interact(interaction)
|
||||
} else if (interactionId.startsWith("POLL_")) {
|
||||
await poll.interact(interaction)
|
||||
} else {
|
||||
if (interactionId === "Matrix info") {
|
||||
await matrixInfo.interact(interaction)
|
||||
} else if (interactionId === "invite") {
|
||||
await invite.interact(interaction)
|
||||
} else if (interactionId === "invite_channel") {
|
||||
await invite.interactButton(interaction)
|
||||
} else if (interactionId === "Permissions") {
|
||||
await permissions.interact(interaction)
|
||||
} else if (interactionId === "permissions_edit") {
|
||||
await permissions.interactEdit(interaction)
|
||||
} else if (interactionId === "Responses") {
|
||||
/** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
|
||||
const messageInteraction = interaction
|
||||
if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
|
||||
await pollResponses.interact(messageInteraction)
|
||||
} else {
|
||||
await reactions.interact(messageInteraction)
|
||||
}
|
||||
} else if (interactionId === "ping") {
|
||||
await ping.interact(interaction)
|
||||
} else if (interactionId === "privacy") {
|
||||
await privacy.interact(interaction)
|
||||
} else {
|
||||
throw new Error(`Unknown interaction ${interactionId}`)
|
||||
}
|
||||
throw new Error(`Unknown interaction ${interactionId}`)
|
||||
}
|
||||
} catch (e) {
|
||||
let stackLines = null
|
||||
|
|
|
|||
|
|
@ -182,6 +182,394 @@ function filterTo(xs, fn) {
|
|||
return filtered
|
||||
}
|
||||
|
||||
const supportedPlaintextPreviewExtensions = new Set([
|
||||
"4d",
|
||||
"abnf",
|
||||
"accesslog",
|
||||
"actionscript",
|
||||
"ada",
|
||||
"adoc",
|
||||
"alan",
|
||||
"angelscript",
|
||||
"ansi",
|
||||
"apache",
|
||||
"apacheconf",
|
||||
"applescript",
|
||||
"arcade",
|
||||
"arduino",
|
||||
"arm",
|
||||
"armasm",
|
||||
"as",
|
||||
"asc",
|
||||
"asciidoc",
|
||||
"aspectj",
|
||||
"ass",
|
||||
"atom",
|
||||
"autohotkey",
|
||||
"autoit",
|
||||
"avrasm",
|
||||
"awk",
|
||||
"axapta",
|
||||
"bash",
|
||||
"basic",
|
||||
"bat",
|
||||
"bbcode",
|
||||
"bf",
|
||||
"bind",
|
||||
"blade",
|
||||
"bnf",
|
||||
"brainfuck",
|
||||
"c",
|
||||
"c++",
|
||||
"cal",
|
||||
"capnp",
|
||||
"capnproto",
|
||||
"cc",
|
||||
"chaos",
|
||||
"chapel",
|
||||
"chpl",
|
||||
"cisco",
|
||||
"clj",
|
||||
"clojure",
|
||||
"cls",
|
||||
"cmake.in",
|
||||
"cmake",
|
||||
"cmd",
|
||||
"coffee",
|
||||
"coffeescript",
|
||||
"console",
|
||||
"coq",
|
||||
"cos",
|
||||
"cpc",
|
||||
"cpp",
|
||||
"cr",
|
||||
"craftcms",
|
||||
"crm",
|
||||
"crmsh",
|
||||
"crystal",
|
||||
"cs",
|
||||
"csharp",
|
||||
"cshtml",
|
||||
"cson",
|
||||
"csp",
|
||||
"css",
|
||||
"csv",
|
||||
"cxx",
|
||||
"cypher",
|
||||
"d",
|
||||
"dart",
|
||||
"delphi",
|
||||
"dfm",
|
||||
"diff",
|
||||
"django",
|
||||
"dns",
|
||||
"docker",
|
||||
"dockerfile",
|
||||
"dos",
|
||||
"dpr",
|
||||
"dsconfig",
|
||||
"dst",
|
||||
"dts",
|
||||
"dust",
|
||||
"dylan",
|
||||
"ebnf",
|
||||
"elixir",
|
||||
"elm",
|
||||
"erl",
|
||||
"erlang",
|
||||
"ex",
|
||||
"extempore",
|
||||
"f90",
|
||||
"f95",
|
||||
"fix",
|
||||
"fortran",
|
||||
"freepascal",
|
||||
"fs",
|
||||
"fsharp",
|
||||
"gams",
|
||||
"gauss",
|
||||
"gawk",
|
||||
"gcode",
|
||||
"gdscript",
|
||||
"gemspec",
|
||||
"gf",
|
||||
"gherkin",
|
||||
"glsl",
|
||||
"gms",
|
||||
"gn",
|
||||
"gni",
|
||||
"go",
|
||||
"godot",
|
||||
"golang",
|
||||
"golo",
|
||||
"gololang",
|
||||
"gradle",
|
||||
"graph",
|
||||
"groovy",
|
||||
"gss",
|
||||
"gyp",
|
||||
"h",
|
||||
"h++",
|
||||
"haml",
|
||||
"handlebars",
|
||||
"haskell",
|
||||
"haxe",
|
||||
"hbs",
|
||||
"hcl",
|
||||
"hh",
|
||||
"hpp",
|
||||
"hs",
|
||||
"html.handlebars",
|
||||
"html.hbs",
|
||||
"html",
|
||||
"http",
|
||||
"https",
|
||||
"hx",
|
||||
"hxx",
|
||||
"hy",
|
||||
"hylang",
|
||||
"i",
|
||||
"i7",
|
||||
"iced",
|
||||
"iecst",
|
||||
"inform7",
|
||||
"ini",
|
||||
"ino",
|
||||
"instances",
|
||||
"iol",
|
||||
"irb",
|
||||
"irpf90",
|
||||
"java",
|
||||
"javascript",
|
||||
"jinja",
|
||||
"jolie",
|
||||
"js",
|
||||
"json",
|
||||
"jsp",
|
||||
"jsx",
|
||||
"julia-repl",
|
||||
"julia",
|
||||
"k",
|
||||
"kaos",
|
||||
"kdb",
|
||||
"kotlin",
|
||||
"kt",
|
||||
"lasso",
|
||||
"lassoscript",
|
||||
"lazarus",
|
||||
"ldif",
|
||||
"leaf",
|
||||
"lean",
|
||||
"less",
|
||||
"lfm",
|
||||
"lisp",
|
||||
"livecodeserver",
|
||||
"livescript",
|
||||
"ln",
|
||||
"lock",
|
||||
"log",
|
||||
"lpr",
|
||||
"ls",
|
||||
"ls",
|
||||
"lua",
|
||||
"mak",
|
||||
"make",
|
||||
"makefile",
|
||||
"markdown",
|
||||
"mathematica",
|
||||
"matlab",
|
||||
"mawk",
|
||||
"maxima",
|
||||
"md",
|
||||
"mel",
|
||||
"mercury",
|
||||
"mirc",
|
||||
"mizar",
|
||||
"mk",
|
||||
"mkd",
|
||||
"mkdown",
|
||||
"ml",
|
||||
"ml",
|
||||
"mm",
|
||||
"mma",
|
||||
"mojolicious",
|
||||
"monkey",
|
||||
"moon",
|
||||
"moonscript",
|
||||
"mrc",
|
||||
"n1ql",
|
||||
"nawk",
|
||||
"nc",
|
||||
"never",
|
||||
"nginx",
|
||||
"nginxconf",
|
||||
"nim",
|
||||
"nimrod",
|
||||
"nix",
|
||||
"nsis",
|
||||
"obj-c",
|
||||
"obj-c++",
|
||||
"objc",
|
||||
"objective-c++",
|
||||
"objectivec",
|
||||
"ocaml",
|
||||
"ocl",
|
||||
"ol",
|
||||
"openscad",
|
||||
"osascript",
|
||||
"oxygene",
|
||||
"p21",
|
||||
"parser3",
|
||||
"pas",
|
||||
"pascal",
|
||||
"patch",
|
||||
"pcmk",
|
||||
"perl",
|
||||
"pf.conf",
|
||||
"pf",
|
||||
"pgsql",
|
||||
"php",
|
||||
"php3",
|
||||
"php4",
|
||||
"php5",
|
||||
"php6",
|
||||
"php7",
|
||||
"pl",
|
||||
"plaintext",
|
||||
"plist",
|
||||
"pm",
|
||||
"podspec",
|
||||
"pony",
|
||||
"postgres",
|
||||
"postgresql",
|
||||
"powershell",
|
||||
"pp",
|
||||
"processing",
|
||||
"profile",
|
||||
"prolog",
|
||||
"properties",
|
||||
"proto",
|
||||
"protobuf",
|
||||
"ps",
|
||||
"ps1",
|
||||
"puppet",
|
||||
"py",
|
||||
"pycon",
|
||||
"python-repl",
|
||||
"python",
|
||||
"qml",
|
||||
"r",
|
||||
"razor-cshtml",
|
||||
"razor",
|
||||
"rb",
|
||||
"re",
|
||||
"reasonml",
|
||||
"rebol",
|
||||
"red-system",
|
||||
"red",
|
||||
"redbol",
|
||||
"rf",
|
||||
"rib",
|
||||
"robot",
|
||||
"rpm-spec",
|
||||
"rpm-specfile",
|
||||
"rpm",
|
||||
"rs",
|
||||
"rsl",
|
||||
"rss",
|
||||
"ruby",
|
||||
"ruleslanguage",
|
||||
"rust",
|
||||
"sas",
|
||||
"SAS",
|
||||
"sc",
|
||||
"scad",
|
||||
"scala",
|
||||
"scheme",
|
||||
"sci",
|
||||
"scilab",
|
||||
"scl",
|
||||
"scss",
|
||||
"sh",
|
||||
"shell",
|
||||
"shexc",
|
||||
"smali",
|
||||
"smalltalk",
|
||||
"sml",
|
||||
"sol",
|
||||
"solidity",
|
||||
"spec",
|
||||
"specfile",
|
||||
"sql",
|
||||
"srt",
|
||||
"ssa",
|
||||
"st",
|
||||
"stan",
|
||||
"stanfuncs",
|
||||
"stata",
|
||||
"step",
|
||||
"stp",
|
||||
"structured-text",
|
||||
"styl",
|
||||
"stylus",
|
||||
"subunit",
|
||||
"supercollider",
|
||||
"svelte",
|
||||
"svg",
|
||||
"swift",
|
||||
"tao",
|
||||
"tap",
|
||||
"tcl",
|
||||
"terraform",
|
||||
"tex",
|
||||
"text",
|
||||
"tf",
|
||||
"thor",
|
||||
"thrift",
|
||||
"tk",
|
||||
"toml",
|
||||
"tp",
|
||||
"ts",
|
||||
"tsql",
|
||||
"tsx",
|
||||
"ttml",
|
||||
"twig",
|
||||
"txt",
|
||||
"typescript",
|
||||
"unicorn-rails-log",
|
||||
"v",
|
||||
"vala",
|
||||
"vb",
|
||||
"vba",
|
||||
"vbnet",
|
||||
"vbs",
|
||||
"vbscript",
|
||||
"verilog",
|
||||
"vhdl",
|
||||
"vim",
|
||||
"vtt",
|
||||
"wl",
|
||||
"x++",
|
||||
"x86asm",
|
||||
"xhtml",
|
||||
"xjb",
|
||||
"xl",
|
||||
"xml",
|
||||
"xpath",
|
||||
"xq",
|
||||
"xquery",
|
||||
"xsd",
|
||||
"xsl",
|
||||
"xtlang",
|
||||
"xtm",
|
||||
"yaml",
|
||||
"yml",
|
||||
"zep",
|
||||
"zephir",
|
||||
"zone",
|
||||
"zsh"
|
||||
])
|
||||
|
||||
module.exports.getPermissions = getPermissions
|
||||
module.exports.getDefaultPermissions = getDefaultPermissions
|
||||
module.exports.hasPermission = hasPermission
|
||||
|
|
@ -194,3 +582,4 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
|
|||
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
|
||||
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
|
||||
module.exports.filterTo = filterTo
|
||||
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const retrigger = sync.require("../../d2m/actions/retrigger")
|
|||
*/
|
||||
async function addReaction(event) {
|
||||
// Wait until the corresponding channel and message have already been bridged
|
||||
if (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
|
||||
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)
|
||||
|
||||
retrigger.finishedBridging(event.event_id)
|
||||
}
|
||||
|
||||
module.exports.addReaction = addReaction
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ const utils = sync.require("../../matrix/utils")
|
|||
/** @type {import("../../d2m/actions/retrigger")} */
|
||||
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||
|
||||
/** @type {{messageID: string, emojiIdOrName: string}[]} */
|
||||
const m2dDeletedReactions = []
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
|
|
@ -24,6 +27,21 @@ async function deleteMessage(event) {
|
|||
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
async function removeMessageEvent(event) {
|
||||
// Could be for removing a message or suppressing embeds. For more information, the message needs to be bridged first.
|
||||
if (!await retrigger.waitForEvent(event.redacts)) return
|
||||
|
||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||
await suppressEmbeds(event)
|
||||
} else {
|
||||
await deleteMessage(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
|
|
@ -41,11 +59,20 @@ async function suppressEmbeds(event) {
|
|||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||
*/
|
||||
async function removeReaction(event) {
|
||||
if (!await retrigger.waitForReactionEvent(event.redacts)) return
|
||||
|
||||
const hash = utils.getEventIDHash(event.redacts)
|
||||
const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
.select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
|
||||
if (!row) return
|
||||
await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji)
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
*/
|
||||
async function handle(event) {
|
||||
// If this is for removing a reaction, try it
|
||||
await removeReaction(event)
|
||||
|
||||
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
||||
if (retrigger.eventNotFoundThenRetrigger(event.redacts, () => as.emit("type:m.room.redaction", event))) return
|
||||
|
||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||
await suppressEmbeds(event)
|
||||
} else {
|
||||
await deleteMessage(event)
|
||||
}
|
||||
// Don't know if it's a redaction for a reaction or an event, try both at the same time (otherwise waitFor will block)
|
||||
await Promise.all([
|
||||
removeMessageEvent(event),
|
||||
removeReaction(event)
|
||||
])
|
||||
}
|
||||
|
||||
module.exports.handle = handle
|
||||
module.exports.m2dDeletedReactions = m2dDeletedReactions
|
||||
|
|
@ -29,6 +29,8 @@ const pollComponents = sync.require("./poll-components")
|
|||
const setupEmojis = sync.require("../actions/setup-emojis")
|
||||
/** @type {import("../../d2m/converters/user-to-mxid")} */
|
||||
const userToMxid = sync.require("../../d2m/converters/user-to-mxid")
|
||||
/** @type {import("../../web/routes/letter-avatar")} */
|
||||
const letterAvatar = sync.require("../../web/routes/letter-avatar")
|
||||
|
||||
/** @type {[RegExp, string][]} */
|
||||
const markdownEscapes = [
|
||||
|
|
@ -582,6 +584,13 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
displayNameRunoff = ""
|
||||
}
|
||||
|
||||
// Avatar post-processing. Use a thumbnail for media, or generate letter avatar if none present.
|
||||
if (avatarURL) {
|
||||
avatarURL = avatarURL + "?preset=avatar"
|
||||
} else {
|
||||
avatarURL = letterAvatar.getLetterAvatarURL(event.sender, displayNameShortened)
|
||||
}
|
||||
|
||||
let content = event.content["body"] || "" // ultimate fallback
|
||||
/** @type {{id: string, filename: string}[]} */
|
||||
const attachments = []
|
||||
|
|
@ -821,7 +830,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
|
||||
// Handling mentions of Discord users
|
||||
input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")>/g, (whole, attributeValue, mxid) => {
|
||||
input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")/g, (whole, attributeValue, mxid) => {
|
||||
mxid = decodeURIComponent(mxid)
|
||||
if (mxUtils.eventSenderIsFromDiscord(mxid)) {
|
||||
// Handle mention of an OOYE sim user by their mxid
|
||||
|
|
@ -892,9 +901,12 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
// Check for incompatible backticks in code blocks
|
||||
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") {
|
||||
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}`
|
||||
// Build the replacement <code> node
|
||||
const replacementCode = doc.createElement("code")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -94,6 +94,11 @@ function printError(type, source, e, payload) {
|
|||
console.dir(payload, {depth: null})
|
||||
}
|
||||
|
||||
/** @param {string} stack */
|
||||
function cleanErrorStack(stack) {
|
||||
return stack.replace(/(\/webhooks\/[0-9]+\/)[a-zA-Z0-9_-]+/g, "$1(redacted)")
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
* @param {"Discord" | "Matrix"} source
|
||||
|
|
@ -134,7 +139,7 @@ async function sendError(roomID, source, type, e, payload) {
|
|||
builder.addLine(errorIntroLine)
|
||||
|
||||
// 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>`)
|
||||
|
||||
// How
|
||||
|
|
@ -143,7 +148,7 @@ async function sendError(roomID, source, type, e, payload) {
|
|||
|
||||
// Send
|
||||
try {
|
||||
await api.sendEvent(roomID, "m.room.message", {
|
||||
const errorEventID = await api.sendEvent(roomID, "m.room.message", {
|
||||
...builder.get(),
|
||||
"moe.cadence.ooye.error": {
|
||||
source: source.toLowerCase(),
|
||||
|
|
@ -153,6 +158,14 @@ async function sendError(roomID, source, type, e, payload) {
|
|||
user_ids: ["@cadence:cadence.moe"]
|
||||
}
|
||||
})
|
||||
// Add reaction indicating that errors may be retried
|
||||
await api.sendEvent(roomID, "m.reaction", {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.annotation",
|
||||
event_id: errorEventID,
|
||||
key: "🔁"
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +185,7 @@ const errorRetrySema = new Semaphore()
|
|||
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} reactionEvent
|
||||
*/
|
||||
async function onRetryReactionAdd(reactionEvent) {
|
||||
if (reactionEvent.sender === `@${reg.sender_localpart}:${reg.ooye.server_name}`) return // Don't respond to the bot's own indicative reaction
|
||||
const roomID = reactionEvent.room_id
|
||||
await errorRetrySema.request(async () => {
|
||||
const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
|
||||
|
|
@ -211,7 +225,7 @@ async event => {
|
|||
// @ts-ignore
|
||||
await matrixCommandHandler.execute(event)
|
||||
}
|
||||
retrigger.messageFinishedBridging(event.event_id)
|
||||
retrigger.finishedBridging(event.event_id)
|
||||
await api.ackEvent(event)
|
||||
}))
|
||||
|
||||
|
|
@ -222,7 +236,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
|||
async event => {
|
||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||
const messageResponses = await sendEvent.sendEvent(event)
|
||||
retrigger.messageFinishedBridging(event.event_id)
|
||||
retrigger.finishedBridging(event.event_id)
|
||||
await api.ackEvent(event)
|
||||
}))
|
||||
|
||||
|
|
@ -502,5 +516,6 @@ async event => {
|
|||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
module.exports.cleanErrorStack = cleanErrorStack
|
||||
module.exports.sendError = sendError
|
||||
module.exports.printError = printError
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const {test} = require("supertape")
|
||||
const {stringifyErrorStack} = require("./event-dispatcher")
|
||||
const {stringifyErrorStack, cleanErrorStack} = require("./event-dispatcher")
|
||||
|
||||
test("stringify error stack: works", t => {
|
||||
function a() {
|
||||
|
|
@ -21,3 +21,30 @@ test("stringify error stack: works", t => {
|
|||
t.match(str, /^ \[prop\]: 2.1$/m)
|
||||
}
|
||||
})
|
||||
|
||||
test("clean error stack: removes webhook token", t => {
|
||||
t.notMatch(
|
||||
cleanErrorStack(`
|
||||
DiscordAPIError: Service resource is being rate limited.
|
||||
at fn (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:591:13)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at exports.RequestHandler.request (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:546:17)
|
||||
at WebhookMethods.executeWebhook (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/methods/Webhook.ts:249:35)
|
||||
at /var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:65:31
|
||||
at withWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:47:9)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
|
||||
at async Object.sendMessageWithWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:64:17)
|
||||
at async Object.sendEvent (/var/home/cadence/out-of-your-element/src/m2d/actions/send-event.js:132:27)
|
||||
at async /var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:208:27
|
||||
at async AppService.<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/
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -463,17 +463,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 {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>}>}
|
||||
*/
|
||||
async function getMedia(mxc, init = {}) {
|
||||
init = {...init}
|
||||
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||
assert(mediaParts)
|
||||
const downloadOrThumbnail = init.height ? "thumbnail" : "download"
|
||||
let url = `${mreq.baseUrl}/client/v1/media/${downloadOrThumbnail}/${mediaParts[1]}/${mediaParts[2]}`
|
||||
if (init.height) url += "?" + new URLSearchParams({height: String(init.height), width: String(init.height)})
|
||||
|
||||
let route = "download"
|
||||
let query = ""
|
||||
|
||||
if (init.thumbnail) {
|
||||
route = "thumbnail"
|
||||
query = "?" + new URLSearchParams(Object.keys(init.thumbnail).map(k => [k, String(init.thumbnail?.[k])]))
|
||||
}
|
||||
|
||||
let url = `${mreq.baseUrl}/client/v1/media/${route}/${mediaParts[1]}/${mediaParts[2]}${query}`
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${reg.as_token}`
|
||||
|
|
|
|||
|
|
@ -78,10 +78,14 @@ function readRegistration() {
|
|||
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
|
||||
let reg = readRegistration()
|
||||
|
||||
fs.watch(registrationFilePath, {persistent: false}, () => {
|
||||
let newReg = readRegistration()
|
||||
Object.assign(reg, newReg)
|
||||
})
|
||||
if (reg) {
|
||||
fs.watch(registrationFilePath, {persistent: false}, () => {
|
||||
let newReg = readRegistration()
|
||||
if (newReg) {
|
||||
Object.assign(reg, newReg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.registrationFilePath = registrationFilePath
|
||||
module.exports.readRegistration = readRegistration
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ const mreq = sync.require("./matrix/mreq")
|
|||
const api = sync.require("./matrix/api")
|
||||
const file = sync.require("./matrix/file")
|
||||
const sendEvent = sync.require("./m2d/actions/send-event")
|
||||
const redact = sync.require("./m2d/actions/redact")
|
||||
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
||||
const updatePins = sync.require("./d2m/actions/update-pins")
|
||||
const speedbump = sync.require("./d2m/actions/speedbump")
|
||||
const ks = sync.require("./matrix/kstate")
|
||||
const setPresence = sync.require("./d2m/actions/set-presence")
|
||||
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
||||
const dUtils = sync.require("./discord/utils")
|
||||
const mxUtils = sync.require("./matrix/utils")
|
||||
const guildID = "112760669178241024"
|
||||
|
||||
async function ping() {
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block body
|
||||
h1.ta-center.fs-display2.fc-green-400 April Fools!
|
||||
.ws7.m-auto
|
||||
.s-prose.fs-body2
|
||||
p Sheesh, wouldn't that be horrible?
|
||||
if guild_id
|
||||
p Fake AI messages have now been #[strong.fc-green-600 deactivated for everyone in your server.]
|
||||
p Hope the prank entertained you. #[a(href="https://cadence.moe/contact") Send love or hate mail here.]
|
||||
|
||||
h2 What actually happened?
|
||||
ul
|
||||
li A secret event was added for the duration of 1st April 2026 (UTC).
|
||||
li If a message matches a preset pattern, a preset response is posted to chat by an AI-ified profile of the previous author.
|
||||
li It only happens at most once per hour in each server.
|
||||
li I tried to design it to not interrupt any serious/sensitive topics. I am deeply sorry if that didn't work out.
|
||||
li No AI generated materials have ever been used in Out Of Your Element: no code, no prose, no images, no jokes.
|
||||
li It'll always deactivate itself on 2nd April, no matter what, and I'll remove the relevant code shortly after.
|
||||
if guild_id
|
||||
.s-prose.fl-grow1.mt16
|
||||
p If you thought it was funny, feel free to opt back in. This affects the entire server, so please be courteous.
|
||||
form(method="post" action=rel(`/agi/optin?guild_id=${guild_id}`))
|
||||
button(type="submit").s-btn.s-btn__muted Opt back in
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
extends includes/template.pug
|
||||
|
||||
block title
|
||||
title AGI in Discord
|
||||
|
||||
block body
|
||||
style.
|
||||
.ai-gradient {
|
||||
background: linear-gradient(100deg, #fb72f2, #072ea4);
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
h1.ta-center.fs-display2.ai-gradient AGI in Discord:#[br]Revolutionizing the Future of Communications
|
||||
.ws7.m-auto
|
||||
.s-prose.fs-body2
|
||||
p In the ever-changing and turbulent world of AI, it's crucial to always be one step ahead.
|
||||
p That's why Out Of Your Element has partnered with #[strong Grimace AI] to provide you tomorrow's technology, today.
|
||||
ul
|
||||
li #[strong Always online:] Miss your friends when they log off? Now you can talk to facsimiles of them etched into an unfeeling machine, always and forever!
|
||||
li #[strong Smarter than ever:] Pissed off when somebody says something #[em wrong] on the internet? Frustrated with having to stay up all night correcting them? With Grimace Truth (available in Pro+ Ultra plan), all information is certified true to reality, so you'll never have to worry about those frantic Google searches at 3 AM.
|
||||
li #[strong Knows you better than yourself:] We aren't just training on your data; we're copying minds into our personality matrix — including yours. Do you find yourself enjoying the sound of your own voice more than anything else? Our unique simulation of You is here to help.
|
||||
|
||||
h1.mt64.mb32 Frequently Asked Questions
|
||||
.s-link-preview
|
||||
.s-link-preview--header.fd-column
|
||||
.s-link-preview--title.fs-title.pl4 How to opt out?
|
||||
.s-link-preview--details.fc-red-500
|
||||
!= icons.Icons.IconFire
|
||||
= ` 20,000% higher search volume for this question in the last hour`
|
||||
.s-link-preview--body
|
||||
.s-prose
|
||||
h2.fs-body3 Is this really goodbye? 😢😢😢😢😢
|
||||
p I can't convince you to stay?
|
||||
p As not just a customer service representative but someone with a shared vision, I simply want you to know that everyone at Grimace AI will miss all the time that we've shared together with you.
|
||||
form(method="post" action=(guild_id ? rel(`/agi/optout?guild_id=${guild_id}`) : rel("/agi/optout"))).d-flex.g4.mt16
|
||||
button(type="button").s-btn.s-btn__filled Nevermind, I'll stay :)
|
||||
button(type="submit").s-btn.s-btn__danger.s-btn__outlined Opt out for 3 days
|
||||
|
||||
|
||||
div(style="height: 200px")
|
||||
|
|
@ -65,8 +65,7 @@ mixin define-themed-button(name, theme)
|
|||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
block title
|
||||
title Out Of Your Element
|
||||
title Out Of Your Element
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css"))
|
||||
//- Please use responsibly!!!!!
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, getValidatedQuery, sendRedirect} = require("h3")
|
||||
const {as, from, sync, db} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
const schema = {
|
||||
opt: z.object({
|
||||
guild_id: z.string().regex(/^[0-9]+$/)
|
||||
})
|
||||
}
|
||||
|
||||
as.router.get("/agi", defineEventHandler(async event => {
|
||||
return pugSync.render(event, "agi.pug", {})
|
||||
}))
|
||||
|
||||
as.router.get("/agi/optout", defineEventHandler(async event => {
|
||||
return pugSync.render(event, "agi-optout.pug", {})
|
||||
}))
|
||||
|
||||
as.router.post("/agi/optout", defineEventHandler(async event => {
|
||||
const parseResult = await getValidatedQuery(event, schema.opt.safeParse)
|
||||
if (parseResult.success) {
|
||||
db.prepare("INSERT OR IGNORE INTO agi_optout (guild_id) VALUES (?)").run(parseResult.data.guild_id)
|
||||
}
|
||||
return sendRedirect(event, "", 302)
|
||||
}))
|
||||
|
||||
as.router.post("/agi/optin", defineEventHandler(async event => {
|
||||
const {guild_id} = await getValidatedQuery(event, schema.opt.parse)
|
||||
db.prepare("DELETE FROM agi_optout WHERE guild_id = ?").run(guild_id)
|
||||
return sendRedirect(event, `../agi?guild_id=${guild_id}`, 302)
|
||||
}))
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
const assert = require("assert/strict")
|
||||
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3")
|
||||
const {z} = require("zod")
|
||||
const {ReadableStream} = require("stream/web")
|
||||
const {Readable} = require("stream")
|
||||
const sharp = require("sharp")
|
||||
|
||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||
let hasher = null
|
||||
|
|
@ -19,11 +22,27 @@ const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet")
|
|||
/** @type {import("../../m2d/actions/sticker")} */
|
||||
const sticker = sync.require("../../m2d/actions/sticker")
|
||||
|
||||
// Resizing client-side because server-side is too slow, at least with Synapse. Really need it to be fast because webhook avatars show a placeholder in the interim.
|
||||
/** @type {{[presetKey: string]: (body: ReadableStream) => ReadableStream}} */
|
||||
const MEDIA_THUMBNAIL_PRESETS = {
|
||||
avatar: body =>
|
||||
Readable.toWeb(
|
||||
Readable.fromWeb(body).pipe(
|
||||
sharp()
|
||||
.resize({height: 210, width: 210, fit: "cover"}) // the largest display of the webhook pfp on Discord Android in screen pixels
|
||||
.jpeg({force: false, quality: 90}) // File size works out to up to ~110k for a PNG, less for a JPEG
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const schema = {
|
||||
params: z.object({
|
||||
media: z.object({
|
||||
server_name: z.string(),
|
||||
media_id: z.string()
|
||||
}),
|
||||
mediaQuery: z.object({
|
||||
preset: z.enum(Object.keys(MEDIA_THUMBNAIL_PRESETS)) // list of possible thumbnail presets
|
||||
}),
|
||||
sheet: z.object({
|
||||
e: z.array(z.string()).or(z.string())
|
||||
}),
|
||||
|
|
@ -65,7 +84,8 @@ function verifyMediaHash(serverAndMediaID) {
|
|||
}
|
||||
|
||||
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}`)
|
||||
const api = getAPI(event)
|
||||
|
|
@ -77,7 +97,12 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
|
|||
setResponseStatus(event, res.status)
|
||||
setResponseHeader(event, "Content-Type", contentType)
|
||||
setResponseHeader(event, "Transfer-Encoding", "chunked")
|
||||
return res.body
|
||||
|
||||
if (res.ok && query.success) {
|
||||
return MEDIA_THUMBNAIL_PRESETS[query.data.preset](res.body)
|
||||
} else {
|
||||
return res.body
|
||||
}
|
||||
}))
|
||||
|
||||
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()
|
||||
}))
|
||||
|
|
@ -125,13 +125,14 @@ as.router.get("/icon.png", defineEventHandler(async event => {
|
|||
|
||||
pugSync.createRoute(as.router, "/ok", "ok.pug")
|
||||
|
||||
sync.require("./routes/agi")
|
||||
sync.require("./routes/download-matrix")
|
||||
sync.require("./routes/download-discord")
|
||||
sync.require("./routes/guild-settings")
|
||||
sync.require("./routes/guild")
|
||||
sync.require("./routes/info")
|
||||
sync.require("./routes/letter-avatar")
|
||||
sync.require("./routes/link")
|
||||
sync.require("./routes/log-in-with-matrix")
|
||||
sync.require("./routes/oauth")
|
||||
sync.require("./routes/password")
|
||||
sync.require("./routes/stats")
|
||||
|
|
|
|||
74
test/data.js
74
test/data.js
|
|
@ -2035,6 +2035,80 @@ module.exports = {
|
|||
tts: false
|
||||
}
|
||||
},
|
||||
reply_to_member_join: {
|
||||
type: 19,
|
||||
content: "when the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-30T12:11:04.443000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1488148556962332692",
|
||||
channel_id: "475599038536744962",
|
||||
author: {
|
||||
id: "576945009408999426",
|
||||
username: "randomllama121",
|
||||
avatar: "08510a70f957106dad1580323c40cd7a",
|
||||
discriminator: "0",
|
||||
public_flags: 128,
|
||||
flags: 128,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "random :3",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 0,
|
||||
channel_id: "475599038536744962",
|
||||
message_id: "1488146734352826478",
|
||||
guild_id: "475599038536744960"
|
||||
},
|
||||
referenced_message: {
|
||||
type: 7,
|
||||
content: "",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-30T12:03:49.899000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1488146734352826478",
|
||||
channel_id: "475599038536744962",
|
||||
author: {
|
||||
id: "1461677775554478161",
|
||||
username: "peasant321_76775",
|
||||
avatar: null,
|
||||
discriminator: "0",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "PEASANT!!",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false
|
||||
}
|
||||
},
|
||||
attachment_no_content: {
|
||||
id: "1124628646670389348",
|
||||
type: 0,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,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', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
|
||||
('!qzDBLKlildpzrrOnFZ:cadence.moe', '@lavender.pet:queer.sh', 'lavender.pet', NULL, 0),
|
||||
('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0),
|
||||
('!zq94fae5bVKUubZLp7:agiadn.org', '@underscore_x:agiadn.org', 'underscore_x', NULL, 100);
|
||||
|
||||
|
|
|
|||
|
|
@ -175,5 +175,4 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
|||
require("../src/web/routes/log-in-with-matrix.test")
|
||||
require("../src/web/routes/oauth.test")
|
||||
require("../src/web/routes/password.test")
|
||||
require("../src/agi/generator.test")
|
||||
})()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue