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 9847400..7b27322 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,20 +10,21 @@
"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",
"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 +37,8 @@
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
- "snowtransfer": "^0.17.1",
- "stream-mime-type": "^1.0.2",
- "try-to-catch": "^3.0.1",
+ "snowtransfer": "^0.17.5",
+ "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"
},
@@ -68,6 +68,23 @@
"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",
@@ -137,9 +154,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": {
@@ -156,9 +173,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"
@@ -178,9 +195,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",
@@ -246,6 +263,15 @@
"pug-error": "^2.1.0"
}
},
+ "node_modules/@cloudrac3r/stream-type": {
+ "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",
"resolved": "https://registry.npmjs.org/@cloudrac3r/tap-dot/-/tap-dot-2.0.3.tgz",
@@ -765,21 +791,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"
}
@@ -1036,11 +1053,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",
@@ -1048,9 +1060,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": {
@@ -1085,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",
@@ -1132,10 +1153,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",
@@ -1211,34 +1236,25 @@
"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": "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 +1263,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 +1272,7 @@
"c8": "bin/c8.js"
},
"engines": {
- "node": ">=18"
+ "node": "20 || >=22"
},
"peerDependencies": {
"monocart-coverage-reports": "^2"
@@ -1348,35 +1364,14 @@
"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.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 +1512,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"
@@ -1561,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",
@@ -1617,22 +1593,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",
@@ -1655,10 +1615,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",
@@ -1731,66 +1692,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 +1886,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 +1920,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 +1943,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 +1998,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 +2022,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 +2104,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 +2129,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,24 +2139,12 @@
"minipass": "^7.1.2"
},
"engines": {
- "node": "20 || >=22"
+ "node": "18 || 20 || >=22"
},
"funding": {
"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",
@@ -2425,25 +2312,11 @@
"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": {
+ "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",
@@ -2648,12 +2521,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"
@@ -2688,30 +2561,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",
@@ -2734,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"
},
@@ -2763,26 +2603,10 @@
"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.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 +2624,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 +2643,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",
@@ -2889,52 +2704,19 @@
"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": "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_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": "20 || >=22"
}
},
"node_modules/timer-node": {
@@ -2949,22 +2731,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",
@@ -2976,11 +2742,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": {
@@ -3099,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",
@@ -3185,9 +2931,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..0e666aa 100644
--- a/package.json
+++ b/package.json
@@ -19,20 +19,21 @@
},
"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",
"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 +46,8 @@
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
- "snowtransfer": "^0.17.1",
- "stream-mime-type": "^1.0.2",
- "try-to-catch": "^3.0.1",
+ "snowtransfer": "^0.17.5",
+ "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..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
+ }
}
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: '* @cadence asked
, I respond: Stop drinking paint. (No)
Hit 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: '
@cadence asked
, I respond: Stop drinking paint. (No)
Hit 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 7f77b81..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
@@ -519,29 +542,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,
@@ -582,7 +636,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)
}
@@ -734,42 +789,30 @@ 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)
}
// 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
@@ -815,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
@@ -905,11 +940,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} `)
}
}
@@ -964,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)
@@ -1030,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)}`)
@@ -1039,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: "
", + "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 1a73aea..c4b812d 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,36 @@ 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: `" + + "⏺️ 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
public @Nullable EntityType<?>`
+ }])
+})
+
+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({
@@ -1007,7 +1037,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 ☆`
}])
@@ -1084,7 +1114,7 @@ test("message2event: multiple attachments are combined into the same event where
formatted_body: "hey"
+ `📸 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)` - + `