diff --git a/package-lock.json b/package-lock.json
index 9847400..e154da1 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,9 +36,9 @@
"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",
+ "try-to-catch": "^4.0.5",
"uqr": "^0.1.2",
"xxhash-wasm": "^1.0.2",
"zod": "^4.0.17"
@@ -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",
@@ -2818,16 +2756,6 @@
"node": ">=22"
}
},
- "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==",
- "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",
@@ -2903,17 +2831,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": {
@@ -2976,11 +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": {
@@ -3185,9 +3115,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..0d0c2b6 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,9 +45,9 @@
"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",
+ "try-to-catch": "^4.0.5",
"uqr": "^0.1.2",
"xxhash-wasm": "^1.0.2",
"zod": "^4.0.17"
@@ -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"
},
diff --git a/scripts/backfill.js b/scripts/backfill.js
index 27600f0..c0c440e 100644
--- a/scripts/backfill.js
+++ b/scripts/backfill.js
@@ -38,12 +38,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,6 +56,18 @@ async function event(event) {
if (!channel) return
const guild_id = event.d.id
+ let roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
+ if (!roomID) {
+ console.log(`Channel #${channel.name} is not bridged yet. Attempting to auto-create...`)
+ try {
+ roomID = await createRoom.syncRoom(channelID)
+ console.log(`Successfully bridged to new room: ${roomID}`)
+ } catch (e) {
+ console.error(`Failed to auto-create room: ${e.message}`)
+ process.exit(1)
+ }
+ }
+
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}`)
diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js
index 7f77b81..81c821f 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)
}
@@ -769,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
@@ -905,11 +917,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/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/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/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/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/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}.`
+ })
+ }
+ )
}]
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/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/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/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.js b/src/web/routes/guild-settings.js
index 63dd3ec..ae52825 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, `/guild?guild_id=${guildID}`, 302)
+ }
+}))
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")
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})