From 74c0c28cf4c1683e38a2dfa32b0b93f1fe7ab3b9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Mar 2026 22:34:04 +1300 Subject: [PATCH 01/45] Update dependencies --- package-lock.json | 252 ++++++++++++++++++---------------------------- package.json | 6 +- 2 files changed, 99 insertions(+), 159 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9847400..4e6ecd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.15.2", + "cloudstorm": "^0.17.0", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -36,7 +36,7 @@ "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.17.1", + "snowtransfer": "^0.17.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", @@ -46,7 +46,7 @@ "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^22.17.1", - "c8": "^10.1.2", + "c8": "^11.0.0", "cross-env": "^7.0.3", "supertape": "^12.0.12" }, @@ -137,9 +137,9 @@ } }, "node_modules/@chriscdn/promise-semaphore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.1.2.tgz", - "integrity": "sha512-rELbH6FSr9wr5J249Ax8dpzQdTaqEgcW+lilDKZxB13Hz0Bz3Iyx4q/7qZxPMnra9FUW4ZOkVf+bx5tbi6Goog==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.1.3.tgz", + "integrity": "sha512-EAmwIbH1L2CNsJWloXBG4Kv89H7IUsjYFQnGnmus3OX70LcD5Uu5A7sohPx3O0Ks9UQWEgcr5n2IfxBSuYvOeg==", "license": "MIT" }, "node_modules/@cloudcmd/stub": { @@ -178,9 +178,9 @@ } }, "node_modules/@cloudrac3r/in-your-element": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cloudrac3r/in-your-element/-/in-your-element-1.1.1.tgz", - "integrity": "sha512-AKp9vnSDA9wzJl4O3C/LA8jgI5m1r0M3MRBQGHcVVL22SrrZMdcy+kWjlZWK343KVLOkuTAISA2D+Jb/zyZS6A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cloudrac3r/in-your-element/-/in-your-element-1.1.2.tgz", + "integrity": "sha512-adFZel24sGHpTI1vgJdBN5twcdu6QmPFlO8qAJt49KO6N8mwDcbUC2GPqH5pGerXNv1Lpq0eXsNLm+ytKrOTaQ==", "license": "AGPL-3.0-or-later", "dependencies": { "h3": "^1.12.0", @@ -765,21 +765,12 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1048,9 +1039,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "dev": true, "license": "MIT", "dependencies": { @@ -1132,10 +1123,14 @@ "license": "MIT" }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -1225,20 +1220,24 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/c8": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", - "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", + "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", "dev": true, + "license": "ISC", "dependencies": { "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", @@ -1247,7 +1246,7 @@ "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "test-exclude": "^7.0.1", + "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" @@ -1256,7 +1255,7 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=18" + "node": "20 || >=22" }, "peerDependencies": { "monocart-coverage-reports": "^2" @@ -1370,13 +1369,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.15.2.tgz", - "integrity": "sha512-5y7E0uI39R3d7c+AWksqAQAlZlpx+qNjxjQfNIem2hh68s6QRmOFHTKu34I7pBE6JonpZf8AmoMYArY/4lLVmg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.0.tgz", + "integrity": "sha512-zsd9y5ljNnbxdvDid9TgWePDqo7il4so5spzx6NDwZ67qWQjR96UUhLxJ+BAOdBBSPF9UXFM61dAzC2g918q+A==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.37", - "snowtransfer": "^0.17.0" + "discord-api-types": "^0.38.40", + "snowtransfer": "^0.17.5" }, "engines": { "node": ">=22.0.0" @@ -1517,9 +1516,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.38", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz", - "integrity": "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q==", + "version": "0.38.41", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.41.tgz", + "integrity": "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1731,66 +1730,18 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", - "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "jackspeak": "^4.2.3" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1973,22 +1924,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-diff": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", @@ -2023,6 +1958,13 @@ "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==", "dev": true }, + "node_modules/just-snake-case": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/just-snake-case/-/just-snake-case-3.2.0.tgz", + "integrity": "sha512-iugHP9bSE0jOq3BzN0W0rdu/OOkFirPe8FtUw6v9y37UlbUDgJ1x4xiGNfUhI6mV9dc/paaifyiyn+F+mrm8gw==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2039,9 +1981,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2094,16 +2036,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2118,10 +2060,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2199,12 +2142,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2230,9 +2167,9 @@ "dev": true }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2240,7 +2177,7 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2648,12 +2585,12 @@ } }, "node_modules/snowtransfer": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.1.tgz", - "integrity": "sha512-WSXj055EJhzzfD7B3oHVyRTxkqFCaxcVhwKY6B3NkBSHRyM6wHxZLq6VbFYhopUg+lMtd7S1ZO8JM+Ut+js2iA==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.5.tgz", + "integrity": "sha512-nVI1UJNFoX1ndGFZxB3zb3X5SWtD9hIAcw7wCgVKWvCf42Wg2B4UFIrZWI83HxaSBY0CGbPZmZzZb3RSt/v2wQ==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.37" + "discord-api-types": "^0.38.40" }, "engines": { "node": ">=22.0.0" @@ -2780,9 +2717,9 @@ } }, "node_modules/supertape": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.0.12.tgz", - "integrity": "sha512-ugmCQsB7s22fCTJKiMb6+Fd8kP7Hsvlo6/aly0qLGgOepu1PVBydhrBPMWaoY3wf+VqLtMkkvwGxUTCFde5z/g==", + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.7.0.tgz", + "integrity": "sha512-5PXh6HsfEJKkC0SMhPNkH35o8Okj8xlVvoju9R0aCohzsK+GEufeYZ1IPhRBRQ2DBLXdMZHVF6N/4pAefxNuAA==", "dev": true, "license": "MIT", "dependencies": { @@ -2800,9 +2737,10 @@ "cli-progress": "^3.8.2", "flatted": "^3.3.1", "fullstore": "^4.0.0", - "glob": "^11.0.1", + "glob": "^13.0.0", "jest-diff": "^30.0.3", "json-with-bigint": "^3.4.4", + "just-snake-case": "^3.2.0", "once": "^1.4.0", "resolve": "^1.17.0", "stacktracey": "^2.1.7", @@ -2819,9 +2757,9 @@ } }, "node_modules/supertape/node_modules/try-to-catch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.3.tgz", - "integrity": "sha512-mUz1zpe6nkRQW0XZ/Ojfe/Eg7e5h3s+r+h7ONfP3Oo27/Jm8mkNDAnLzZ/A3sEMApROolzuJGBiQhGmmVDAFLw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.5.tgz", + "integrity": "sha512-VKBslDQsy4pGj2TMNgDdskWb7AWSi/9dPEmcNv3sdL0+aRMQTPJo6aEqlcuN0vbOwFfsE1oAXmx3bFdf6vrJFg==", "dev": true, "license": "MIT", "engines": { @@ -2903,17 +2841,18 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" + "glob": "^13.0.6", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=18" + "node": "20 || >=22" } }, "node_modules/through2": { @@ -2979,6 +2918,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3185,9 +3125,9 @@ } }, "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index afbb90a..71639b0 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.15.2", + "cloudstorm": "^0.17.0", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -45,7 +45,7 @@ "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.17.1", + "snowtransfer": "^0.17.5", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", @@ -58,7 +58,7 @@ "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.3", "@types/node": "^22.17.1", - "c8": "^10.1.2", + "c8": "^11.0.0", "cross-env": "^7.0.3", "supertape": "^12.0.12" }, From 9eaa85c072fefd1978247cbfe4dcc4d71af8fe97 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Mar 2026 22:34:51 +1300 Subject: [PATCH 02/45] Add /invite Matrix command to get Discord invite --- src/matrix/matrix-command-handler.js | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index e382a32..d568f7b 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") const Ty = require("../types") const {pipeline} = require("stream").promises const sharp = require("sharp") @@ -262,6 +263,46 @@ const commands = [{ await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")}) } ) +}, { + aliases: ["invite"], + execute: replyctx( + async (event, realBody, words, ctx) => { + // Guard + /** @type {string} */ // @ts-ignore + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + const guildID = discord.channels.get(channelID)?.["guild_id"] + if (!guildID) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This room isn't bridged to the other side." + }) + } + + const guild = discord.guilds.get(guildID) + assert(guild) + const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command creates an invite link to the Discord side. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Invite permission." + }) + } + + const invite = await discord.snow.channel.createChannelInvite(channelID) + const validHours = Math.ceil(invite.max_age / (60 * 60)) + const validUses = + ( invite.max_uses === 0 ? "unlimited uses" + : invite.max_uses === 1 ? "single-use" + : `${invite.max_uses} uses`) + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: `https://discord.gg/${invite.code}\nValid for next ${validHours} hours, ${validUses}.` + }) + } + ) }] From 6a2606cbdb41f8ee5f7aa8c8da2d0fa90e93bf60 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Mar 2026 22:35:10 +1300 Subject: [PATCH 03/45] Add UI for defining default roles --- src/db/migrations/0035-role-default.sql | 9 ++++ src/db/orm-defs.d.ts | 5 +++ src/web/pug-sync.js | 1 + src/web/pug/fragments/default-roles-list.pug | 5 +++ src/web/pug/guild.pug | 21 ++++++++- src/web/pug/includes/default-roles-list.pug | 19 +++++++++ src/web/pug/includes/template.pug | 25 +++++++++-- src/web/routes/guild-settings.js | 45 +++++++++++++++++++- 8 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 src/db/migrations/0035-role-default.sql create mode 100644 src/web/pug/fragments/default-roles-list.pug create mode 100644 src/web/pug/includes/default-roles-list.pug diff --git a/src/db/migrations/0035-role-default.sql b/src/db/migrations/0035-role-default.sql new file mode 100644 index 0000000..6c44e7e --- /dev/null +++ b/src/db/migrations/0035-role-default.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +CREATE TABLE "role_default" ( + "guild_id" TEXT NOT NULL, + "role_id" TEXT NOT NULL, + PRIMARY KEY ("guild_id", "role_id") +); + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index 79f02ad..f6628f2 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -104,6 +104,11 @@ export type Models = { historical_room_index: number } + role_default: { + guild_id: string + role_id: string + } + room_upgrade_pending: { new_room_id: string old_room_id: string diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js index f87550d..e61c53b 100644 --- a/src/web/pug-sync.js +++ b/src/web/pug-sync.js @@ -77,6 +77,7 @@ function renderPath(event, path, locals) { compile() fs.watch(path, {persistent: false}, compile) fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile) + fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile) } const cb = pugCache.get(path) diff --git a/src/web/pug/fragments/default-roles-list.pug b/src/web/pug/fragments/default-roles-list.pug new file mode 100644 index 0000000..3b36549 --- /dev/null +++ b/src/web/pug/fragments/default-roles-list.pug @@ -0,0 +1,5 @@ +//- locals: guild, guild_id + +include ../includes/default-roles-list.pug ++default-roles-list(guild, guild_id) ++add-roles-menu(guild, guild_id) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index a9e770b..74b476a 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -1,4 +1,5 @@ extends includes/template.pug +include includes/default-roles-list.pug mixin badge-readonly .s-badge.s-badge__xs.s-badge__icon.s-badge__muted @@ -76,7 +77,7 @@ block body if space_id h2.mt48.fs-headline1 Server settings - h3.mt32.fs-category Privacy level + h3.mt32.fs-category How Matrix users join span#privacy-level-loading .s-card form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input") @@ -105,6 +106,24 @@ block body p.s-description.m0 Shareable invite links, like Discord p.s-description.m0 Publicly listed in directory, like Discord server discovery + h3.mt32.fs-category Default roles + .s-card + form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles + input(type="hidden" name="guild_id" value=guild_id) + .d-flex.fw-wrap.g4 + .s-tag.s-tag__md.fs-body1.s-tag__required @everyone + + +default-roles-list(guild, guild_id) + + button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0 + .s-tag--dismiss.m1 + != icons.Icons.IconPlusSm + + #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible + .s-popover--arrow.s-popover--arrow__tc + +add-roles-menu(guild, guild_id) + p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate. + h3.mt32.fs-category Features .s-card.d-grid.px0.g16 form.d-flex.ai-center.g16 diff --git a/src/web/pug/includes/default-roles-list.pug b/src/web/pug/includes/default-roles-list.pug new file mode 100644 index 0000000..8c0a4e0 --- /dev/null +++ b/src/web/pug/includes/default-roles-list.pug @@ -0,0 +1,19 @@ +mixin default-roles-list(guild, guild_id) + #default-roles-list(style="display: contents") + each roleID in select("role_default", "role_id", {guild_id}).pluck().all() + - let r = guild.roles.find(r => r.id === roleID) + if r + .s-tag.s-tag__md.fs-body1= r.name + span(id=`role-loading-${roleID}`) + button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss + != icons.Icons.IconClearSm + +mixin add-roles-menu(guild, guild_id) + ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu + li.s-menu--title.d-flex(role="separator") Select role + span#add-role-loading + each r in guild.roles.sort((a, b) => b.position - a.position) + if r.id !== guild_id && !r.managed + - let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get() + li(role="menuitem") + button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 9fe80aa..278a16a 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -91,6 +91,19 @@ html(lang="en") .s-btn__dropdown:has(+ :popover-open) { background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important; } + .s-btn__dropdown.s-tag:has(+ :popover-open) .s-tag--dismiss { + background-color: var(--black-500) !important; + color: var(--black-150) !important; + } + .s-tag .is-loading { + margin-right: -4px; + } + .s-tag .is-loading + .s-tag--dismiss { + display: none !important; + } + a.s-block-link, .s-block-link { + --_bl-bs-color: var(--green-400); + } @media (prefers-color-scheme: dark) { body.theme-system .s-popover { --_po-bg: var(--black-100); @@ -141,11 +154,15 @@ html(lang="en") //- Guild list popover script. document.querySelectorAll("[popovertarget]").forEach(e => { - e.addEventListener("click", () => { - const rect = e.getBoundingClientRect() - const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }` + const target = document.getElementById(e.getAttribute("popovertarget")) + e.addEventListener("click", calculate) + target.addEventListener("toggle", calculate) + function calculate() { + const buttonRect = e.getBoundingClientRect() + const targetRect = target.getBoundingClientRect() + const t = `:popover-open { position: absolute; top: ${Math.floor(buttonRect.bottom + window.scrollY)}px; left: ${Math.floor(Math.max(targetRect.width / 2, buttonRect.left + buttonRect.width / 2))}px; width: ${Math.floor(buttonRect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }` document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length) - }) + } }) //- Prevent default script. diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 63dd3ec..62a28a1 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -4,10 +4,12 @@ const assert = require("assert/strict") const {z} = require("zod") const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3") -const {as, db, sync, select} = require("../../passthrough") +const {as, db, sync, select, discord} = require("../../passthrough") /** @type {import("../auth")} */ const auth = sync.require("../auth") +/** @type {import("../pug-sync")} */ +const pugSync = sync.require("../pug-sync") /** @type {import("../../d2m/actions/set-presence")} */ const setPresence = sync.require("../../d2m/actions/set-presence") @@ -20,6 +22,14 @@ function getCreateSpace(event) { return event.context.createSpace || sync.require("../../d2m/actions/create-space") } +const schema = { + defaultRoles: z.object({ + guild_id: z.string(), + toggle_role: z.string().optional(), + remove_role: z.string().optional() + }) +} + /** * @typedef Options * @prop {(value: string?) => number} transform @@ -94,3 +104,36 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", { await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request } })) + +as.router.post("/api/default-roles", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse) + + const managed = await auth.getManagedGuilds(event) + const guildID = parsedBody.guild_id + if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) + + const roleID = parsedBody.toggle_role || parsedBody.remove_role + assert(roleID) + assert.notEqual(guildID, roleID) // the @everyone role is always default + + const guild = discord.guilds.get(guildID) + assert(guild) + + let shouldRemove = !!parsedBody.remove_role + if (!shouldRemove) { + shouldRemove = !!select("role_default", "role_id", {guild_id: guildID, role_id: roleID}).get() + } + + if (shouldRemove) { + db.prepare("DELETE FROM role_default WHERE guild_id = ? AND role_id = ?").run(guildID, roleID) + } else { + assert(guild.roles.find(r => r.id === roleID)) + db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID) + } + + if (getRequestHeader(event, "HX-Request")) { + return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID}) + } else { + return sendRedirect(event, "", 302) + } +})) From f7a5b2d74cedd132d6ae12b8e0e00d8f0c2b72ee Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Mar 2026 22:36:05 +1300 Subject: [PATCH 04/45] Update tryToCatch dependency and usages --- package-lock.json | 20 +++++--------------- package.json | 2 +- src/d2m/converters/user-to-mxid.test.js | 2 +- src/matrix/read-registration.test.js | 2 +- src/web/routes/download-discord.test.js | 2 +- src/web/routes/download-matrix.test.js | 2 +- src/web/routes/guild-settings.test.js | 2 +- src/web/routes/guild.test.js | 2 +- src/web/routes/link.test.js | 2 +- src/web/routes/log-in-with-matrix.test.js | 2 +- src/web/routes/oauth.test.js | 2 +- src/web/routes/password.test.js | 2 +- 12 files changed, 16 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e6ecd4..e154da1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "sharp": "^0.34.5", "snowtransfer": "^0.17.5", "stream-mime-type": "^1.0.2", - "try-to-catch": "^3.0.1", + "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", "zod": "^4.0.17" @@ -2756,16 +2756,6 @@ "node": ">=22" } }, - "node_modules/supertape/node_modules/try-to-catch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.5.tgz", - "integrity": "sha512-VKBslDQsy4pGj2TMNgDdskWb7AWSi/9dPEmcNv3sdL0+aRMQTPJo6aEqlcuN0vbOwFfsE1oAXmx3bFdf6vrJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22" - } - }, "node_modules/supertape/node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", @@ -2915,12 +2905,12 @@ } }, "node_modules/try-to-catch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", - "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-4.0.5.tgz", + "integrity": "sha512-VKBslDQsy4pGj2TMNgDdskWb7AWSi/9dPEmcNv3sdL0+aRMQTPJo6aEqlcuN0vbOwFfsE1oAXmx3bFdf6vrJFg==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=22" } }, "node_modules/tslib": { diff --git a/package.json b/package.json index 71639b0..0d0c2b6 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "sharp": "^0.34.5", "snowtransfer": "^0.17.5", "stream-mime-type": "^1.0.2", - "try-to-catch": "^3.0.1", + "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", "zod": "^4.0.17" diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index f8cf16a..b020e15 100644 --- a/src/d2m/converters/user-to-mxid.test.js +++ b/src/d2m/converters/user-to-mxid.test.js @@ -1,5 +1,5 @@ const {test} = require("supertape") -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const assert = require("assert") const data = require("../../../test/data") const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid") diff --git a/src/matrix/read-registration.test.js b/src/matrix/read-registration.test.js index 5fb3b55..a8dcc25 100644 --- a/src/matrix/read-registration.test.js +++ b/src/matrix/read-registration.test.js @@ -1,6 +1,6 @@ // @ts-check -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {test} = require("supertape") const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration") diff --git a/src/web/routes/download-discord.test.js b/src/web/routes/download-discord.test.js index e4f4ab4..0d4b884 100644 --- a/src/web/routes/download-discord.test.js +++ b/src/web/routes/download-discord.test.js @@ -1,7 +1,7 @@ // @ts-check const assert = require("assert").strict -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {test} = require("supertape") const {router} = require("../../../test/web") const {_cache} = require("./download-discord") diff --git a/src/web/routes/download-matrix.test.js b/src/web/routes/download-matrix.test.js index ccbcfdd..610a62d 100644 --- a/src/web/routes/download-matrix.test.js +++ b/src/web/routes/download-matrix.test.js @@ -2,7 +2,7 @@ const fs = require("fs") const {convertImageStream} = require("../../m2d/converters/emoji-sheet") -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {test} = require("supertape") const {router} = require("../../../test/web") const streamWeb = require("stream/web") diff --git a/src/web/routes/guild-settings.test.js b/src/web/routes/guild-settings.test.js index fccc266..00acb89 100644 --- a/src/web/routes/guild-settings.test.js +++ b/src/web/routes/guild-settings.test.js @@ -1,6 +1,6 @@ // @ts-check -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {router, test} = require("../../../test/web") const {select} = require("../../passthrough") const {MatrixServerError} = require("../../matrix/mreq") diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index aa17548..06b604b 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {router, test} = require("../../../test/web") const {MatrixServerError} = require("../../matrix/mreq") const {_getPosition} = require("./guild") diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index e8473f8..70299d5 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -1,6 +1,6 @@ // @ts-check -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {router, test} = require("../../../test/web") const {MatrixServerError} = require("../../matrix/mreq") const {select, db} = require("../../passthrough") diff --git a/src/web/routes/log-in-with-matrix.test.js b/src/web/routes/log-in-with-matrix.test.js index 830556e..2f9afcc 100644 --- a/src/web/routes/log-in-with-matrix.test.js +++ b/src/web/routes/log-in-with-matrix.test.js @@ -1,6 +1,6 @@ // @ts-check -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {router, test} = require("../../../test/web") const {MatrixServerError} = require("../../matrix/mreq") diff --git a/src/web/routes/oauth.test.js b/src/web/routes/oauth.test.js index 2f3a791..1a06e39 100644 --- a/src/web/routes/oauth.test.js +++ b/src/web/routes/oauth.test.js @@ -1,7 +1,7 @@ // @ts-check const DiscordTypes = require("discord-api-types/v10") -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const assert = require("assert/strict") const {router, test} = require("../../../test/web") diff --git a/src/web/routes/password.test.js b/src/web/routes/password.test.js index aa60bd3..fca4e70 100644 --- a/src/web/routes/password.test.js +++ b/src/web/routes/password.test.js @@ -1,6 +1,6 @@ // @ts-check -const tryToCatch = require("try-to-catch") +const {tryToCatch} = require("try-to-catch") const {test} = require("supertape") const {router} = require("../../../test/web") From cd8549da387cf365e2b29c13601f1671a6825e40 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 8 Mar 2026 23:32:36 +1300 Subject: [PATCH 05/45] Fix sticker tests and coverage --- src/d2m/converters/message-to-event.js | 10 ++-- src/m2d/converters/event-to-message.test.js | 64 +++------------------ src/matrix/utils.js | 13 ----- src/web/routes/guild-settings.js | 2 +- 4 files changed, 13 insertions(+), 76 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 7f77b81..b1af2e3 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -582,7 +582,8 @@ async function messageToEvent(message, guild, options = {}, di) { // check that condition 1 or 2 is met if (repliedToEventInDifferentRoom || repliedToUnknownEvent) { let referenced = message.referenced_message - if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves + /* c8 ignore next 4 - backend couldn't be bothered to dereference the message, have to do it ourselves */ + if (!referenced) { assert(message.message_reference?.message_id) referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id) } @@ -905,11 +906,8 @@ async function messageToEvent(message, guild, options = {}, di) { else if (component.type === DiscordTypes.ComponentType.Button) { // May only be a section accessory or in an action row (up to 5) if (component.style === DiscordTypes.ButtonStyle.Link) { - if (component.label) { - stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) - } else { - stack.msb.add(component.url) - } + assert(component.label) // required for Discord to validate link buttons + stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) } } diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 629f2b8..aa426cd 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -4747,17 +4747,17 @@ test("event2message: stickers work", async t => { messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "", + content: "[get_real2](https://bridge.example.org/download/sticker/cadence.moe/NyMXQFAAdniImbHzsygScbmN/_.webp)", avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", - attachments: [{id: "0", filename: "get_real2.gif"}], - pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}] + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) }) test("event2message: stickers fetch mimetype from server when mimetype not provided", async t => { - let called = 0 t.deepEqual( await eventToMessage({ type: "m.sticker", @@ -4768,20 +4768,6 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi }, event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }, {}, {}, { - api: { - async getMedia(mxc, options) { - called++ - t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf") - t.equal(options.method, "HEAD") - return { - status: 200, - headers: new Map([ - ["content-type", "image/gif"] - ]) - } - } - } }), { ensureJoined: [], @@ -4789,48 +4775,14 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "", + content: "[YESYESYES](https://bridge.example.org/download/sticker/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf/_.webp)", avatar_url: undefined, - attachments: [{id: "0", filename: "YESYESYES.gif"}], - pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}] + allowed_mentions: { + parse: ["users", "roles"] + } }] } ) - t.equal(called, 1, "sticker headers should be fetched") -}) - -test("event2message: stickers with unknown mimetype are not allowed", async t => { - let called = 0 - try { - await eventToMessage({ - type: "m.sticker", - sender: "@cadence:cadence.moe", - content: { - body: "something", - url: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe" - }, - event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }, {}, {}, { - api: { - async getMedia(mxc, options) { - called++ - t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe") - t.equal(options.method, "HEAD") - return { - status: 404, - headers: new Map([ - ["content-type", "application/json"] - ]) - } - } - } - }) - /* c8 ignore next */ - t.fail("should throw an error") - } catch (e) { - t.match(e.toString(), "mimetype") - } }) test("event2message: static emojis work", async t => { diff --git a/src/matrix/utils.js b/src/matrix/utils.js index 9f5cb0f..eee635b 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -225,19 +225,6 @@ async function getViaServersQuery(roomID, api) { return qs } -function generatePermittedMediaHash(mxc) { - assert(hasher, "xxhash is not ready yet") - const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) - if (!mediaParts) return undefined - - const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}` - const unsignedHash = hasher.h64(serverAndMediaID) - const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range - db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) - - return serverAndMediaID -} - /** * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge. diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 62a28a1..ae52825 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -134,6 +134,6 @@ as.router.post("/api/default-roles", defineEventHandler(async event => { if (getRequestHeader(event, "HX-Request")) { return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID}) } else { - return sendRedirect(event, "", 302) + return sendRedirect(event, `/guild?guild_id=${guildID}`, 302) } })) From f5ee130463e9a2157a58c840794d669d536d595f Mon Sep 17 00:00:00 2001 From: Bea Date: Sun, 8 Mar 2026 22:11:28 +0000 Subject: [PATCH 06/45] Handle expired invites & fix test registration (#73) This PR addresses a bridge crash discovered while backfilling old channels, alongside a wee QoL fix for the test suite. * **Expired Events (`d2m`):** Wraps Discord scheduled event/invite link lookups in a try-catch block. If a link is expired (404 or Discord error 10006), the bridge now posts a fallback `m.notice` rather than throwing an error and halting message conversion. * **Test Suite Setup:** Updates `test.js` to initialize the mock registration object using `getTemplateRegistration()` preventing test runner crashes when running without a local `registration.yaml` file. Co-authored-by: Cadence Ember Reviewed-on: https://gitdab.com/cadence/out-of-your-element/pulls/73 Co-authored-by: Bea Co-committed-by: Bea --- src/d2m/converters/message-to-event.js | 13 +++++++- src/d2m/converters/message-to-event.test.js | 22 +++++++++++++ test/test.js | 36 ++++++++++----------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index b1af2e3..81c821f 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -770,7 +770,18 @@ async function messageToEvent(message, guild, options = {}, di) { // Then scheduled events if (message.content && di?.snow) { for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old - const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) + let invite + try { + invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) + } catch (e) { + // Skip expired/invalid invites and events + if (e.message === `{"message": "Unknown Invite", "code": 10006}`) { + break + } else { + throw e + } + } + const event = invite.guild_scheduled_event if (!event) continue // the event ID provided was not valid diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 1a73aea..f071417 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1538,6 +1538,28 @@ test("message2event: vc invite event renders embed with room link", async t => { ]) }) +test("message2event: expired/invalid invites are sent as-is", async t => { + const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { + snow: { + invite: { + async getInvite() { + throw new Error(`{"message": "Unknown Invite", "code": 10006}`) + } + } + } + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + body: "https://discord.gg/placeholder?event=1381190945646710824", + format: "org.matrix.custom.html", + formatted_body: "https://discord.gg/placeholder?event=1381190945646710824", + "m.mentions": {}, + msgtype: "m.text", + } + ]) +}) + test("message2event: channel links are converted even inside lists (parser post-processer descends into list items)", async t => { let called = 0 const events = await messageToEvent({ diff --git a/test/test.js b/test/test.js index e05b687..da6bcba 100644 --- a/test/test.js +++ b/test/test.js @@ -6,31 +6,29 @@ const sqlite = require("better-sqlite3") const {Writable} = require("stream") const migrate = require("../src/db/migrate") const HeatSync = require("heatsync") -const {test, extend} = require("supertape") +const {test} = require("supertape") const data = require("./data") const {green} = require("ansi-colors") +const mixin = require("@cloudrac3r/mixin-deep") const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") -const {reg} = require("../src/matrix/read-registration") -reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby" -reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded -reg.ooye.server_name = "cadence.moe" -reg.ooye.namespace_prefix = "_ooye_" -reg.sender_localpart = "_ooye_bot" -reg.id = "baby" -reg.as_token = "don't actually take authenticated actions on the server" -reg.hs_token = "don't actually take authenticated actions on the server" -reg.namespaces = { - users: [{regex: "@_ooye_.*:cadence.moe", exclusive: true}], - aliases: [{regex: "#_ooye_.*:cadence.moe", exclusive: true}] -} -reg.ooye.bridge_origin = "https://bridge.example.org" -reg.ooye.time_zone = "Pacific/Auckland" -reg.ooye.max_file_size = 5000000 -reg.ooye.web_password = "password123" -reg.ooye.include_user_id_in_mxid = false +const registration = require("../src/matrix/read-registration") +registration.reg = mixin(registration.getTemplateRegistration("cadence.moe"), { + id: "baby", + url: "http://localhost:6693", + as_token: "don't actually take authenticated actions on the server", + hs_token: "don't actually take authenticated actions on the server", + ooye: { + server_origin: "https://matrix.cadence.moe", + bridge_origin: "https://bridge.example.org", + discord_token: "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby", + discord_client_secret: "baby", + web_password: "password123", + time_zone: "Pacific/Auckland", + } +}) const sync = new HeatSync({watchFS: false}) From ada3933d9cb3357d2f9cb0b46e2aa00bd3450613 Mon Sep 17 00:00:00 2001 From: Bea Date: Mon, 9 Mar 2026 00:22:41 +0000 Subject: [PATCH 07/45] Backfill: Create new rooms when needed This updates the backfill script to attempt to create rooms for unbridged rooms, rather than bombing out that the room isn't already bridged. Co-authored-by: Cadence Ember Reviewed-on: https://gitdab.com/cadence/out-of-your-element/pulls/75 Co-authored-by: Bea Co-committed-by: Bea --- scripts/backfill.js | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/scripts/backfill.js b/scripts/backfill.js index 27600f0..941e803 100644 --- a/scripts/backfill.js +++ b/scripts/backfill.js @@ -10,7 +10,6 @@ if (!channelID) { process.exit(1) } -const assert = require("assert/strict") const sqlite = require("better-sqlite3") const backfill = new sqlite("scripts/backfill.db") backfill.prepare("CREATE TABLE IF NOT EXISTS backfill (channel_id TEXT NOT NULL, message_id INTEGER NOT NULL, PRIMARY KEY (channel_id, message_id))").run() @@ -38,12 +37,8 @@ passthrough.select = orm.select /** @type {import("../src/d2m/event-dispatcher")}*/ const eventDispatcher = sync.require("../src/d2m/event-dispatcher") - -const roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get() -if (!roomID) { - console.error("Please choose a channel that's already bridged.") - process.exit(1) -} +/** @type {import("../src/d2m/actions/create-room")} */ +const createRoom = sync.require("../src/d2m/actions/create-room") ;(async () => { await discord.cloud.connect() @@ -60,23 +55,29 @@ async function event(event) { if (!channel) return const guild_id = event.d.id - let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0" - console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`) + try { + await createRoom.syncRoom(channelID) + let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0" + console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`) - while (last) { - const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)}) - messages.reverse() // More recent messages come first -> More recent messages come last - for (const message of messages) { - const simulatedGatewayDispatchData = { - guild_id, - backfill: true, - ...message + while (last) { + const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)}) + messages.reverse() // More recent messages come first -> More recent messages come last + for (const message of messages) { + const simulatedGatewayDispatchData = { + guild_id, + backfill: true, + ...message + } + await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData) + preparedInsert.run(channelID, message.id) } - await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData) - preparedInsert.run(channelID, message.id) + last = messages.at(-1)?.id } - last = messages.at(-1)?.id - } - process.exit() + process.exit() + } catch (e) { + console.error(e) + process.exit(1) // won't exit automatically on thrown error due to living discord connection, so manual exit is necessary + } } From 6ca1b836e1e6bf21e09d4217f2373e7d82e4b081 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 11 Mar 2026 12:38:05 +1300 Subject: [PATCH 08/45] Add more debugging information --- src/d2m/discord-client.js | 6 +++++- src/matrix/file.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index 7b0fcf8..c2a0549 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -52,7 +52,11 @@ class DiscordClient { /** @type {Map>} */ this.guildChannelMap = new Map() if (listen !== "no") { - this.cloud.on("event", message => discordPackets.onPacket(this, message, listen)) + this.cloud.on("event", message => { + process.nextTick(() => { + discordPackets.onPacket(this, message, listen) + }) + }) } const addEventLogger = (eventName, logName) => { diff --git a/src/matrix/file.js b/src/matrix/file.js index 7bc1fec..c469aea 100644 --- a/src/matrix/file.js +++ b/src/matrix/file.js @@ -85,6 +85,7 @@ async function _actuallyUploadDiscordFileToMxc(url) { writeRegistration(reg) return root } + e.uploadURL = url throw e } } From 5f768fee01ee2ffcbd879f635fb6bc7c410dd169 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 12 Mar 2026 16:23:22 +1300 Subject: [PATCH 09/45] d->m: Don't guess mentions in code blocks --- src/d2m/converters/edit-to-changes.test.js | 4 +- src/d2m/converters/find-mentions.js | 12 +++- src/d2m/converters/message-to-event.js | 68 ++++++++++++--------- src/d2m/converters/message-to-event.test.js | 25 ++++++-- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index cb1fb5a..842c24e 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -78,7 +78,7 @@ test("edit2changes: bot response", async t => { newContent: { $type: "m.room.message", msgtype: "m.text", - body: "* :ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", format: "org.matrix.custom.html", formatted_body: '* :ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', "m.mentions": { @@ -87,7 +87,7 @@ test("edit2changes: bot response", async t => { // *** Replaced With: *** "m.new_content": { msgtype: "m.text", - body: ":ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + body: ":ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", format: "org.matrix.custom.html", formatted_body: ':ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', "m.mentions": { diff --git a/src/d2m/converters/find-mentions.js b/src/d2m/converters/find-mentions.js index 8726830..8107459 100644 --- a/src/d2m/converters/find-mentions.js +++ b/src/d2m/converters/find-mentions.js @@ -146,10 +146,18 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) { // Highlight the relevant part of the message const start = baseOffset + best.scored.matchedInputTokens[0].index const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end - const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end) + const newNodes = [{ + type: "text", content: content.slice(0, start) + }, { + type: "link", target: `https://matrix.to/#/${best.mxid}`, content: [ + {type: "text", content: content.slice(start, end)} + ] + }, { + type: "text", content: content.slice(end) + }] return { mxid: best.mxid, - newContent + newNodes } } } diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 81c821f..05a376a 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -519,29 +519,60 @@ async function messageToEvent(message, guild, options = {}, di) { return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed })) - async function transformParsedVia(parsed) { - for (const node of parsed) { + async function transformParsedVia(parsed, scanTextForMentions) { + for (let n = 0; n < parsed.length; n++) { + const node = parsed[n] if (node.type === "discordChannel" || node.type === "discordChannelLink") { node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get() if (node.row?.room_id) { node.via = await getViaServersMemo(node.row.room_id) } } + else if (node.type === "text" && typeof node.content === "string") { + // Merge adjacent text nodes into this one + while (parsed[n+1]?.type === "text" && typeof parsed[n+1].content === "string") { + node.content += parsed[n+1].content + parsed.splice(n+1, 1) + } + // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. + if (scanTextForMentions) { + let content = node.content + const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)] + for (let i = matches.length; i--;) { + const m = matches[i] + const prefix = m[1] + const maximumWrittenSection = m[2].toLowerCase() + if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it + if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here + + var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() + assert(roomID) + var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name}))) + + const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content) + if (found) { + addMention(found.mxid) + parsed.splice(n, 1, ...found.newNodes) + content = found.newNodes[0].content + } + } + } + } for (const maybeChildNodesArray of [node, node.content, node.items]) { if (Array.isArray(maybeChildNodesArray)) { - await transformParsedVia(maybeChildNodesArray) + await transformParsedVia(maybeChildNodesArray, scanTextForMentions && ["blockQuote", "list", "paragraph", "em", "strong", "u", "del", "text"].includes(node.type)) } } } return parsed } - let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, { + let html = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, customOptions.isTheMessageContent && options.scanTextForMentions !== false), { discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers), ...customOptions }, customParser, customHtmlOutput) - let body = await markdown.toHtmlWithPostParser(content, transformParsedVia, { + let body = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, false), { // not scanning plaintext body for mentions as we don't parse whether they're in code discordCallback: getDiscordParseCallbacks(message, guild, false), discordOnly: true, escapeHTML: false, @@ -735,35 +766,12 @@ async function messageToEvent(message, guild, options = {}, di) { // Then text content if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) { - // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. - let content = message.content - if (options.scanTextForMentions !== false) { - const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)] - for (let i = matches.length; i--;) { - const m = matches[i] - const prefix = m[1] - const maximumWrittenSection = m[2].toLowerCase() - if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it - if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here - - var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() - assert(roomID) - var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name}))) - - const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content) - if (found) { - addMention(found.mxid) - content = found.newContent - } - } - } - // Scan the content for emojihax and replace them with real emojis - content = content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { + let content = message.content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { return `<:${name}:${id}>` }) - const {body, html} = await transformContent(content) + const {body, html} = await transformContent(content, {isTheMessageContent: true}) await addTextEvent(body, html, msgtype) } diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index f071417..4728c2b 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -789,7 +789,7 @@ test("message2event: simple written @mention for matrix user", async t => { ] }, msgtype: "m.text", - body: "[@ash](https://matrix.to/#/@she_who_brings_destruction:cadence.moe) do you need anything from the store btw as I'm heading there after gym", + body: "@ash do you need anything from the store btw as I'm heading there after gym", format: "org.matrix.custom.html", formatted_body: `@ash do you need anything from the store btw as I'm heading there after gym` }]) @@ -838,7 +838,7 @@ test("message2event: many written @mentions for matrix users", async t => { ] }, msgtype: "m.text", - body: "[@Cadence](https://matrix.to/#/@cadence:cadence.moe), tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and [@huck](https://matrix.to/#/@huckleton:cadence.moe)", + body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck", format: "org.matrix.custom.html", formatted_body: `@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck` }]) @@ -890,7 +890,7 @@ test("message2event: written @mentions may match part of the name", async t => { ] }, msgtype: "m.text", - body: "I wonder if [@cadence](https://matrix.to/#/@secret:cadence.moe) saw this?", + body: "I wonder if @cadence saw this?", format: "org.matrix.custom.html", formatted_body: `I wonder if @cadence saw this?` }]) @@ -941,7 +941,7 @@ test("message2event: written @mentions may match part of the mxid", async t => { ] }, msgtype: "m.text", - body: "I wonder if [@huck](https://matrix.to/#/@huckleton:cadence.moe) saw this?", + body: "I wonder if @huck saw this?", format: "org.matrix.custom.html", formatted_body: `I wonder if @huck saw this?` }]) @@ -962,6 +962,21 @@ test("message2event: written @mentions do not match in URLs", async t => { }]) }) +test("message2event: written @mentions do not match in inline code", async t => { + const events = await messageToEvent({ + ...data.message.advanced_written_at_mention_for_matrix, + content: "`public @Nullable EntityType`" + }, data.guild.general, {}, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "`public @Nullable EntityType`", + format: "org.matrix.custom.html", + formatted_body: `public @Nullable EntityType<?>` + }]) +}) + test("message2event: entire message may match elaborate display name", async t => { let called = 0 const events = await messageToEvent({ @@ -1007,7 +1022,7 @@ test("message2event: entire message may match elaborate display name", async t = ] }, msgtype: "m.text", - body: "[@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆](https://matrix.to/#/@wa:cadence.moe)", + body: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆", format: "org.matrix.custom.html", formatted_body: `@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆` }]) From 99f4c52bebeb514c45a529e06abafefb44790f78 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Mar 2026 10:17:04 +1300 Subject: [PATCH 10/45] Fix attempting to follow an upgrade path twice --- src/matrix/room-upgrade.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/matrix/room-upgrade.js b/src/matrix/room-upgrade.js index 5a2606e..e7de906 100644 --- a/src/matrix/room-upgrade.js +++ b/src/matrix/room-upgrade.js @@ -54,17 +54,17 @@ async function onBotMembership(event, api, createRoom) { assert.equal(event.type, "m.room.member") assert.equal(event.state_key, utils.bot) - // Check if an upgrade is pending for this room - const newRoomID = event.room_id - const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get() - if (!oldRoomID) return false - const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get() - assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining - - // Check if is join/invite - if (event.content.membership !== "invite" && event.content.membership !== "join") return false - return await roomUpgradeSema.request(async () => { + // Check if an upgrade is pending for this room + const newRoomID = event.room_id + const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get() + if (!oldRoomID) return false + const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get() + assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining + + // Check if is join/invite + if (event.content.membership !== "invite" && event.content.membership !== "join") return false + // If invited, join if (event.content.membership === "invite") { await api.joinRoom(newRoomID) From ff022e8793dc32420fda889035d9188112947233 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Mar 2026 11:12:44 +1300 Subject: [PATCH 11/45] Combine additional embed images into same event --- src/d2m/converters/message-to-event.js | 45 ++++-- .../message-to-event.test.embeds.js | 38 +++++ src/d2m/converters/message-to-event.test.js | 2 +- test/data.js | 135 ++++++++++++++++++ 4 files changed, 209 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 05a376a..adc56e6 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -261,6 +261,29 @@ function getFormattedInteraction(interaction, isThinkingInteraction) { } } +/** + * @param {any} newEvents merge into events + * @param {any} events will be modified + * @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc + */ +function mergeTextEvents(newEvents, events, forceSameMsgtype) { + let prev = events.at(-1) + for (const ne of newEvents) { + const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype) + const typesPermitted = !forceSameMsgtype || ne?.msgtype === prev?.msgtype + if (isAllText && typesPermitted) { + const rep = new mxUtils.MatrixStringBuilder() + rep.body = prev.body + rep.formattedBody = prev.formatted_body + rep.addLine(ne.body, ne.formatted_body) + prev.body = rep.body + prev.formatted_body = rep.formattedBody + } else { + events.push(ne) + } + } +} + /** * @param {DiscordTypes.APIMessage} message * @param {DiscordTypes.APIGuild} guild @@ -835,15 +858,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Try to merge attachment events with the previous event // This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together. - let prev = events.at(-1) - for (const atch of attachmentEvents) { - if (atch.msgtype === "m.text" && prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(prev?.msgtype)) { - prev.body = prev.body + "\n" + atch.body - prev.formatted_body = prev.formatted_body + "
" + atch.formatted_body - } else { - events.push(atch) - } - } + mergeTextEvents(attachmentEvents, events, false) } // Then components @@ -981,6 +996,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once const rep = new mxUtils.MatrixStringBuilder() + let isAdditionalImage = false if (isKlipyGIF) { assert(embed.video?.url) @@ -1047,7 +1063,11 @@ async function messageToEvent(message, guild, options = {}, di) { let chosenImage = embed.image?.url // the thumbnail seems to be used for "article" type but displayed big at the bottom by discord if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url - if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) + + if (chosenImage) { + isAdditionalImage = !rep.body && !!events.length + rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) + } if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`) @@ -1056,6 +1076,11 @@ async function messageToEvent(message, guild, options = {}, di) { body = body.split("\n").map(l => "| " + l).join("\n") html = `
${html}
` + if (isAdditionalImage) { + mergeTextEvents([{...rep.get(), body, html, msgtype: "m.notice"}], events, true) + continue + } + // Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person await addTextEvent(body, html, "m.notice") } diff --git a/src/d2m/converters/message-to-event.test.embeds.js b/src/d2m/converters/message-to-event.test.embeds.js index 259aa66..73244d3 100644 --- a/src/d2m/converters/message-to-event.test.embeds.js +++ b/src/d2m/converters/message-to-event.test.embeds.js @@ -204,6 +204,44 @@ test("message2event embeds: author url without name", async t => { }]) }) +test("message2event embeds: 4 images", async t => { + const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046", + format: "org.matrix.custom.html", + formatted_body: "🔀 Forwarded message
https://fixupx.com/i/status/2032003668787020046
", + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "» | ## ⏺️ AUTOMATON WEST (@AUTOMATON_ENG) https://x.com/AUTOMATON_ENG/status/2032003668787020046" + + "\n» | " + + "\n» | 4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”" + + "\n» | ︀︀" + + "\n» | ︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)" + + "\n» | " + + "\n» | **[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **" + + "\n» | " + + "\n» | 📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig" + + "\n» | — FixupX" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig", + format: "org.matrix.custom.html", + formatted_body: "

⏺️ AUTOMATON WEST (@AUTOMATON_ENG)

" + + "

4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non-AI made social network”" + + "
︀︀
︀︀automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/" + + "

💬 36 🔁 212  3.0K 👁 131.7K 

" + + "

📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig

— FixupX
" + + "

📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig

" + + "

📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig

" + + "

📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig

", + "m.mentions": {} + }]) +}) + test("message2event embeds: vx image", async t => { const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general) t.deepEqual(events, [{ diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 4728c2b..e805365 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1099,7 +1099,7 @@ test("message2event: multiple attachments are combined into the same event where formatted_body: "hey" + `
📄 Uploaded file: hey.jpg (100 MB)` + `
📸 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)
` - + `
📄 Uploaded file: hey.jpg (100 MB)` + + `📄 Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", "m.mentions": {}, diff --git a/test/data.js b/test/data.js index 6a53cb0..46e8b0f 100644 --- a/test/data.js +++ b/test/data.js @@ -5067,6 +5067,141 @@ module.exports = { pinned: false, mention_everyone: false, tts: false + }, + four_images: { + type: 0, + content: "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-03-12T18:00:50.737000+00:00", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1481713598278533241", + channel_id: "687028734322147344", + author: { + id: "112760500130975744", + username: "minimus", + avatar: "a_a354b9eaff512485b49c82b13691b941", + discriminator: "0", + public_flags: 512, + flags: 512, + banner: null, + accent_color: null, + global_name: "minimus", + avatar_decoration_data: null, + collectibles: null, + display_name_styles: { font_id: 11, effect_id: 5, colors: [ 6106655 ] }, + banner_color: null, + clan: null, + primary_guild: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "637339857118822430", + message_id: "1481696763483258891", + guild_id: "408573045540651009" + }, + message_snapshots: [ + { + message: { + type: 0, + content: "https://fixupx.com/i/status/2032003668787020046", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [ + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + description: "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”\n" + + "︀︀\n" + + "︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)\n" + + "\n" + + "**[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **", + color: 6513919, + timestamp: "2026-03-12T08:00:02+00:00", + author: { + name: "AUTOMATON WEST (@AUTOMATON_ENG)", + url: "https://x.com/AUTOMATON_ENG/status/2032003668787020046", + icon_url: "https://pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg", + proxy_icon_url: "https://images-ext-1.discordapp.net/external/1OzGhjvZTRstTxM38_7pqHXlmdbMddqh1F8R0-WrKqw/https/pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg" + }, + image: { + url: "https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/NkNgp2SyY1OCH9IdS8hqsUqbnbrp3A9oLNwYusVVCVQ/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg", + width: 872, + height: 886, + content_type: "image/jpeg", + placeholder: "6vcFFwL6R3lye2V3l1mIl5l3WPN5FZ8H", + placeholder_version: 1, + flags: 0 + }, + footer: { + text: "FixupX", + icon_url: "https://assets.fxembed.com/logos/fixupx64.png", + proxy_icon_url: "https://images-ext-1.discordapp.net/external/LwQ70Uiqfu0OCN4ZbA4f482TGCgQa-xGsnUFYfhIgYA/https/assets.fxembed.com/logos/fixupx64.png" + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/Rquh1ec-tG9hMqdHqIVSphO7zf5B5Fg_7yTWhCjlsek/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg", + width: 1114, + height: 991, + content_type: "image/jpeg", + placeholder: "JQgKDoL3epZ8ZIdnlmmHZ4d4CIGmUEc=", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/XrkhHNH3CvlZYvjkdykVnf-_xdz6HWX8uwesoAwwSfY/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUrPobgAAeb90.jpg", + width: 944, + height: 954, + content_type: "image/jpeg", + placeholder: "m/cJDwCbV0mfaoZzlihqeXdqCVN9A6oD", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/lO-5hBMU9bGH13Ax9xum2T2Mg0ATdv0b6BEx_VeVi80/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg", + width: 1200, + height: 630, + content_type: "image/jpeg", + placeholder: "tfcJDIK3mIl1eIiPdY23dX9b9w==", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + } + ], + timestamp: "2026-03-12T16:53:57.009000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] } }, message_with_components: { From f90cdfdbb5442cb0c262137ca207ebd2cc2c3b7d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Mar 2026 14:25:48 +1300 Subject: [PATCH 12/45] Update dependencies, make stream-type independent --- docs/developer-orientation.md | 3 +- package-lock.json | 177 ++++---------------- package.json | 4 +- src/d2m/converters/message-to-event.test.js | 15 ++ src/m2d/actions/sticker.js | 8 +- src/m2d/converters/emoji-sheet.js | 18 +- 6 files changed, 62 insertions(+), 163 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index dbb19f3..e17f263 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,7 +89,7 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin # Dependency justification -Total transitive production dependencies: 134 +Total transitive production dependencies: 121 ### 🦕 @@ -108,6 +108,7 @@ Total transitive production dependencies: 134 * (0) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It depends on h3 and zod, which are already pulled in by OOYE. * (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. +* (0) @cloudrac3r/stream-type: Determine type of Matrix files that don't specify it in info. Switched from stream-mime-type to this. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. * (3) @stackoverflow/stacks: Stack Overflow design language and icons. * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. diff --git a/package-lock.json b/package-lock.json index e154da1..c93d483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,14 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.6.10", + "@cloudrac3r/discord-markdown": "^2.7.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", "@cloudrac3r/mixin-deep": "^3.0.1", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", + "@cloudrac3r/stream-type": "^1.0.0", "@cloudrac3r/turndown": "^7.1.4", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -37,7 +38,6 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", "snowtransfer": "^0.17.5", - "stream-mime-type": "^1.0.2", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", @@ -68,6 +68,22 @@ "ts-node": "^10.9.2" } }, + "../nodejs-stream-type": { + "name": "@cloudrac3r/stream-type", + "version": "1.0.0", + "license": "AGPL-3.0-only", + "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.3", + "@types/node": "^22.19.15", + "c8": "^11.0.0", + "cross-env": "^10.1.0", + "supertape": "^12.10.4", + "try-to-catch": "^4.0.5" + }, + "engines": { + "node": ">=22.6.0" + } + }, "../tap-dot": { "name": "@cloudrac3r/tap-dot", "version": "2.0.0", @@ -156,9 +172,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.10.tgz", - "integrity": "sha512-E+F9UYDUHP2kHDCciX63SBzgsUnHpu2Pp/h98x9Zo+vKuzXjCQ5PcFNdUlH6M18bvHDZPoIsKVmjnON8UYaAPQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.7.0.tgz", + "integrity": "sha512-1iR9tKI2WJe8UNB+4VSh7D8m6RP7ugByuf8RNWyJwyhIrSlqQ8ljY1BKXodSvDg7seZkf7B7V2t5FfK7UpTw/A==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -246,6 +262,10 @@ "pug-error": "^2.1.0" } }, + "node_modules/@cloudrac3r/stream-type": { + "resolved": "../nodejs-stream-type", + "link": true + }, "node_modules/@cloudrac3r/tap-dot": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.3.tgz", @@ -1027,11 +1047,6 @@ "node": ">=22" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -1616,22 +1631,6 @@ "node": ">= 4.9.1" } }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1654,10 +1653,11 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { "version": "3.3.1", @@ -2183,18 +2183,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -2362,34 +2350,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2625,30 +2585,6 @@ "get-source": "^2.0.12" } }, - "node_modules/stream-head": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-head/-/stream-head-2.0.2.tgz", - "integrity": "sha512-aRkUMcmgbDl2Yjd5LqsB1LKB58Ot3JZ4ffuFMkFuvkPQT5X5XFMr4YK2dctApc+d3o52CXU1KUFisYaF/4zjAQ==", - "dependencies": { - "through2": "4.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stream-mime-type": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-mime-type/-/stream-mime-type-1.0.2.tgz", - "integrity": "sha512-80GzRn7JICPDEPBhSyqJjbztqX66+3DpkuUUcgDHtRBQlZRTkbCz0BsISggUl7AnyinJk9zyHVnd2lftlZXDdg==", - "dependencies": { - "file-type": "^16.0.1", - "mime-types": "^2.1.27", - "stream-head": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2700,22 +2636,6 @@ "node": ">=0.10.0" } }, - "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/supertape": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.7.0.tgz", @@ -2845,27 +2765,6 @@ "node": "20 || >=22" } }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/timer-node": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.9.tgz", @@ -2878,22 +2777,6 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" }, - "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/try-catch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-4.0.7.tgz", diff --git a/package.json b/package.json index 0d0c2b6..0e666aa 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,14 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.6.10", + "@cloudrac3r/discord-markdown": "^2.7.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", "@cloudrac3r/mixin-deep": "^3.0.1", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", + "@cloudrac3r/stream-type": "^1.0.0", "@cloudrac3r/turndown": "^7.1.4", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -46,7 +47,6 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", "snowtransfer": "^0.17.5", - "stream-mime-type": "^1.0.2", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index e805365..c4b812d 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -977,6 +977,21 @@ test("message2event: written @mentions do not match in inline code", async t => }]) }) +test("message2event: written @mentions do not match in code block", async t => { + const events = await messageToEvent({ + ...data.message.advanced_written_at_mention_for_matrix, + content: "```java\npublic @Nullable EntityType\n```" + }, data.guild.general, {}, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "```java\npublic @Nullable EntityType\n```", + format: "org.matrix.custom.html", + formatted_body: `
public @Nullable EntityType<?>
` + }]) +}) + test("message2event: entire message may match elaborate display name", async t => { let called = 0 const events = await messageToEvent({ diff --git a/src/m2d/actions/sticker.js b/src/m2d/actions/sticker.js index 341d8b0..8eeb5d2 100644 --- a/src/m2d/actions/sticker.js +++ b/src/m2d/actions/sticker.js @@ -9,7 +9,7 @@ const sharp = require("sharp") const api = sync.require("../../matrix/api") /** @type {import("../../matrix/mreq")} */ const mreq = sync.require("../../matrix/mreq") -const streamMimeType = require("stream-mime-type") +const {streamType} = require("@cloudrac3r/stream-type") const WIDTH = 160 const HEIGHT = 160 @@ -26,13 +26,13 @@ async function getAndResizeSticker(mxc) { } const streamIn = Readable.fromWeb(res.body) - const { stream, mime } = await streamMimeType.getMimeType(streamIn) - const animated = ["image/gif", "image/webp"].includes(mime) + const {streamThrough, type} = await streamType(streamIn) + const animated = ["image/gif", "image/webp"].includes(type) const transformer = sharp({animated: animated}) .resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}}) .webp() - stream.pipe(transformer) + streamThrough.pipe(transformer) return Readable.toWeb(transformer) } diff --git a/src/m2d/converters/emoji-sheet.js b/src/m2d/converters/emoji-sheet.js index 16d5065..dd66a17 100644 --- a/src/m2d/converters/emoji-sheet.js +++ b/src/m2d/converters/emoji-sheet.js @@ -6,7 +6,7 @@ const {pipeline} = require("stream").promises const sharp = require("sharp") const {GIFrame} = require("@cloudrac3r/giframe") const {PNG} = require("@cloudrac3r/pngjs") -const streamMimeType = require("stream-mime-type") +const {streamType} = require("@cloudrac3r/stream-type") const SIZE = 48 const RESULT_WIDTH = 400 @@ -54,11 +54,11 @@ async function compositeMatrixEmojis(mxcs, mxcDownloader) { * @returns {Promise} Uncompressed PNG image */ async function convertImageStream(streamIn, stopStream) { - const {stream, mime} = await streamMimeType.getMimeType(streamIn) - assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(mime), `Mime type ${mime} is impossible for emojis`) + const {streamThrough, type} = await streamType(streamIn) + assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(type), `Mime type ${type} is impossible for emojis`) try { - if (mime === "image/png" || mime === "image/jpeg" || mime === "image/webp") { + if (type === "image/png" || type === "image/jpeg" || type === "image/webp") { /** @type {{info: sharp.OutputInfo, buffer: Buffer}} */ const result = await new Promise((resolve, reject) => { const transformer = sharp() @@ -70,15 +70,15 @@ async function convertImageStream(streamIn, stopStream) { resolve({info, buffer}) }) pipeline( - stream, + streamThrough, transformer ) }) return result.buffer - } else if (mime === "image/gif") { + } else if (type === "image/gif") { const giframe = new GIFrame(0) - stream.on("data", chunk => { + streamThrough.on("data", chunk => { giframe.feed(chunk) }) const frame = await giframe.getFrame() @@ -91,10 +91,10 @@ async function convertImageStream(streamIn, stopStream) { .toBuffer({resolveWithObject: true}) return buffer.data - } else if (mime === "image/apng") { + } else if (type === "image/apng") { const png = new PNG({maxFrames: 1}) // @ts-ignore - stream.pipe(png) + streamThrough.pipe(png) /** @type {Buffer} */ // @ts-ignore const frame = await new Promise(resolve => png.on("parsed", resolve)) stopStream() From cb4e8df91e3e38864befa98f786af0cbf5a60494 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Mar 2026 14:34:59 +1300 Subject: [PATCH 13/45] Fix package-lock --- package-lock.json | 133 ++++++++++++---------------------------------- 1 file changed, 33 insertions(+), 100 deletions(-) diff --git a/package-lock.json b/package-lock.json index c93d483..7b27322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "../nodejs-stream-type": { "name": "@cloudrac3r/stream-type", "version": "1.0.0", + "extraneous": true, "license": "AGPL-3.0-only", "devDependencies": { "@cloudrac3r/tap-dot": "^2.0.3", @@ -263,8 +264,13 @@ } }, "node_modules/@cloudrac3r/stream-type": { - "resolved": "../nodejs-stream-type", - "link": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/stream-type/-/stream-type-1.0.0.tgz", + "integrity": "sha512-orfdUaeDT00fkELxAab+pJNZWwis+KijJEWw+cUWOD2VqqQWriL04W5DOPN0dlsJvn4VoyBe6cYGrzsJ5YPcOw==", + "license": "AGPL-3.0-only", + "engines": { + "node": ">=22.6.0" + } }, "node_modules/@cloudrac3r/tap-dot": { "version": "2.0.3", @@ -1091,6 +1097,15 @@ "node": ">=6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1221,19 +1236,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/brace-expansion": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", @@ -1362,27 +1364,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cloudstorm": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.0.tgz", @@ -1575,25 +1556,6 @@ "node": ">=8.6" } }, - "node_modules/enquirer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/enquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/entities": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz", @@ -2350,6 +2312,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2607,20 +2583,11 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2737,19 +2704,6 @@ "node": ">=6" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -2912,27 +2866,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", From e6c3013993e8e365549b1db9a249ed5b544b1e9a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Mar 2026 20:23:43 +1300 Subject: [PATCH 14/45] Make default permission setting functional --- src/d2m/actions/create-room.js | 2 +- src/d2m/actions/register-user.js | 2 +- src/discord/utils.js | 12 ++++++++++- src/m2d/converters/event-to-message.js | 4 ++-- src/matrix/matrix-command-handler.js | 29 +++++++++++++++++++------- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 651eaf4..0f2f903 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -122,7 +122,7 @@ async function channelToKState(channel, guild, di) { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } - const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages) const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index 1bdd6e3..c837ccb 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -154,7 +154,7 @@ function memberToPowerLevel(user, member, guild, channel) { if (!member) return 0 const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, user.id, channel.permission_overwrites) - const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) /* * PL 100 = Administrator = People who can brick the room. RATIONALE: * - Administrator. diff --git a/src/discord/utils.js b/src/discord/utils.js index a51b155..2431246 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -5,7 +5,7 @@ const assert = require("assert").strict const {reg} = require("../matrix/read-registration") -const {db} = require("../passthrough") +const {db, select} = require("../passthrough") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null @@ -58,6 +58,15 @@ function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrite return allowed } +/** + * @param {{id: string, roles: DiscordTypes.APIGuild["roles"]}} guild + * @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channel] + */ +function getDefaultPermissions(guild, channel) { + const defaultRoles = select("role_default", "role_id", {guild_id: guild.id}).pluck().all() + return getPermissions(guild.id, defaultRoles, guild.roles, undefined, channel) +} + /** * Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`. * It is designed like this to avoid developer error with bit manipulations. @@ -174,6 +183,7 @@ function filterTo(xs, fn) { } module.exports.getPermissions = getPermissions +module.exports.getDefaultPermissions = getDefaultPermissions module.exports.hasPermission = hasPermission module.exports.hasSomePermissions = hasSomePermissions module.exports.hasAllPermissions = hasAllPermissions diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 81ad48c..458924d 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -898,7 +898,7 @@ async function eventToMessage(event, guild, channel, di) { let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<" if (!shouldSuppress && guild?.roles) { // Suppress if regular users don't have permission - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel?.permission_overwrites) const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) shouldSuppress = !canEmbedLinks } @@ -961,7 +961,7 @@ async function eventToMessage(event, guild, channel, di) { // Suppress if regular users don't have permission if (!shouldSuppress && guild?.roles) { - const permissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites) + const permissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites) const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) shouldSuppress = !canEmbedLinks } diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index d568f7b..b38b4b1 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -105,7 +105,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] let matrixOnlyReason = null const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality." // Check if we can/should upload to Discord, for various causes @@ -115,7 +116,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) const slots = getSlotCount(guild.premium_tier) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (guild.emojis.length >= slots) { matrixOnlyReason = "CAPACITY" } else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...) @@ -240,7 +241,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] if (!guildID) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -251,7 +253,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -270,7 +272,8 @@ const commands = [{ // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() - const guildID = discord.channels.get(channelID)?.["guild_id"] + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] if (!guildID) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -281,7 +284,7 @@ const commands = [{ const guild = discord.guilds.get(guildID) assert(guild) - const permissions = dUtils.getPermissions(guild.id, [], guild.roles) + const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"]) if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -290,7 +293,19 @@ const commands = [{ }) } - const invite = await discord.snow.channel.createChannelInvite(channelID) + try { + var invite = await discord.snow.channel.createChannelInvite(channelID) + } catch (e) { + if (e.message === `{"message": "Missing Permissions", "code": 50013}`) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "I don't have permission to create invites to the Discord channel/server." + }) + } else { + throw e + } + } const validHours = Math.ceil(invite.max_age / (60 * 60)) const validUses = ( invite.max_uses === 0 ? "unlimited uses" From 3365023fe39eeda639b06d24ccb67b3b55cef734 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 01:21:38 +1300 Subject: [PATCH 15/45] Sync default roles changes immediately --- src/d2m/discord-packets.js | 2 +- src/web/routes/guild-settings.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 8cf2fde..b1e381e 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -26,6 +26,7 @@ const utils = { client.user = message.d.user client.application = message.d.application console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + interactions.registerInteractions() } else if (message.t === "GUILD_CREATE") { message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web @@ -47,7 +48,6 @@ const utils = { if (listen === "full") { try { - interactions.registerInteractions() await eventDispatcher.checkMissedExpressions(message.d) await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index ae52825..8119f93 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -131,6 +131,9 @@ as.router.post("/api/default-roles", defineEventHandler(async event => { db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID) } + const createSpace = getCreateSpace(event) + await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request + if (getRequestHeader(event, "HX-Request")) { return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID}) } else { From 6716b432bae356f60731df554dfae01e55705a9d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 01:33:29 +1300 Subject: [PATCH 16/45] Wait for response before next click (don't queue) --- src/web/pug/guild.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 74b476a..6dd8601 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -108,7 +108,7 @@ block body h3.mt32.fs-category Default roles .s-card - form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles + form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles input(type="hidden" name="guild_id" value=guild_id) .d-flex.fw-wrap.g4 .s-tag.s-tag__md.fs-body1.s-tag__required @everyone From d3afa728ed0b5ac76ed2946f2519c9b0734eeb7f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Mar 2026 20:53:41 +1300 Subject: [PATCH 17/45] Fix m->d posting embeds even when setting is off --- src/d2m/converters/edit-to-changes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index 4f743eb..61f3290 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -151,9 +151,11 @@ async function editToChanges(message, guild, api) { const messageReallyOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 2 * 60 * 1000 // older than 2 minutes ago // Don't post new generated embeds for messages if the setting was disabled. const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 + // Bots may rely on embeds to send new content, so the rules may be more lax for them. + const botEmbedsApproved = message.author?.bot && !originallyFromMatrix if (messageReallyOld) { eventsToSend = [] // Only allow edits to change and delete, but not send new. - } else if ((messageQuiteOld || !embedsEnabled) && !message.author?.bot) { + } else if ((messageQuiteOld || !embedsEnabled) && !botEmbedsApproved) { eventsToSend = eventsToSend.filter(e => e.msgtype !== "m.notice") // Only send events that aren't embeds. } From f1b111a8a4b871ed15f5293b15bce9f33c9ae4c7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 17 Mar 2026 12:35:42 +1300 Subject: [PATCH 18/45] Refuse to operate on encrypted rooms - Refuse to link to encrypted rooms - Do not show encrypted rooms as link candidates (if server supports) - Reject invites to encrypted rooms with message - Unbridge and leave room if it becomes encrypted --- src/d2m/actions/create-room.js | 5 ++-- src/m2d/event-dispatcher.js | 15 ++++++++++ src/matrix/api.js | 11 +++++--- src/types.d.ts | 9 +++++- src/web/pug/guild.pug | 5 ++++ src/web/routes/guild.js | 3 +- src/web/routes/link.js | 6 ++++ src/web/routes/link.test.js | 50 +++++++++++++++++++++++++++++++--- 8 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 0f2f903..31d3022 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -442,8 +442,9 @@ function syncRoom(channelID) { /** * @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional) * @param {string} guildID + * @param {string} messageBeforeLeave */ -async function unbridgeChannel(channel, guildID) { +async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room was removed from the bridge.") { const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() assert.ok(roomID) const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get() @@ -493,7 +494,7 @@ async function unbridgeChannel(channel, guildID) { // send a notification in the room await api.sendEvent(roomID, "m.room.message", { msgtype: "m.notice", - body: "⚠️ This room was removed from the bridge." + body: `⚠️ ${messageBeforeLeave}` }) // if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 70e293b..085c69c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -413,6 +413,7 @@ async event => { console.error(e) return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`) } + if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.") if (!inviteRoomState?.name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite.`) await api.joinRoom(event.room_id) db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar) @@ -483,6 +484,20 @@ async event => { await roomUpgrade.onTombstone(event, api) })) +sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + // Dramatically unbridge rooms if they become encrypted + if (event.state_key !== "") return + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + if (!channelID) return + const channel = discord.channels.get(channelID) + if (!channel) return + await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.") +})) + module.exports.stringifyErrorStack = stringifyErrorStack module.exports.sendError = sendError module.exports.printError = printError diff --git a/src/matrix/api.js b/src/matrix/api.js index 87bbf0c..f24f4d9 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -172,7 +172,7 @@ function getStateEventOuter(roomID, type, key) { /** * @param {string} roomID * @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event] - * @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?}>} + * @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?, encryption: string?}>} */ async function getInviteState(roomID, event) { function getFromInviteRoomState(strippedState, nskey, key) { @@ -191,7 +191,8 @@ async function getInviteState(roomID, event) { name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"), topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"), avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"), - type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type") + type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"), + encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm") } } @@ -227,7 +228,8 @@ async function getInviteState(roomID, event) { name: getFromInviteRoomState(strippedState, "m.room.name", "name"), topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"), avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"), - type: getFromInviteRoomState(strippedState, "m.room.create", "type") + type: getFromInviteRoomState(strippedState, "m.room.create", "type"), + encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm") } } } catch (e) {} @@ -240,7 +242,8 @@ async function getInviteState(roomID, event) { name: room.name ?? null, topic: room.topic ?? null, avatar: room.avatar_url ?? null, - type: room.room_type ?? null + type: room.room_type ?? null, + encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null } } diff --git a/src/types.d.ts b/src/types.d.ts index a85907d..be037ca 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -157,7 +157,7 @@ export namespace Event { type: string state_key: string sender: string - content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias + content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias | Event.M_Room_Encryption } export type M_Room_Create = { @@ -390,6 +390,12 @@ export namespace Event { body: string replacement_room: string } + + export type M_Room_Encryption = { + algorithm: string + rotation_period_ms?: number + rotation_period_msgs?: number + } } export namespace R { @@ -437,6 +443,7 @@ export namespace R { num_joined_members: number room_id: string room_type?: string + encryption?: string } export type ResolvedRoom = { diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 6dd8601..9791ae3 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -249,6 +249,11 @@ block body ul.my8.ml24 each row in removedLinkedRooms li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name + h3.mt24 Unavailable rooms: Encryption not supported + .s-card.p0 + ul.my8.ml24 + each row in removedEncryptedRooms + li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name h3.mt24 Unavailable rooms: Wrong type .s-card.p0 ul.my8.ml24 diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index a5508c4..70092d5 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -123,13 +123,14 @@ function getChannelRoomsLinks(guild, rooms, roles) { let unlinkedRooms = [...rooms] let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id)) let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type) + let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"]) // https://discord.com/developers/docs/topics/threads#active-archived-threads // need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) return { linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms, - removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms + removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms } } diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 43995fc..772a19c 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -204,6 +204,12 @@ as.router.post("/api/link", defineEventHandler(async event => { throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`}) } + // Check room is not encrypted + const encryption = await api.getStateEvent(parsedBody.matrix, "m.room.encryption", "").catch(() => null) + if (encryption) { + throw createError({status: 400, message: "Bad Request", data: "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room."}) + } + // Check bridge has PL 100 const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api) if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"}) diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index 70299d5..0182093 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -435,6 +435,47 @@ test("web link room: check that bridge can join room (uses via for join attempt) t.equal(called, 2) }) +test("web link room: check that room is not encrypted", async t => { +let called = 0 + const [error] = await tryToCatch(() => router.test("post", "/api/link", { + sessionData: { + managedGuilds: ["665289423482519565"] + }, + body: { + discord: "665310973967597573", + matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe", + guild_id: "665289423482519565" + }, + api: { + async joinRoom(roomID) { + called++ + return roomID + }, + async *generateFullHierarchy(spaceID) { + called++ + t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe") + yield { + room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe", + children_state: [], + guest_can_join: false, + num_joined_members: 2 + } + /* c8 ignore next */ + }, + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe") + if (type === "m.room.encryption" && key === "") { + return {algorithm: "m.megolm.v1.aes-sha2"} + } + throw new Error("Unknown state event") + } + } + })) + t.equal(error.data, "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room.") + t.equal(called, 3) +}) + test("web link room: check that bridge has PL 100 in target room", async t => { let called = 0 const [error] = await tryToCatch(() => router.test("post", "/api/link", { @@ -465,9 +506,10 @@ test("web link room: check that bridge has PL 100 in target room", async t => { async getStateEvent(roomID, type, key) { called++ t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe") - t.equal(type, "m.room.power_levels") - t.equal(key, "") - return {users_default: 50} + if (type === "m.room.power_levels" && key === "") { + return {users_default: 50} + } + throw new Error("Unknown state event") }, async getStateEventOuter(roomID, type, key) { called++ @@ -489,7 +531,7 @@ test("web link room: check that bridge has PL 100 in target room", async t => { } })) t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room") - t.equal(called, 4) + t.equal(called, 5) }) test("web link room: successfully calls createRoom", async t => { From 711e024caaf3739b39bc1b91040b3db6371a6071 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 17 Mar 2026 14:02:11 +1300 Subject: [PATCH 19/45] Update dependencies --- docs/developer-orientation.md | 15 +- package-lock.json | 631 ++++++++++++++++++++-------------- 2 files changed, 371 insertions(+), 275 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index e17f263..bff64c2 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,15 +89,14 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin # Dependency justification -Total transitive production dependencies: 121 +Total transitive production dependencies: 144 ### 🦕 -* (31) better-sqlite3: SQLite is the best database, and this is the best library for it. -* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.) -* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only. -* (9) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, self-service, and more. -* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. +* (35) better-sqlite3: SQLite is the best database, and this is the best library for it. +* (29) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. It has libvips prebuilts for each platform. +* (26) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.) +* (9) h3: Web server. OOYE needs this for the web UI, appservice listener, authmedia proxy, and more. ### 🪱 @@ -116,12 +115,12 @@ Total transitive production dependencies: 121 * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. * (0) discord-api-types: Bitfields needed at runtime and types needed for development. * (0) domino: DOM implementation that's already pulled in by turndown. -* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. +* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. * (0) entities: Looks fine. No dependencies. * (0) get-relative-path: Looks fine. No dependencies. * (1) heatsync: Module hot-reloader that I trust. * (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used. -* (0) mime-type: File extension to mime type mapping that's already pulled in by stream-mime-type. +* (1) mime-types: List of mime type mappings. Needed to serve static files. * (0) prettier-bytes: It does what I want and has no dependencies. * (0) snowtransfer: Discord API library with bring-your-own-caching that I trust. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. diff --git a/package-lock.json b/package-lock.json index 7b27322..10b4668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,50 +54,6 @@ "node": ">=22" } }, - "../extended-errors/enhance-errors": { - "version": "1.0.0", - "extraneous": true, - "license": "UNLICENSED", - "dependencies": { - "ts-expose-internals": "^5.6.3", - "ts-patch": "^3.3.0", - "typescript": "^5.9.3" - }, - "devDependencies": { - "@types/node": "^22.19.1", - "ts-node": "^10.9.2" - } - }, - "../nodejs-stream-type": { - "name": "@cloudrac3r/stream-type", - "version": "1.0.0", - "extraneous": true, - "license": "AGPL-3.0-only", - "devDependencies": { - "@cloudrac3r/tap-dot": "^2.0.3", - "@types/node": "^22.19.15", - "c8": "^11.0.0", - "cross-env": "^10.1.0", - "supertape": "^12.10.4", - "try-to-catch": "^4.0.5" - }, - "engines": { - "node": ">=22.6.0" - } - }, - "../tap-dot": { - "name": "@cloudrac3r/tap-dot", - "version": "2.0.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@cloudrac3r/tap-out": "^3.2.3", - "ansi-colors": "^4.1.3" - }, - "bin": { - "tap-dot": "bin/dot" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -117,12 +73,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -132,9 +88,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -149,6 +105,7 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -184,12 +141,14 @@ "node_modules/@cloudrac3r/giframe": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/giframe/-/giframe-0.4.3.tgz", - "integrity": "sha512-LKuRfdHrhvgPP0heYdlVRecswk/kYaC3fI+X+GQmnkJE36uN1E2dg5l5QdLoukliH7g8S2hgDYk0jsR7sJf8Dg==" + "integrity": "sha512-LKuRfdHrhvgPP0heYdlVRecswk/kYaC3fI+X+GQmnkJE36uN1E2dg5l5QdLoukliH7g8S2hgDYk0jsR7sJf8Dg==", + "license": "MIT" }, "node_modules/@cloudrac3r/html-template-tag": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@cloudrac3r/html-template-tag/-/html-template-tag-5.0.1.tgz", "integrity": "sha512-aH+ZdWJf53E63bVb2FiSnpM81qtF2ZNVbrXjrHcfnofyV/GTYJjZHnmPYC2FgXxJ+I8+bZP3DiwYzj7zXYoekw==", + "license": "MIT", "dependencies": { "html-es6cape": "^2.0.0" } @@ -220,6 +179,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/pngjs/-/pngjs-7.0.3.tgz", "integrity": "sha512-Aghuja9XAIqBPmY2jk8dKZSyK90gImxA4hJeEYYAWkZO34bf+zliUAvGBygoBZA0EgXSmfxewVchL+9y3w+rDw==", + "license": "MIT", "engines": { "node": ">=14.19.0" } @@ -228,6 +188,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug/-/pug-4.0.4.tgz", "integrity": "sha512-RZhxM/WfSHT0n39URlwDdugBfGfwEWmr+w+mCyiT9jaiqCjeZPpXkps/cWLA1XRLo7fzq0+9THtGzVKXS487/A==", + "license": "MIT", "dependencies": { "@cloudrac3r/pug-code-gen": "3.0.5", "@cloudrac3r/pug-lexer": "5.0.3", @@ -242,6 +203,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-code-gen/-/pug-code-gen-3.0.5.tgz", "integrity": "sha512-dKKpy3i9YlVa3lBgu5Jds513c7AtzmmsR2/lGhY2NOODSpIiTcbWLw1obA9YEmmH1tAJny+J6ePYN1N1RgjjQA==", + "license": "MIT", "dependencies": { "constantinople": "^4.0.1", "doctypes": "^1.1.0", @@ -257,6 +219,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@cloudrac3r/pug-lexer/-/pug-lexer-5.0.3.tgz", "integrity": "sha512-ym4g4q+l9IC2H1wXCDnF79AQZ48xtxO675JOT316e17W2wHWtgRccXpT6DkBAaRDZycmkGzSxID1S15T2lZj+g==", + "license": "MIT", "dependencies": { "character-parser": "^4.0.0", "is-expression": "^4.0.0", @@ -307,14 +270,15 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz", "integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==", + "license": "MIT", "dependencies": { "domino": "^2.1.6" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, "dependencies": { @@ -324,12 +288,13 @@ "node_modules/@hotwired/stimulus": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", - "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==" + "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==", + "license": "MIT" }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" @@ -802,9 +767,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { @@ -835,10 +800,11 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -865,6 +831,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -889,6 +856,7 @@ "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-2.0.0.tgz", "integrity": "sha512-/tl1XiBog6XMb1T9kalFerYU86sYsl6EtrlvGI5RVtlHOQdEEJAIPRxmX4vnKG3uoY5aVEkJOWzbPM5tsncmFQ==", "dev": true, + "license": "MIT", "dependencies": { "fastest-levenshtein": "^1.0.12", "just-kebab-case": "^4.2.0" @@ -898,9 +866,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.47", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", - "integrity": "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -976,19 +944,6 @@ "node": ">=22" } }, - "node_modules/@supertape/formatter-progress-bar/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@supertape/formatter-short": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-3.0.0.tgz", @@ -1027,19 +982,6 @@ "node": ">=22" } }, - "node_modules/@supertape/formatter-time/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@supertape/operator-stub": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-4.0.0.tgz", @@ -1054,10 +996,11 @@ } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.19.15", @@ -1070,9 +1013,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1082,6 +1025,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1093,6 +1037,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1107,15 +1052,13 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1126,19 +1069,22 @@ "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", "dev": true, + "license": "MIT", "dependencies": { "printable-characters": "^1.0.42" } }, "node_modules/assert-never": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", - "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT" }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", "dependencies": { "@babel/types": "^7.9.6" }, @@ -1153,13 +1099,13 @@ "license": "MIT" }, "node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/base64-js": { @@ -1179,12 +1125,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "12.6.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", - "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "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==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1199,6 +1146,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" } @@ -1207,13 +1155,27 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/bl/node_modules/buffer": { + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -1231,24 +1193,12 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, - "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/c8": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", @@ -1284,16 +1234,13 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1302,17 +1249,20 @@ "node_modules/character-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-4.0.0.tgz", - "integrity": "sha512-jWburCrDpd+aPopB7esjh/gLyZoHZS4C2xwwJlkTPyhhJdXG+FCG0P4qCOInvOd9yhiuAEJYlZsUMQ0JSK4ykw==" + "integrity": "sha512-jWburCrDpd+aPopB7esjh/gLyZoHZS4C2xwwJlkTPyhhJdXG+FCG0P4qCOInvOd9yhiuAEJYlZsUMQ0JSK4ykw==", + "license": "MIT" }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" }, "node_modules/chunk-text": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/chunk-text/-/chunk-text-2.0.1.tgz", "integrity": "sha512-ER6TSpe2DT4wjOVOKJ3FFAYv7wE77HA/Ztz88Peiv3lq/2oVMsItYJJsVVI0xNZM8cdImOOTNqlw+LQz7gYdJg==", + "license": "MIT", "dependencies": { "runes": "^0.4.3" }, @@ -1322,9 +1272,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -1355,6 +1305,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1382,6 +1333,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1393,22 +1345,25 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.6.0", "@babel/types": "^7.6.1" } }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, "node_modules/cookie-es": { "version": "1.2.2", @@ -1421,6 +1376,7 @@ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -1439,6 +1395,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1467,12 +1424,14 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -1487,6 +1446,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -1494,7 +1454,8 @@ "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" }, "node_modules/destr": { "version": "2.0.5", @@ -1512,9 +1473,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.41", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.41.tgz", - "integrity": "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ==", + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1523,23 +1484,27 @@ "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" }, "node_modules/domino": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", - "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", + "license": "BSD-2-Clause" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -1548,6 +1513,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -1560,6 +1526,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz", "integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1568,10 +1535,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1580,6 +1548,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" } @@ -1589,6 +1558,7 @@ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } @@ -1596,13 +1566,15 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1641,7 +1613,8 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" }, "node_modules/fullstore": { "version": "4.0.0", @@ -1658,6 +1631,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1667,6 +1641,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1674,13 +1649,15 @@ "node_modules/get-relative-path": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-relative-path/-/get-relative-path-1.0.2.tgz", - "integrity": "sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==" + "integrity": "sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==", + "license": "MIT" }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", "dev": true, + "license": "Unlicense", "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -1689,7 +1666,8 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" }, "node_modules/glob": { "version": "13.0.6", @@ -1710,9 +1688,9 @@ } }, "node_modules/h3": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", - "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.6.tgz", + "integrity": "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -1731,15 +1709,17 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1762,13 +1742,15 @@ "node_modules/html-es6cape": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-es6cape/-/html-es6cape-2.0.2.tgz", - "integrity": "sha512-utzhH8rq2VABdW1LsPdv5tmxeMNOtP83If0jKCa79xPBgLWfcMvdf9K+EZoxJ5P7KioCxTs6WBnSDWLQHJ2lWA==" + "integrity": "sha512-utzhH8rq2VABdW1LsPdv5tmxeMNOtP83If0jKCa79xPBgLWfcMvdf9K+EZoxJ5P7KioCxTs6WBnSDWLQHJ2lWA==", + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/htmx.org": { "version": "2.0.8", @@ -1793,17 +1775,20 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" }, "node_modules/iron-webcrypto": { "version": "1.2.1", @@ -1815,12 +1800,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1830,6 +1819,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", "dependencies": { "acorn": "^7.1.1", "object-assign": "^4.1.1" @@ -1840,6 +1830,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1848,13 +1839,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -1864,6 +1857,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -1874,10 +1868,11 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -1887,30 +1882,64 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" }, "node_modules/json-with-bigint": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.4.4.tgz", - "integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==", + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", "dev": true, "license": "MIT" }, @@ -1918,7 +1947,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-4.2.0.tgz", "integrity": "sha512-p2BdO7o4BI+pMun3J+dhaOfYan5JsZrw9wjshRjkWY9+p+u+kKSMhNWYnot2yHDR9CSahZ9iT3dcqJ+V72qHMw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/just-snake-case": { "version": "3.2.0", @@ -1932,6 +1962,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -1943,9 +1974,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -1956,6 +1987,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1970,6 +2002,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1990,6 +2023,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2017,6 +2051,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2034,17 +2069,20 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" }, "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" }, "node_modules/node-abi": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", - "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -2062,6 +2100,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2070,6 +2109,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -2079,6 +2119,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2094,6 +2135,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -2109,6 +2151,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2118,6 +2161,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2126,7 +2170,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.2", @@ -2146,16 +2191,18 @@ } }, "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -2173,12 +2220,13 @@ "node_modules/prettier-bytes": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", - "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==" + "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==", + "license": "ISC" }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2190,29 +2238,18 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/pug-attrs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", "dependencies": { "constantinople": "^4.0.1", "js-stringify": "^1.0.2", @@ -2222,12 +2259,14 @@ "node_modules/pug-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" }, "node_modules/pug-linker": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0", "pug-walk": "^2.0.0" @@ -2237,6 +2276,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", "dependencies": { "object-assign": "^4.1.1", "pug-walk": "^2.0.0" @@ -2246,6 +2286,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0", "token-stream": "1.0.0" @@ -2254,12 +2295,14 @@ "node_modules/pug-runtime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" }, "node_modules/pug-strip-comments": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", "dependencies": { "pug-error": "^2.0.0" } @@ -2267,12 +2310,14 @@ "node_modules/pug-walk": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2288,6 +2333,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -2331,23 +2377,28 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2356,6 +2407,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/runes/-/runes-0.4.3.tgz", "integrity": "sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -2377,12 +2429,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2440,6 +2493,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2452,6 +2506,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2461,6 +2516,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2485,7 +2541,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/simple-get": { "version": "4.0.1", @@ -2505,6 +2562,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -2537,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2552,10 +2611,11 @@ } }, "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", "dev": true, + "license": "Unlicense", "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -2565,6 +2625,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -2574,6 +2635,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2599,14 +2661,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/supertape": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.7.0.tgz", - "integrity": "sha512-5PXh6HsfEJKkC0SMhPNkH35o8Okj8xlVvoju9R0aCohzsK+GEufeYZ1IPhRBRQ2DBLXdMZHVF6N/4pAefxNuAA==", + "version": "12.10.5", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-12.10.5.tgz", + "integrity": "sha512-1Px+6mhFaqcht3p4tkf3o4G8lbBazvx4pgFngm4vGwWipYm3fykm6SJ4ThXobiaNsptz53CDWA2q4B/2KtmA4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2658,6 +2721,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2670,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2693,6 +2758,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -2729,12 +2795,13 @@ "node_modules/token-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" }, "node_modules/try-catch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-4.0.7.tgz", - "integrity": "sha512-gkBWUxbiN4T4PsO8KhoQYWzUPN6e0/h12H9H3YhcfPbwaN8b84fy8cFqL4rWTiPh7qHPFaEfklr6OkVxYRW0Gg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-4.0.9.tgz", + "integrity": "sha512-tEWGmsfqZ9NBzvDOGbACxu+AaXajM6+RtmIM6wCIkFD6lCa3/UvjNuWjCRoOjn8qTKuZlQmrMh8vSTBMQcceew==", "dev": true, "license": "MIT", "engines": { @@ -2761,6 +2828,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -2796,17 +2864,19 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" @@ -2816,6 +2886,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2825,6 +2896,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2839,6 +2911,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", "dependencies": { "@babel/parser": "^7.9.6", "@babel/types": "^7.9.6", @@ -2854,6 +2927,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2866,27 +2940,47 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", - "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==" + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -2896,6 +2990,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2914,6 +3009,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -2923,6 +3019,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, From 5b04b5d71231b89f2320f97f05fc0968ca28ba29 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:33:50 +1300 Subject: [PATCH 20/45] Reformat /plu/ral emulated replies --- src/d2m/converters/message-to-event.js | 27 ++++++++++++++++--------- src/db/migrations/0035-role-default.sql | 2 +- src/discord/utils.js | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index adc56e6..3f598f2 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -35,10 +35,10 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() - const interaction = message.interaction_metadata || message.interaction + const interactionMetadata = message.interaction_metadata const username = message.mentions?.find(ment => ment.id === node.id)?.username || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username - || (interaction?.user.id === node.id ? interaction.user.username : null) + || (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null) || (message.author?.id === node.id ? message.author.username : null) || "unknown-user" if (mxid && useHTML) { @@ -357,9 +357,8 @@ async function messageToEvent(message, guild, options = {}, di) { }] } - const interaction = message.interaction_metadata || message.interaction - const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction - const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) + let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction + let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) /** @type {{room?: boolean, user_ids?: string[]}} @@ -400,6 +399,16 @@ async function messageToEvent(message, guild, options = {}, di) { } else if (message.referenced_message) { repliedToUnknownEvent = true } + } else if (message.type === DiscordTypes.MessageType.ContextMenuCommand && message.interaction && message.message_reference?.message_id) { + // It could be a /plu/ral emulated reply + if (message.interaction.name.startsWith("Reply ") && message.content.startsWith("-# [↪](")) { + const row = await getHistoricalEventRow(message.message_reference?.message_id) + if (row && "event_id" in row) { + repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id}) + message.content = message.content.replace(/^.*\n/, "") + isInteraction = false // declutter + } + } } else if (dUtils.isWebhookMessage(message) && message.embeds[0]?.author?.name?.endsWith("↩️")) { // It could be a PluralKit emulated reply, let's see if it has a message link const isEmulatedReplyToText = message.embeds[0].description?.startsWith("**[Reply to:]") @@ -685,8 +694,8 @@ async function messageToEvent(message, guild, options = {}, di) { } } - if (isInteraction && !isThinkingInteraction && events.length === 0) { - const formattedInteraction = getFormattedInteraction(interaction, false) + if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) { + const formattedInteraction = getFormattedInteraction(message.interaction, false) body = `${formattedInteraction.body}\n${body}` html = `${formattedInteraction.html}${html}` } @@ -782,8 +791,8 @@ async function messageToEvent(message, guild, options = {}, di) { events.push(...forwardedEvents) } - if (isThinkingInteraction) { - const formattedInteraction = getFormattedInteraction(interaction, true) + if (isInteraction && isThinkingInteraction && message.interaction) { + const formattedInteraction = getFormattedInteraction(message.interaction, true) await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice") } diff --git a/src/db/migrations/0035-role-default.sql b/src/db/migrations/0035-role-default.sql index 6c44e7e..a5ce62d 100644 --- a/src/db/migrations/0035-role-default.sql +++ b/src/db/migrations/0035-role-default.sql @@ -4,6 +4,6 @@ CREATE TABLE "role_default" ( "guild_id" TEXT NOT NULL, "role_id" TEXT NOT NULL, PRIMARY KEY ("guild_id", "role_id") -); +) WITHOUT ROWID; COMMIT; diff --git a/src/discord/utils.js b/src/discord/utils.js index 2431246..aed7068 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -114,7 +114,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) { * @param {DiscordTypes.APIMessage} message */ function isWebhookMessage(message) { - return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand + return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand } /** From f8896dce7f6193caf98f36d83077ea680f1a45c3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:34:19 +1300 Subject: [PATCH 21/45] Type fixes in set-presence.js --- src/d2m/actions/set-presence.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/d2m/actions/set-presence.js b/src/d2m/actions/set-presence.js index f26668f..0a31038 100644 --- a/src/d2m/actions/set-presence.js +++ b/src/d2m/actions/set-presence.js @@ -1,5 +1,7 @@ // @ts-check +const assert = require("assert").strict + const passthrough = require("../../passthrough") const {sync, select} = passthrough /** @type {import("../../matrix/api")} */ @@ -26,7 +28,7 @@ const presenceLoopInterval = 28e3 // Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence const guildPresenceSetting = new class { - /** @private @type {Set} */ guilds + /** @private @type {Set} */ guilds = new Set() constructor() { this.update() } @@ -40,7 +42,7 @@ const guildPresenceSetting = new class { class Presence extends sync.reloadClassMethods(() => Presence) { /** @type {string} */ userID - /** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string}} */ data + /** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string} | undefined} */ data /** @private @type {?string | undefined} */ mxid /** @private @type {number} */ delay = Math.random() @@ -66,6 +68,7 @@ class Presence extends sync.reloadClassMethods(() => Presence) { // I haven't tried, but I assume Synapse explodes if you try to update too many presences at the same time. // This random delay will space them out over the whole 28 second cycle. setTimeout(() => { + assert(this.data) api.setPresence(this.data, mxid).catch(() => {}) }, this.delay * presenceLoopInterval).unref() } From d2557f73bb4ff4f15f65900c15156e3335fe519a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 13:35:53 +1300 Subject: [PATCH 22/45] Let sims rejoin after being unbanned The sim_member cache was getting stuck, so OOYE thought it was already in the room when it actually wasn't. --- src/m2d/event-dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 085c69c..c11b696 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -423,7 +423,10 @@ async event => { if (event.content.membership === "leave" || event.content.membership === "ban") { // Member is gone + // if Matrix member, data was cached in member_cache db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) + // if Discord member (so kicked/banned by Matrix user), data was cached in sim_member + db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) // Unregister room's use as a direct chat and/or an invite target if the bot itself left if (event.state_key === utils.bot) { From 876d91fbf487ca327dd8fdd6f3913ebd701b4d93 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 14:30:10 +1300 Subject: [PATCH 23/45] Remove sims when the Discord user leaves --- src/d2m/actions/register-user.js | 10 ++++- src/d2m/actions/remove-member.js | 26 +++++++++++ src/d2m/actions/send-message.js | 2 +- .../message-to-event.test.components.js | 2 +- src/d2m/converters/remove-member-mxids.js | 38 ++++++++++++++++ .../converters/remove-member-mxids.test.js | 43 +++++++++++++++++++ src/d2m/discord-packets.js | 3 +- src/d2m/event-dispatcher.js | 35 +++++++++++++++ src/db/migrations/0036-app-user-install.sql | 10 +++++ src/db/orm-defs.d.ts | 6 +++ test/ooye-test-data.sql | 17 +++++++- test/test.js | 1 + 12 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src/d2m/actions/remove-member.js create mode 100644 src/d2m/converters/remove-member-mxids.js create mode 100644 src/d2m/converters/remove-member-mxids.test.js create mode 100644 src/db/migrations/0036-app-user-install.sql diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index c837ccb..d475e54 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -206,14 +206,16 @@ function _hashProfileContent(content, powerLevel) { * 3. Calculate the power level the user should get based on their Discord permissions * 4. Compare against the previously known state content, which is helpfully stored in the database * 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time + * 6. If the sim is for a user-installed app, check which user it was added by * @param {DiscordTypes.APIUser} user * @param {Omit | undefined} member * @param {DiscordTypes.APIGuildChannel} channel * @param {DiscordTypes.APIGuild} guild * @param {string} roomID + * @param {DiscordTypes.APIMessageInteractionMetadata} [interactionMetadata] * @returns {Promise} mxid of the updated sim */ -async function syncUser(user, member, channel, guild, roomID) { +async function syncUser(user, member, channel, guild, roomID, interactionMetadata) { const mxid = await ensureSimJoined(user, roomID) const content = await memberToStateContent(user, member, guild.id) const powerLevel = memberToPowerLevel(user, member, guild, channel) @@ -222,6 +224,12 @@ async function syncUser(user, member, channel, guild, roomID) { allowOverwrite: !!member, globalProfile: await userToGlobalProfile(user) }) + + const appInstalledByUser = user.bot && interactionMetadata?.authorizing_integration_owners?.[DiscordTypes.ApplicationIntegrationType.UserInstall] + if (appInstalledByUser) { + db.prepare("INSERT OR IGNORE INTO app_user_install (app_bot_id, user_id, guild_id) VALUES (?, ?, ?)").run(user.id, appInstalledByUser, guild.id) + } + return mxid } diff --git a/src/d2m/actions/remove-member.js b/src/d2m/actions/remove-member.js new file mode 100644 index 0000000..4dbd5a6 --- /dev/null +++ b/src/d2m/actions/remove-member.js @@ -0,0 +1,26 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const {sync, db, select, from} = passthrough +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") +/** @type {import("../converters/remove-member-mxids")} */ +const removeMemberMxids = sync.require("../converters/remove-member-mxids") + +/** + * @param {string} userID discord user ID that left + * @param {string} guildID discord guild ID that they left + */ +async function removeMember(userID, guildID) { + const {userAppDeletions, membership} = removeMemberMxids.removeMemberMxids(userID, guildID) + db.transaction(() => { + for (const d of userAppDeletions) { + db.prepare("DELETE FROM app_user_install WHERE guild_id = ? and user_id = ?").run(guildID, d) + } + })() + for (const m of membership) { + await api.leaveRoom(m.room_id, m.mxid) + } +} + +module.exports.removeMember = removeMember diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index eb919bb..8550d43 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -51,7 +51,7 @@ async function sendMessage(message, channel, guild, row) { if (message.author.id === discord.application.id) { // no need to sync the bot's own user } else { - senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID) + senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID, message.interaction_metadata) } } diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 7d875a6..137b63b 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -65,7 +65,7 @@ test("message2event components: pk question mark output", async t => { + "
" + "

System: INX (xffgnx)" + "
Member: Lillith (pphhoh)" - + "
Sent by: infinidoge1337 (@unknown-user:)" + + "
Sent by: infinidoge1337 (@unknown-user)" + "

Account Roles (7)" + "
§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping

" + `🖼️ https://files.inx.moe/p/cdn/lillith.webp` diff --git a/src/d2m/converters/remove-member-mxids.js b/src/d2m/converters/remove-member-mxids.js new file mode 100644 index 0000000..de26662 --- /dev/null +++ b/src/d2m/converters/remove-member-mxids.js @@ -0,0 +1,38 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const {db, select, from} = passthrough + +/** + * @param {string} userID discord user ID that left + * @param {string} guildID discord guild ID that they left + */ +function removeMemberMxids(userID, guildID) { + // Get sims for user and remove + let membership = from("sim").join("sim_member", "mxid").join("channel_room", "room_id") + .select("room_id", "mxid").where({user_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all() + membership = membership.concat(from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").join("channel_room", "room_id") + .select("room_id", "mxid").where({proxy_owner_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all()) + + // Get user installed apps and remove + /** @type {string[]} */ + let userAppDeletions = [] + // 1. Select apps that have 1 user remaining + /** @type {Set} */ + const appsWithOneUser = new Set(db.prepare("SELECT app_bot_id FROM app_user_install WHERE guild_id = ? GROUP BY app_bot_id HAVING count(*) = 1").pluck().all(guildID)) + // 2. Select apps installed by this user + const appsFromThisUser = new Set(select("app_user_install", "app_bot_id", {guild_id: guildID, user_id: userID}).pluck().all()) + if (appsFromThisUser.size) userAppDeletions.push(userID) + // Then remove user installed apps if this was the last user with them + const appsToRemove = appsWithOneUser.intersection(appsFromThisUser) + for (const botID of appsToRemove) { + // Remove sims for user installed app + const appRemoval = removeMemberMxids(botID, guildID) + membership = membership.concat(appRemoval.membership) + userAppDeletions = userAppDeletions.concat(appRemoval.userAppDeletions) + } + + return {membership, userAppDeletions} +} + +module.exports.removeMemberMxids = removeMemberMxids diff --git a/src/d2m/converters/remove-member-mxids.test.js b/src/d2m/converters/remove-member-mxids.test.js new file mode 100644 index 0000000..a880dff --- /dev/null +++ b/src/d2m/converters/remove-member-mxids.test.js @@ -0,0 +1,43 @@ +// @ts-check + +const {test} = require("supertape") +const {removeMemberMxids} = require("./remove-member-mxids") + +test("remove member mxids: would remove mxid for all rooms in this server", t => { + t.deepEqual(removeMemberMxids("772659086046658620", "112760669178241024"), { + userAppDeletions: [], + membership: [{ + mxid: "@_ooye_cadence:cadence.moe", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, { + mxid: "@_ooye_cadence:cadence.moe", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }] + }) +}) + +test("remove member mxids: removes sims too", t => { + t.deepEqual(removeMemberMxids("196188877885538304", "112760669178241024"), { + userAppDeletions: [], + membership: [{ + mxid: '@_ooye_ampflower:cadence.moe', + room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe' + }, { + mxid: '@_ooye__pk_zoego:cadence.moe', + room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe' + }] + }) +}) + +test("remove member mxids: removes apps too", t => { + t.deepEqual(removeMemberMxids("197126718400626689", "66192955777486848"), { + userAppDeletions: ["197126718400626689"], + membership: [{ + mxid: '@_ooye_infinidoge1337:cadence.moe', + room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe' + }, { + mxid: '@_ooye_evil_lillith_sheher:cadence.moe', + room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe' + }] + }) +}) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index b1e381e..afea9ea 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -49,8 +49,9 @@ const utils = { if (listen === "full") { try { await eventDispatcher.checkMissedExpressions(message.d) - await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) + await eventDispatcher.checkMissedPins(client, message.d) + await eventDispatcher.checkMissedLeaves(client, message.d) } catch (e) { console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") console.error(e) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 01bbc67..7d156a0 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -32,6 +32,8 @@ const speedbump = sync.require("./actions/speedbump") const retrigger = sync.require("./actions/retrigger") /** @type {import("./actions/set-presence")} */ const setPresence = sync.require("./actions/set-presence") +/** @type {import("./actions/remove-member")} */ +const removeMember = sync.require("./actions/remove-member") /** @type {import("./actions/poll-vote")} */ const vote = sync.require("./actions/poll-vote") /** @type {import("../m2d/event-dispatcher")} */ @@ -172,6 +174,31 @@ module.exports = { await createSpace.syncSpaceExpressions(data, true) }, + /** + * When logging back in, check if any members left while we were gone. + * Do this by getting the member list from Discord and seeing who we still have locally that isn't there in the response. + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild + */ + async checkMissedLeaves(client, guild) { + const maxLimit = 1000 + if (guild.member_count >= maxLimit) return // too large to want to scan + const discordMembers = await client.snow.guild.getGuildMembers(guild.id, {limit: maxLimit}) + if (discordMembers.length >= maxLimit) return // response was maxed out, there are guild members that weren't listed, can't act safely + const discordMembersSet = new Set(discordMembers.map(m => m.user.id)) + // no indexes on this one but I'll cope + const membersAddedOnMatrix = new Set(from("sim").join("sim_member", "mxid").join("channel_room", "room_id") + .pluck("user_id").selectUnsafe("DISTINCT user_id").where({guild_id: guild.id}).and("AND user_id not like '%-%' and user_id not like '%\\_%' escape '\\'").all()) + const userInstalledAppIDs = new Set(from("app_user_install").pluck("app_bot_id").selectUnsafe("DISTINCT app_bot_id").where({guild_id: guild.id}).all()) + // loop over members added on matrix and if the member does not exist on discord-side then they should be removed + for (const userID of membersAddedOnMatrix) { + if (userInstalledAppIDs.has(userID)) continue // skip user installed apps here since they're never true members - they'll be removed by removeMember when the associated user is removed + if (!discordMembersSet.has(userID)) { + await removeMember.removeMember(userID, guild.id) + } + } + }, + /** * Announces to the parent room that the thread room has been created. * See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement" @@ -211,6 +238,14 @@ module.exports = { } }, + /** + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data + */ + async GUILD_MEMBER_REMOVE(client, data) { + await removeMember.removeMember(data.user.id, data.guild_id) + }, + /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread diff --git a/src/db/migrations/0036-app-user-install.sql b/src/db/migrations/0036-app-user-install.sql new file mode 100644 index 0000000..087a0ac --- /dev/null +++ b/src/db/migrations/0036-app-user-install.sql @@ -0,0 +1,10 @@ +BEGIN TRANSACTION; + +CREATE TABLE "app_user_install" ( + "guild_id" TEXT NOT NULL, + "app_bot_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + PRIMARY KEY ("guild_id", "app_bot_id", "user_id") +) WITHOUT ROWID; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index f6628f2..d95bfc3 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -1,4 +1,10 @@ export type Models = { + app_user_install: { + guild_id: string + app_bot_id: string + user_id: string + } + auto_emoji: { name: string emoji_id: string diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1dd9dfe..07f8c24 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -38,15 +38,28 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'), ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'), ('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'), -('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'); +('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'), +('196188877885538304', 'ampflower', 'ampflower', '@_ooye_ampflower:cadence.moe'), +('1458668878107381800', 'Evil Lillith (she/her)', 'evil_lillith_sheher', '@_ooye_evil_lillith_sheher:cadence.moe'), +('197126718400626689', 'infinidoge1337', 'infinidoge1337', '@_ooye_infinidoge1337:cadence.moe'); + INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL), -('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); +('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL), +('@_ooye_cadence:cadence.moe', '!kLRqKKUQXcibIMtOpl:cadence.moe', NULL), +('@_ooye_cadence:cadence.moe', '!fGgIymcYWOqjbSRUdV:cadence.moe', NULL), +('@_ooye_ampflower:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL), +('@_ooye__pk_zoego:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL), +('@_ooye_infinidoge1337:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL), +('@_ooye_evil_lillith_sheher:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL); INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); +INSERT INTO app_user_install (guild_id, app_bot_id, user_id) VALUES +('66192955777486848', '1458668878107381800', '197126718400626689'); + INSERT INTO message_room (message_id, historical_room_index) WITH a (message_id, channel_id) AS (VALUES ('1106366167788044450', '122155380120748034'), diff --git a/test/test.js b/test/test.js index da6bcba..4cd9627 100644 --- a/test/test.js +++ b/test/test.js @@ -152,6 +152,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/converters/message-to-event.test.embeds") require("../src/d2m/converters/message-to-event.test.pk") require("../src/d2m/converters/pins-to-list.test") + require("../src/d2m/converters/remove-member-mxids.test") require("../src/d2m/converters/remove-reaction.test") require("../src/d2m/converters/thread-to-announcement.test") require("../src/d2m/converters/user-to-mxid.test") From e8d9a5e4ae0078e664365283ea2ad0f60c8b7a81 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 19 Mar 2026 14:30:19 +1300 Subject: [PATCH 24/45] Script to remove uncached bridged users --- scripts/remove-uncached-bridged-users.js | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 scripts/remove-uncached-bridged-users.js diff --git a/scripts/remove-uncached-bridged-users.js b/scripts/remove-uncached-bridged-users.js new file mode 100644 index 0000000..b3ceb8a --- /dev/null +++ b/scripts/remove-uncached-bridged-users.js @@ -0,0 +1,36 @@ +// @ts-check + +const HeatSync = require("heatsync") +const sync = new HeatSync({watchFS: false}) + +const sqlite = require("better-sqlite3") +const db = new sqlite("ooye.db", {fileMustExist: true}) + +const passthrough = require("../src/passthrough") +Object.assign(passthrough, {db, sync}) + +const api = require("../src/matrix/api") +const utils = require("../src/matrix/utils") +const {reg} = require("../src/matrix/read-registration") + +const rooms = db.prepare("select room_id, name, nick from channel_room").all() + +;(async () => { + // Search for members starting with @_ooye_ and kick them if they are not in sim_member cache + for (const room of rooms) { + try { + const members = await api.getJoinedMembers(room.room_id) + for (const mxid of Object.keys(members.joined)) { + if (!mxid.startsWith("@" + reg.sender_localpart) && utils.eventSenderIsFromDiscord(mxid) && !db.prepare("select mxid from sim_member where mxid = ? and room_id = ?").get(mxid, room.room_id)) { + await api.leaveRoom(room.room_id, mxid) + } + } + } catch (e) { + if (e.message.includes("Appservice not in room")) { + // ok + } else { + throw e + } + } + } +})() From 8c87d93011d8176ef019ae24ecce213197f5c2bb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:17:40 +1300 Subject: [PATCH 25/45] Remove member repetition bugfixes --- src/d2m/actions/remove-member.js | 13 ++++++++++++- src/d2m/event-dispatcher.js | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/d2m/actions/remove-member.js b/src/d2m/actions/remove-member.js index 4dbd5a6..56ac750 100644 --- a/src/d2m/actions/remove-member.js +++ b/src/d2m/actions/remove-member.js @@ -19,7 +19,18 @@ async function removeMember(userID, guildID) { } })() for (const m of membership) { - await api.leaveRoom(m.room_id, m.mxid) + try { + await api.leaveRoom(m.room_id, m.mxid) + } catch (e) { + if (String(e).includes("not in room")) { + // no further action needed + } else { + throw e + } + } + // Update cache to say that the member isn't in the room any more + // You'd think this would happen automatically when the leave event arrives at Matrix's event dispatcher, but that isn't 100% reliable. + db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?").run(m.room_id, m.mxid) } } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 7d156a0..c86cc13 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -125,6 +125,7 @@ module.exports = { // Send in order for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) { const message = messages[i] + if (message.type === DiscordTypes.MessageType.UserJoin) continue // since join announcements don't become events, it would be a repetition to act on them during backfill if (!members.has(message.author.id)) members.set(message.author.id, await client.snow.guild.getGuildMember(guild.id, message.author.id).catch(() => undefined)) await module.exports.MESSAGE_CREATE(client, { From 9dbd871e0bafba5c077d40fa5098b0caf476319a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:42:51 +1300 Subject: [PATCH 26/45] Defuse mentions in m->d reply if client says so --- src/m2d/converters/event-to-message.js | 33 +++++- src/m2d/converters/event-to-message.test.js | 124 +++++++++++++++++++- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 458924d..a49dd1c 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -471,7 +471,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { // @ts-ignore - typescript doesn't know about indices yet content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]), ensureJoined: [], - allowedMentionsParse: ["everyone"] + allowedMentionsParse: ["everyone"], + allowedMentionsUsers: [] } } } else if (writtenMentionMatch[1].length < 40) { // the API supports up to 100 characters, but really if you're searching more than 40, something messed up @@ -482,7 +483,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { // @ts-ignore - typescript doesn't know about indices yet content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), ensureJoined: [results[0].user], - allowedMentionsParse: [] + allowedMentionsParse: [], + allowedMentionsUsers: [results[0].user.id] } } } @@ -544,6 +546,7 @@ async function eventToMessage(event, guild, channel, di) { let displayName = event.sender let avatarURL = undefined const allowedMentionsParse = ["users", "roles"] + const allowedMentionsUsers = [] /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" @@ -986,16 +989,34 @@ async function eventToMessage(event, guild, channel, di) { } } + // Complete content content = displayNameRunoff + replyLine + content - // Split into 2000 character chunks const chunks = chunk(content, 2000) + + // If m.mentions is specified and valid, overwrite allowedMentionsParse with a converted m.mentions + let allowed_mentions = {parse: allowedMentionsParse} + if (event.content["m.mentions"]) { + // Combine requested mentions with detected written mentions to get the full list + if (Array.isArray(event.content["m.mentions"].user_ids)) { + for (const mxid of event.content["m.mentions"].user_ids) { + const user_id = select("sim", "user_id", {mxid}).pluck().get() + if (!user_id) continue + allowedMentionsUsers.push( + select("sim_proxy", "proxy_owner_id", {user_id}).pluck().get() || user_id + ) + } + } + // Specific mentions were requested, so do not parse users + allowed_mentions.parse = allowed_mentions.parse.filter(x => x !== "users") + allowed_mentions.users = allowedMentionsUsers + } + + // Assemble chunks into Discord messages content /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */ const messages = chunks.map(content => ({ content, - allowed_mentions: { - parse: allowedMentionsParse - }, + allowed_mentions, username: displayNameShortened, avatar_url: avatarURL })) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index aa426cd..b057b63 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -266,7 +266,8 @@ test("event2message: markdown in link text does not attempt to be escaped becaus content: "hey [@mario sports mix [she/her]](), is it possible to listen on a unix socket?", avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -547,7 +548,8 @@ test("event2message: links don't have angle brackets added by accident", async t content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp", avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1296,7 +1298,8 @@ test("event2message: lists have appropriate line breaks", async t => { content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`, avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1337,7 +1340,8 @@ test("event2message: ordered list start attribute works", async t => { content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`, avatar_url: undefined, allowed_mentions: { - parse: ["users", "roles"] + parse: ["roles"], + users: [] } }] } @@ -1463,6 +1467,118 @@ test("event2message: rich reply to a sim user", async t => { ) }) +test("event2message: rich reply to a sim user, explicitly enabling mentions in client", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + }, + "m.mentions": { + user_ids: ["@_ooye_kyuugryphon:cadence.moe"] + } + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, data.channel.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + + "\nTesting this reply, ignore", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["roles"], + users: ["111604486476181504"] + } + }] + } + ) +}) + +test("event2message: rich reply to a sim user, explicitly disabling mentions in client", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + }, + "m.mentions": {} + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, data.channel.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + " Slow news day." + + "\nTesting this reply, ignore", + avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", + allowed_mentions: { + parse: ["roles"], + users: [] + } + }] + } + ) +}) + test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => { t.deepEqual( await eventToMessage({ From 0dac3d2898d410ed9bbd6faa209101db39630f6c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:53:09 +1300 Subject: [PATCH 27/45] Internal language adjusted --- src/d2m/actions/create-room.js | 2 +- src/d2m/converters/message-to-event.test.embeds.js | 4 ++-- src/m2d/converters/event-to-message.js | 2 +- src/m2d/converters/event-to-message.test.js | 6 +++--- test/data.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 31d3022..c2ec01a 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -256,7 +256,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) { /** * Handling power levels separately. The spec doesn't specify what happens, Dendrite differs, - * and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates. + * and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates. * We don't want the `events` key to be overridden completely. * https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210 * https://github.com/matrix-org/matrix-spec/issues/492 diff --git a/src/d2m/converters/message-to-event.test.embeds.js b/src/d2m/converters/message-to-event.test.embeds.js index 73244d3..fdb0807 100644 --- a/src/d2m/converters/message-to-event.test.embeds.js +++ b/src/d2m/converters/message-to-event.test.embeds.js @@ -125,8 +125,8 @@ test("message2event embeds: blockquote in embed", async t => { t.equal(called, 1, "should call getJoinedMembers once") }) -test("message2event embeds: crazy html is all escaped", async t => { - const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general) +test("message2event embeds: extreme html is all escaped", async t => { + const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index a49dd1c..2d45756 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -766,7 +766,7 @@ async function eventToMessage(event, guild, channel, di) { // Generate a reply preview for a standard message repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards - repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line + repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) repliedToContent = repliedToContent.replace(/]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b057b63..1c263b4 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -1943,9 +1943,9 @@ test("event2message: should suppress embeds for links in reply preview", async t sender: "@rnl:cadence.moe", content: { msgtype: "m.text", - body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, format: "org.matrix.custom.html", - formatted_body: `
In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + formatted_body: `
In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, "m.relates_to": { "m.in_reply_to": { event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU" @@ -1975,7 +1975,7 @@ test("event2message: should suppress embeds for links in reply preview", async t username: "RNL", content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:" + " " - + `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, + + `\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`, avatar_url: undefined, allowed_mentions: { parse: ["users", "roles"] diff --git a/test/data.js b/test/data.js index 46e8b0f..45e0388 100644 --- a/test/data.js +++ b/test/data.js @@ -4617,7 +4617,7 @@ module.exports = { flags: 0, components: [] }, - escaping_crazy_html_tags: { + extreme_html_escaping: { id: "1158894131322552391", type: 0, content: "", From 8b9d8ec0ccece7c0bfeec90326d457478605e1a9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 00:59:52 +1300 Subject: [PATCH 28/45] Widen newline tag detection --- src/m2d/converters/event-to-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 2d45756..1b23787 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -766,7 +766,7 @@ async function eventToMessage(event, guild, channel, di) { // Generate a reply preview for a standard message repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body repliedToContent = repliedToContent.replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards - repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line + repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) repliedToContent = repliedToContent.replace(/]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/) From c75e87f403612f2fd20d05a94f7b5d397181094f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 01:27:34 +1300 Subject: [PATCH 29/45] Stream files in serveStatic for lower memory use --- src/web/server.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/web/server.js b/src/web/server.js index dc13cf0..837e14d 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -83,7 +83,13 @@ function tryStatic(event, fallthrough) { // Everything else else { const mime = mimeTypes.lookup(id) - if (typeof mime === "string") defaultContentType(event, mime) + if (typeof mime === "string") { + if (mime.startsWith("text/")) { + defaultContentType(event, mime + "; charset=utf-8") // usually wise + } else { + defaultContentType(event, mime) + } + } return { size: stats.size } @@ -94,7 +100,7 @@ function tryStatic(event, fallthrough) { const path = join(publicDir, id) return pugSync.renderPath(event, path, {}) } else { - return fs.promises.readFile(join(publicDir, id)) + return fs.createReadStream(join(publicDir, id)) } } }) From 7367fb3b658647a41d7a97e8c26334c374028711 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 20 Mar 2026 01:37:22 +1300 Subject: [PATCH 30/45] Fix weird background clipping on icons --- src/web/pug/includes/template.pug | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 278a16a..86680eb 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -88,6 +88,12 @@ html(lang="en") --_ts-multiple-bg: var(--green-400); --_ts-multiple-fc: var(--white); } + .s-avatar { + --_av-bg: var(--white); + } + .s-avatar .s-avatar--letter { + color: var(--white); + } .s-btn__dropdown:has(+ :popover-open) { background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important; } From 201814e9f451966fce14a73ac2abdac24d6ef75a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 23 Mar 2026 21:22:33 +1300 Subject: [PATCH 31/45] Update dependencies --- src/agi/elizabot.js | 333 ++++++++++++++++++++++++++++++ src/agi/elizadata.js | 184 +++++++++++++++++ src/agi/generator.js | 55 +++++ src/agi/generator.test.js | 161 +++++++++++++++ src/agi/listener.js | 76 +++++++ src/d2m/actions/send-message.js | 4 + src/d2m/event-dispatcher.js | 7 +- src/db/migrations/0037-agi.sql | 25 +++ src/db/orm-defs.d.ts | 19 ++ src/web/pug/agi-optout.pug | 24 +++ src/web/pug/agi.pug | 41 ++++ src/web/pug/includes/template.pug | 3 +- src/web/routes/agi.js | 36 ++++ src/web/server.js | 1 + test/test.js | 1 + 15 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 src/agi/elizabot.js create mode 100644 src/agi/elizadata.js create mode 100644 src/agi/generator.js create mode 100644 src/agi/generator.test.js create mode 100644 src/agi/listener.js create mode 100644 src/db/migrations/0037-agi.sql create mode 100644 src/web/pug/agi-optout.pug create mode 100644 src/web/pug/agi.pug create mode 100644 src/web/routes/agi.js diff --git a/src/agi/elizabot.js b/src/agi/elizabot.js new file mode 100644 index 0000000..6a8e698 --- /dev/null +++ b/src/agi/elizabot.js @@ -0,0 +1,333 @@ +/* + --- + 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; + + synopsis: + new ElizaBot( ) + ElizaBot.prototype.transform( ) + 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; kb[1]) return -1 + else if (a[1]b[3]) return 1 + else if (a[3]\/\\\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=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; iri)) || (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)` + } +} + +module.exports._generateContent = generateContent +module.exports.generate = generate diff --git a/src/agi/generator.test.js b/src/agi/generator.test.js new file mode 100644 index 0000000..dfecfcf --- /dev/null +++ b/src/agi/generator.test.js @@ -0,0 +1,161 @@ +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." + ) +}) diff --git a/src/agi/listener.js b/src/agi/listener.js new file mode 100644 index 0000000..d707ede --- /dev/null +++ b/src/agi/listener.js @@ -0,0 +1,76 @@ +// @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 diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 8550d43..e9b7fae 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -23,6 +23,8 @@ 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 @@ -137,6 +139,8 @@ async function sendMessage(message, channel, guild, row) { } } + await agiListener.process(message, channel, guild, false) + return eventIDs } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c86cc13..b6593ec 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -40,6 +40,8 @@ const vote = sync.require("./actions/poll-vote") const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") /** @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() @@ -303,7 +305,10 @@ module.exports = { if (message.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: message.webhook_id}).pluck().get() - 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 (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 (dUtils.isEphemeralMessage(message)) return // Ephemeral messages are for the eyes of the receiver only! diff --git a/src/db/migrations/0037-agi.sql b/src/db/migrations/0037-agi.sql new file mode 100644 index 0000000..89e0a58 --- /dev/null +++ b/src/db/migrations/0037-agi.sql @@ -0,0 +1,25 @@ +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; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index d95bfc3..f6ae14a 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -1,4 +1,23 @@ 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 diff --git a/src/web/pug/agi-optout.pug b/src/web/pug/agi-optout.pug new file mode 100644 index 0000000..795e675 --- /dev/null +++ b/src/web/pug/agi-optout.pug @@ -0,0 +1,24 @@ +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 diff --git a/src/web/pug/agi.pug b/src/web/pug/agi.pug new file mode 100644 index 0000000..029c02a --- /dev/null +++ b/src/web/pug/agi.pug @@ -0,0 +1,41 @@ +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") diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug index 86680eb..be1d005 100644 --- a/src/web/pug/includes/template.pug +++ b/src/web/pug/includes/template.pug @@ -65,7 +65,8 @@ mixin define-themed-button(name, theme) doctype html html(lang="en") head - title Out Of Your Element + block title + title Out Of Your Element link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css")) //- Please use responsibly!!!!! diff --git a/src/web/routes/agi.js b/src/web/routes/agi.js new file mode 100644 index 0000000..f899455 --- /dev/null +++ b/src/web/routes/agi.js @@ -0,0 +1,36 @@ +// @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) +})) diff --git a/src/web/server.js b/src/web/server.js index 837e14d..85fa1cb 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -125,6 +125,7 @@ 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") diff --git a/test/test.js b/test/test.js index 4cd9627..70625a0 100644 --- a/test/test.js +++ b/test/test.js @@ -175,4 +175,5 @@ 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") })() From 5c9e569a2acb865c7252f17c10149bb22aabf384 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 15:29:18 +1300 Subject: [PATCH 32/45] Support channel follow messages --- src/d2m/converters/message-to-event.js | 11 ++++++++ src/d2m/converters/message-to-event.test.js | 13 +++++++++ test/data.js | 31 +++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 3f598f2..33d8696 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -357,6 +357,17 @@ async function messageToEvent(message, guild, options = {}, di) { }] } + if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) { + return [{ + $type: "m.room.message", + msgtype: "m.emote", + body: `set this room to receive announcements from ${message.content}`, + format: "org.matrix.custom.html", + formatted_body: tag`set this room to receive announcements from ${message.content}`, + "m.mentions": {} + }] + } + let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index c4b812d..97fc25d 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1142,6 +1142,19 @@ test("message2event: type 4 channel name change", async t => { }]) }) +test("message2event: type 12 channel follow add", async t => { + const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.emote", + body: "set this room to receive announcements from PluralKit #downtime", + format: "org.matrix.custom.html", + formatted_body: "set this room to receive announcements from PluralKit #downtime", + "m.mentions": {} + }]) +}) + test("message2event: thread start message reference", async t => { const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { api: { diff --git a/test/data.js b/test/data.js index 45e0388..f5e8313 100644 --- a/test/data.js +++ b/test/data.js @@ -6170,6 +6170,37 @@ module.exports = { components: [], position: 12 }, + channel_follow_add: { + type: 12, + content: "PluralKit #downtime", + attachments: [], + embeds: [], + timestamp: "2026-03-24T23:16:04.097Z", + edited_timestamp: null, + flags: 0, + components: [], + id: "1486141581047369888", + channel_id: "1451125453082591314", + author: { + id: "154058479798059009", + username: "exaptations", + discriminator: "0", + avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80", + bot: false, + flags: 0, + globalName: "Exa", + }, + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + tts: false, + message_reference: { + type: 0, + channel_id: "1015204661701124206", + guild_id: "466707357099884544" + } + }, updated_to_start_thread_from_here: { t: "MESSAGE_UPDATE", s: 19, From d8c0a947f2dc118ce5255090619efe8070235395 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 15:39:26 +1300 Subject: [PATCH 33/45] Automatically reload registration --- src/matrix/read-registration.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js index 114bf75..86f99a1 100644 --- a/src/matrix/read-registration.js +++ b/src/matrix/read-registration.js @@ -78,6 +78,11 @@ function readRegistration() { /** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore let reg = readRegistration() +fs.watch(registrationFilePath, {persistent: false}, () => { + let newReg = readRegistration() + Object.assign(reg, newReg) +}) + module.exports.registrationFilePath = registrationFilePath module.exports.readRegistration = readRegistration module.exports.getTemplateRegistration = getTemplateRegistration From 41692b11ff53ce230a21998d9847839296b9b5c4 Mon Sep 17 00:00:00 2001 From: Bea Date: Fri, 20 Mar 2026 13:54:19 +0000 Subject: [PATCH 34/45] feat(m2d): support MSC4144 per-message profiles Override webhook username and avatar_url from m.per_message_profile (and unstable com.beeper.per_message_profile) when present. The stable key takes priority over the unstable prefix. --- src/m2d/converters/event-to-message.js | 4 + src/m2d/converters/event-to-message.test.js | 102 ++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 1b23787..7fdbb15 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -557,6 +557,10 @@ async function eventToMessage(event, guild, channel, di) { const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) + // Override display name and avatar from MSC4144 per-message profile if present + const perMessageProfile = event.content["m.per_message_profile"] || event.content["com.beeper.per_message_profile"] + if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname + if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 1c263b4..b283d82 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5526,6 +5526,108 @@ test("event2message: known and unknown emojis in the end are used for sprite she ) }) +test("event2message: m.per_message_profile overrides displayname and avatar_url", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello from a custom profile", + "m.per_message_profile": { + id: "custom-id", + displayname: "Custom Name", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Custom Name", + content: "hello from a custom profile", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: com.beeper.per_message_profile (unstable prefix) overrides displayname and avatar_url", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello from unstable profile", + "com.beeper.per_message_profile": { + id: "custom-id", + displayname: "Unstable Name", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Unstable Name", + content: "hello from unstable profile", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: m.per_message_profile takes priority over com.beeper.per_message_profile", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "stable wins", + "m.per_message_profile": { + id: "stable-id", + displayname: "Stable Name" + }, + "com.beeper.per_message_profile": { + id: "unstable-id", + displayname: "Unstable Name" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Stable Name", + content: "stable wins", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: all unknown chess emojis are used for sprite sheet", async t => { t.deepEqual( await eventToMessage({ From a8b7d64e91c5927de2d179db89dc7396d876b3c5 Mon Sep 17 00:00:00 2001 From: Bea Date: Fri, 20 Mar 2026 14:04:13 +0000 Subject: [PATCH 35/45] feat(m2d): strip per-message profile fallbacks from message content Remove data-mx-profile-fallback elements from formatted_body and displayname prefix from plain body when per-message profile is used. --- src/m2d/converters/event-to-message.js | 8 +++ src/m2d/converters/event-to-message.test.js | 69 +++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 7fdbb15..96732ec 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -803,6 +803,10 @@ async function eventToMessage(event, guild, channel, di) { if (shouldProcessTextEvent) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body + if (perMessageProfile?.has_fallback) { + // Strip fallback elements added for clients that don't support per-message profiles + input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") + } if (event.content.msgtype === "m.emote") { input = `* ${displayName} ${input}` } @@ -948,6 +952,10 @@ async function eventToMessage(event, guild, channel, di) { } else { // Looks like we're using the plaintext body! content = event.content.body + if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) { + // Strip the display name prefix fallback added for clients that don't support per-message profiles + content = content.slice(perMessageProfile.displayname.length + 2) + } if (event.content.msgtype === "m.emote") { content = `* ${displayName} ${content}` diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b283d82..1c37b7a 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5628,6 +5628,75 @@ test("event2message: m.per_message_profile takes priority over com.beeper.per_me ) }) +test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "Tidus Herboren: one more test", + format: "org.matrix.custom.html", + formatted_body: "Tidus Herboren: one more test", + "com.beeper.per_message_profile": { + id: "tidus", + displayname: "Tidus Herboren", + avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + has_fallback: true + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Tidus Herboren", + content: "one more test", + avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "Tidus Herboren: one more test", + "com.beeper.per_message_profile": { + id: "tidus", + displayname: "Tidus Herboren", + has_fallback: true + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "Tidus Herboren", + content: "one more test", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: all unknown chess emojis are used for sprite sheet", async t => { t.deepEqual( await eventToMessage({ From 07ec9832b2ada2b054a11209b11e8654ac1b4092 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:39 +0000 Subject: [PATCH 36/45] fix(m2d): only use unstable com.beeper.per_message_profile prefix --- src/m2d/converters/event-to-message.js | 2 +- src/m2d/converters/event-to-message.test.js | 71 +-------------------- 2 files changed, 2 insertions(+), 71 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 96732ec..5b7d0f4 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -558,7 +558,7 @@ async function eventToMessage(event, guild, channel, di) { if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) // Override display name and avatar from MSC4144 per-message profile if present - const perMessageProfile = event.content["m.per_message_profile"] || event.content["com.beeper.per_message_profile"] + const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) // If the display name is too long to be put into the webhook (80 characters is the maximum), diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 1c37b7a..2a204e9 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5526,40 +5526,7 @@ test("event2message: known and unknown emojis in the end are used for sprite she ) }) -test("event2message: m.per_message_profile overrides displayname and avatar_url", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: "hello from a custom profile", - "m.per_message_profile": { - id: "custom-id", - displayname: "Custom Name", - avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo" - } - }, - event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "Custom Name", - content: "hello from a custom profile", - avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo", - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - -test("event2message: com.beeper.per_message_profile (unstable prefix) overrides displayname and avatar_url", async t => { +test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => { t.deepEqual( await eventToMessage({ type: "m.room.message", @@ -5592,42 +5559,6 @@ test("event2message: com.beeper.per_message_profile (unstable prefix) overrides ) }) -test("event2message: m.per_message_profile takes priority over com.beeper.per_message_profile", async t => { - t.deepEqual( - await eventToMessage({ - type: "m.room.message", - sender: "@cadence:cadence.moe", - content: { - msgtype: "m.text", - body: "stable wins", - "m.per_message_profile": { - id: "stable-id", - displayname: "Stable Name" - }, - "com.beeper.per_message_profile": { - id: "unstable-id", - displayname: "Unstable Name" - } - }, - event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", - room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" - }), - { - ensureJoined: [], - messagesToDelete: [], - messagesToEdit: [], - messagesToSend: [{ - username: "Stable Name", - content: "stable wins", - avatar_url: undefined, - allowed_mentions: { - parse: ["users", "roles"] - } - }] - } - ) -}) - test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { t.deepEqual( await eventToMessage({ From 0b513b7ee07341fd5ed09bb7787c9a7250e75c88 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:40 +0000 Subject: [PATCH 37/45] fix(m2d): implement MSC4144 avatar clearing algorithm - Empty string "" -> undefined (Discord uses default avatar) - Valid MXC URI -> convert to public URL - Omitted/null -> keep member avatar --- src/m2d/converters/event-to-message.js | 12 ++++++-- src/m2d/converters/event-to-message.test.js | 33 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 5b7d0f4..7c233c7 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -557,10 +557,18 @@ async function eventToMessage(event, guild, channel, di) { const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) - // Override display name and avatar from MSC4144 per-message profile if present + // MSC4144: Override display name and avatar from per-message profile if present const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname - if (perMessageProfile?.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + if (perMessageProfile && "avatar_url" in perMessageProfile) { + if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar (use default) + avatarURL = undefined + } else if (perMessageProfile.avatar_url) { + // omitted/null falls back to member avatar + avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } + } // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 2a204e9..bc73df7 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -5559,6 +5559,39 @@ test("event2message: com.beeper.per_message_profile overrides displayname and av ) }) +test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "hello with cleared avatar", + "com.beeper.per_message_profile": { + id: "no-avatar", + displayname: "No Avatar User", + avatar_url: "" + } + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "No Avatar User", + content: "hello with cleared avatar", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => { t.deepEqual( await eventToMessage({ From 8224ed53410d1a410bf6a60c9f6f203067a3c4b5 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 16:45:40 +0000 Subject: [PATCH 38/45] feat(discord): show per-message profile info in matrix info command --- src/discord/interactions/matrix-info.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index c85cec2..f5aa539 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -62,7 +62,20 @@ async function _interact({guild_id, data}, {api}) { .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()) const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() - const name = matrixMember?.displayname || event.sender + // Check for per-message profile + const perMessageProfile = event.content?.["com.beeper.per_message_profile"] + let name = matrixMember?.displayname || event.sender + let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url) + let profileNote = "" + if (perMessageProfile) { + if (perMessageProfile.displayname) { + name = perMessageProfile.displayname + } + if ("avatar_url" in perMessageProfile) { + avatar = perMessageProfile.avatar_url ? utils.getPublicUrlForMxc(perMessageProfile.avatar_url) : undefined + } + profileNote = " (sent with a per-message profile)" + } return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { @@ -70,9 +83,9 @@ async function _interact({guild_id, data}, {api}) { author: { name, url: `https://matrix.to/#/${event.sender}`, - icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url) + icon_url: avatar }, - description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →]()\n\n**User ID**: [${event.sender}]()`, + description: `This Matrix message was delivered to Discord by **Out Of Your Element**${profileNote}.\n[View on Matrix →]()\n\n**User ID**: [${event.sender}]()`, color: 0x0dbd8b, fields: [{ name: "In Channels", From f742d8572a1b3b6a6457b5c4addfede97f4a8dab Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:10:15 +1300 Subject: [PATCH 39/45] MSC4144 minor changes for merge --- src/discord/interactions/matrix-info.js | 19 ++- src/discord/interactions/matrix-info.test.js | 115 +++++++++++++++++++ src/m2d/converters/event-to-message.js | 23 +++- 3 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/discord/interactions/matrix-info.js b/src/discord/interactions/matrix-info.js index f5aa539..0b9a525 100644 --- a/src/discord/interactions/matrix-info.js +++ b/src/discord/interactions/matrix-info.js @@ -62,20 +62,29 @@ async function _interact({guild_id, data}, {api}) { .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()) const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get() - // Check for per-message profile - const perMessageProfile = event.content?.["com.beeper.per_message_profile"] let name = matrixMember?.displayname || event.sender let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url) + + // Check for per-message profile + const perMessageProfile = event.content?.["com.beeper.per_message_profile"] let profileNote = "" if (perMessageProfile) { if (perMessageProfile.displayname) { name = perMessageProfile.displayname } if ("avatar_url" in perMessageProfile) { - avatar = perMessageProfile.avatar_url ? utils.getPublicUrlForMxc(perMessageProfile.avatar_url) : undefined + if (perMessageProfile.avatar_url) { + // use provided avatar_url + avatar = utils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } else if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar + avatar = undefined + } + // else, omitted/null falls back to member avatar } - profileNote = " (sent with a per-message profile)" + profileNote = "Sent with a per-message profile.\n" } + return { type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, data: { @@ -85,7 +94,7 @@ async function _interact({guild_id, data}, {api}) { url: `https://matrix.to/#/${event.sender}`, icon_url: avatar }, - description: `This Matrix message was delivered to Discord by **Out Of Your Element**${profileNote}.\n[View on Matrix →]()\n\n**User ID**: [${event.sender}]()`, + description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →]()\n\n${profileNote}**User ID**: [${event.sender}]()`, color: 0x0dbd8b, fields: [{ name: "In Channels", diff --git a/src/discord/interactions/matrix-info.test.js b/src/discord/interactions/matrix-info.test.js index f455700..8347c12 100644 --- a/src/discord/interactions/matrix-info.test.js +++ b/src/discord/interactions/matrix-info.test.js @@ -85,3 +85,118 @@ test("matrix info: shows info for matrix source message", async t => { ) t.equal(called, 1) }) + +test("matrix info: shows username for per-message profile", async t => { + let called = 0 + const msg = await _interact({ + data: { + target_id: "1128118177155526666", + resolved: { + messages: { + "1141501302736695316": data.message.simple_reply_to_matrix_user + } + } + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + event_id: eventID, + room_id: roomID, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "master chief: i like the halo", + format: "org.matrix.custom.html", + formatted_body: "master chief: i like the halo", + "com.beeper.per_message_profile": { + has_fallback: true, + displayname: "master chief", + avatar_url: "" + } + }, + sender: "@cadence:cadence.moe" + } + }, + async getJoinedMembers(roomID) { + return { + joined: {} + } + }, + async getStateEventOuter(roomID, type, key) { + return { + content: { + room_version: "11" + } + } + }, + async getStateEvent(roomID, type, key) { + return {} + } + } + }) + t.equal(msg.data.embeds[0].author.name, "master chief") + t.match(msg.data.embeds[0].description, "Sent with a per-message profile") + t.equal(called, 1) +}) + +test("matrix info: shows avatar for per-message profile", async t => { + let called = 0 + const msg = await _interact({ + data: { + target_id: "1128118177155526666", + resolved: { + messages: { + "1141501302736695316": data.message.simple_reply_to_matrix_user + } + } + }, + guild_id: "112760669178241024" + }, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + return { + event_id: eventID, + room_id: roomID, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "?", + format: "org.matrix.custom.html", + formatted_body: "?", + "com.beeper.per_message_profile": { + avatar_url: "mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc" + } + }, + sender: "@mystery:cadence.moe" + } + }, + async getJoinedMembers(roomID) { + return { + joined: {} + } + }, + async getStateEventOuter(roomID, type, key) { + return { + content: { + room_version: "11" + } + } + }, + async getStateEvent(roomID, type, key) { + return {} + } + } + }) + t.equal(msg.data.embeds[0].author.name, "@mystery:cadence.moe") + t.equal(msg.data.embeds[0].author.icon_url, "https://bridge.example.org/download/matrix/cadence.moe/HXfFuougamkURPPMflTJRxGc") + t.match(msg.data.embeds[0].description, "Sent with a per-message profile") + t.equal(called, 1) +}) diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 7c233c7..95e477f 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -550,25 +550,30 @@ async function eventToMessage(event, guild, channel, di) { /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" + // Extract a basic display name from the sender const match = event.sender.match(/^@(.*?):/) if (match) displayName = match[1] + // Try to extract an accurate display name and avatar URL from the member event const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) + // MSC4144: Override display name and avatar from per-message profile if present const perMessageProfile = event.content["com.beeper.per_message_profile"] if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname if (perMessageProfile && "avatar_url" in perMessageProfile) { - if (perMessageProfile.avatar_url === "") { - // empty string avatar_url clears the avatar (use default) - avatarURL = undefined - } else if (perMessageProfile.avatar_url) { - // omitted/null falls back to member avatar + if (perMessageProfile.avatar_url) { + // use provided avatar_url avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url) + } else if (perMessageProfile.avatar_url === "") { + // empty string avatar_url clears the avatar + avatarURL = undefined } + // else, omitted/null falls back to member avatar } + // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) @@ -812,7 +817,13 @@ async function eventToMessage(event, guild, channel, di) { if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { let input = event.content.formatted_body if (perMessageProfile?.has_fallback) { - // Strip fallback elements added for clients that don't support per-message profiles + // Strip fallback elements added for clients that don't support per-message profiles. + // Deviates from recommended regexp in MSC to be less strict. Avoiding an HTML parser for performance reasons. + // ┌────A────┐ Opening HTML tag: capture tag name and stay within tag + // ┆ ┆┌─────────────B────────────┐ This text in the tag somewhere, presumably an attribute name + // ┆ ┆┆ ┆┌─C──┐ Rest of the opening tag + // ┆ ┆┆ ┆┆ ┆┌─D─┐ Tag content (no more tags allowed within) + // ┆ ┆┆ ┆┆ ┆┆ ┆┌─E──┐ Closing tag matching opening tag name input = input.replace(/<(\w+)[^>]*\bdata-mx-profile-fallback\b[^>]*>[^<]*<\/\1>/g, "") } if (event.content.msgtype === "m.emote") { From e9fe8206660b4aeda9344dd1f22415b12a75b011 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:22:37 +1300 Subject: [PATCH 40/45] Registration changes should be instant now --- scripts/reset-web-password.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/reset-web-password.js b/scripts/reset-web-password.js index 9131efb..7c3a1a2 100644 --- a/scripts/reset-web-password.js +++ b/scripts/reset-web-password.js @@ -13,5 +13,5 @@ const {prompt} = require("enquirer") reg.ooye.web_password = passwordResponse.web_password writeRegistration(reg) - console.log("Saved. Restart Out Of Your Element to apply this change.") + console.log("Saved. This change should be applied instantly.") })() From 8c023cc9361069afbe21ae1d688cac3d1ac2427c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 25 Mar 2026 16:24:07 +1300 Subject: [PATCH 41/45] Add ping() function to REPL --- src/stdin.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/stdin.js b/src/stdin.js index fea5fad..2548d42 100644 --- a/src/stdin.js +++ b/src/stdin.js @@ -23,10 +23,26 @@ const setPresence = sync.require("./d2m/actions/set-presence") const channelWebhook = sync.require("./m2d/actions/channel-webhook") const guildID = "112760669178241024" +async function ping() { + const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) + if (result.ok) { + return "Ping OK. The homeserver and OOYE are talking to each other fine." + } else { + if (typeof result.root === "string") { + var msg = `Cannot reach homeserver: ${result.root}` + } else if (result.root.error) { + var msg = `Homeserver said: [${result.status}] ${result.root.error}` + } else { + var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}` + } + return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again." + } +} + if (process.stdin.isTTY) { setImmediate(() => { if (!passthrough.repl) { - const cli = repl.start({ prompt: "", eval: customEval, writer: s => s }) + const cli = repl.start({prompt: "", eval: customEval, writer: s => s}) Object.assign(cli.context, passthrough) passthrough.repl = cli } From 953b3e7741922fa801dea54a068ee5ed40e389bc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 26 Mar 2026 00:16:30 +1300 Subject: [PATCH 42/45] Attach message to error Apparently this was causing detached logs, so just stop those complaints if the error isn't being bubbled --- src/d2m/actions/expression.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/d2m/actions/expression.js b/src/d2m/actions/expression.js index c7ab27a..0f714c6 100644 --- a/src/d2m/actions/expression.js +++ b/src/d2m/actions/expression.js @@ -34,7 +34,10 @@ async function emojisToState(emojis, guild) { if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit. return } - console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`) + e["emoji"] = { + name: emoji.name, + id: emoji.id + } throw e }) )) From c423590292e836621d9a1bba09edccc646e892ab Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 10 Mar 2026 23:23:23 +0000 Subject: [PATCH 43/45] Look up thread IDs if channels cannot be found --- scripts/backfill.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/backfill.js b/scripts/backfill.js index 941e803..7135ec2 100644 --- a/scripts/backfill.js +++ b/scripts/backfill.js @@ -51,10 +51,24 @@ const preparedInsert = backfill.prepare("INSERT INTO backfill (channel_id, messa async function event(event) { if (event.t !== "GUILD_CREATE") return - const channel = event.d.channels.find(c => c.id === channelID) - if (!channel) return + let channel = event.d.channels.find(c => c.id === channelID) || (event.d.threads || []).find(c => c.id === channelID) const guild_id = event.d.id + if (!channel) { + // May be an archived thread not present in GUILD_CREATE data - try fetching via API + try { + const fetched = await discord.snow.channel.getChannel(channelID) + if (!fetched.guild_id || fetched.guild_id !== guild_id) return + fetched.guild_id = guild_id + discord.channels.set(fetched.id, fetched) + channel = fetched + } catch (e) { + return + } + } + + if (!channel) return + try { await createRoom.syncRoom(channelID) let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0" From a01a325146c4e97706e2c233a80e63043881c388 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 18:19:15 +0000 Subject: [PATCH 44/45] chore: editorconfig matching existing code format styles An editor config has been written that matches existing indentation and formatting used by this project, which should extend the courtesy of intended tab use to other editors like NeoVim which are often conigured to adhere to `.editorconfig` by default. As part of this change, the official EditorConfig extension has been added to VSCode recommendations, which should additionally enable Code-like editors to additionally adhere to the rules of other files like json and markdown which are the only types that deviate from Cadence's global default. --- .editorconfig | 16 ++++++++++++++++ .vscode/extensions.json | 5 +++++ 2 files changed, 21 insertions(+) create mode 100644 .editorconfig create mode 100644 .vscode/extensions.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8920fb1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = tab +tab_width = 3 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..fd5bdfe --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "editorconfig.editorconfig" + ] +} From 4625544e34e26e2ab6d3fe9a03a63cf39eb2aa21 Mon Sep 17 00:00:00 2001 From: Bea Date: Tue, 24 Mar 2026 19:00:41 +0000 Subject: [PATCH 45/45] chore: add global prettier ignore file As prettier is not configured for this project, and the style doesn't meet prettier's opinionated defaults, it's worth dropping this blanket ignore until such a time (if ever) a prettier config is produced that matches the styleguide of this project. --- .prettierignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*