m->d: fix image captions spec, fix upload issues
This commit is contained in:
@@ -89,14 +89,14 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin
|
|||||||
|
|
||||||
# Dependency justification
|
# Dependency justification
|
||||||
|
|
||||||
Total transitive production dependencies: 139
|
Total transitive production dependencies: 137
|
||||||
|
|
||||||
### <font size="+2">🦕</font>
|
### <font size="+2">🦕</font>
|
||||||
|
|
||||||
* (31) better-sqlite3: SQLite3 is the best database, and this is the best library for it.
|
* (31) better-sqlite3: SQLite3 is the best database, and this is the best library for it.
|
||||||
* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
||||||
* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only.
|
* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only.
|
||||||
* (10) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, and more. 14 transitive dependencies is on the low end for a web server.
|
* (9) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, and more. 14 transitive dependencies is on the low end for a web server.
|
||||||
* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets.
|
* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets.
|
||||||
|
|
||||||
### <font size="-1">🪱</font>
|
### <font size="-1">🪱</font>
|
||||||
@@ -118,7 +118,6 @@ Total transitive production dependencies: 139
|
|||||||
* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||||
* (0) entities: Looks fine. No dependencies.
|
* (0) entities: Looks fine. No dependencies.
|
||||||
* (0) get-relative-path: Looks fine. No dependencies.
|
* (0) get-relative-path: Looks fine. No dependencies.
|
||||||
* (0) get-stream: Only needed if content_length_workaround is true.
|
|
||||||
* (1) heatsync: Module hot-reloader that I trust.
|
* (1) heatsync: Module hot-reloader that I trust.
|
||||||
* (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON.
|
* (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON.
|
||||||
* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used.
|
* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used.
|
||||||
|
46
package-lock.json
generated
46
package-lock.json
generated
@@ -29,8 +29,7 @@
|
|||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"entities": "^5.0.0",
|
"entities": "^5.0.0",
|
||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"get-stream": "^6.0.1",
|
"h3": "^1.15.1",
|
||||||
"h3": "^1.12.0",
|
|
||||||
"heatsync": "^2.7.2",
|
"heatsync": "^2.7.2",
|
||||||
"htmx.org": "^2.0.4",
|
"htmx.org": "^2.0.4",
|
||||||
"lru-cache": "^10.4.3",
|
"lru-cache": "^10.4.3",
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudrac3r/tap-dot": "^2.0.3",
|
"@cloudrac3r/tap-dot": "^2.0.3",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^20.17.19",
|
||||||
"c8": "^10.1.2",
|
"c8": "^10.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"supertape": "^10.4.0"
|
"supertape": "^10.4.0"
|
||||||
@@ -1076,13 +1075,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.19.76",
|
"version": "20.17.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
|
||||||
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
|
"integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
@@ -1795,17 +1794,6 @@
|
|||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-stream": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/github-from-package": {
|
"node_modules/github-from-package": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
@@ -1833,9 +1821,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/h3": {
|
"node_modules/h3": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz",
|
||||||
"integrity": "sha512-OsjX4JW8J4XGgCgEcad20pepFQWnuKH+OwkCJjogF3C+9AZ1iYdtB4hX6vAb5DskBiu5ljEXqApINjR8CqoCMQ==",
|
"integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.2",
|
||||||
"crossws": "^0.3.3",
|
"crossws": "^0.3.3",
|
||||||
@@ -1843,7 +1832,6 @@
|
|||||||
"destr": "^2.0.3",
|
"destr": "^2.0.3",
|
||||||
"iron-webcrypto": "^1.2.1",
|
"iron-webcrypto": "^1.2.1",
|
||||||
"node-mock-http": "^1.0.0",
|
"node-mock-http": "^1.0.0",
|
||||||
"ohash": "^1.1.4",
|
|
||||||
"radix3": "^1.1.2",
|
"radix3": "^1.1.2",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
@@ -2197,11 +2185,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ohash": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="
|
|
||||||
},
|
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -3115,10 +3098,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uqr": {
|
"node_modules/uqr": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
|
@@ -38,8 +38,7 @@
|
|||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"entities": "^5.0.0",
|
"entities": "^5.0.0",
|
||||||
"get-relative-path": "^1.0.2",
|
"get-relative-path": "^1.0.2",
|
||||||
"get-stream": "^6.0.1",
|
"h3": "^1.15.1",
|
||||||
"h3": "^1.12.0",
|
|
||||||
"heatsync": "^2.7.2",
|
"heatsync": "^2.7.2",
|
||||||
"htmx.org": "^2.0.4",
|
"htmx.org": "^2.0.4",
|
||||||
"lru-cache": "^10.4.3",
|
"lru-cache": "^10.4.3",
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudrac3r/tap-dot": "^2.0.3",
|
"@cloudrac3r/tap-dot": "^2.0.3",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^20.17.19",
|
||||||
"c8": "^10.1.2",
|
"c8": "^10.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"supertape": "^10.4.0"
|
"supertape": "^10.4.0"
|
||||||
@@ -66,6 +65,6 @@
|
|||||||
"addbot": "node addbot.js",
|
"addbot": "node addbot.js",
|
||||||
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
|
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
|
||||||
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
||||||
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/matrix/mreq.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -315,7 +315,7 @@ function getUserOrProxyOwnerID(mxid) {
|
|||||||
* At the time of this executing, we know what the end of message emojis are, and we know that at least one of them is unknown.
|
* At the time of this executing, we know what the end of message emojis are, and we know that at least one of them is unknown.
|
||||||
* This function will strip them from the content and generate the correct pending file of the sprite sheet.
|
* This function will strip them from the content and generate the correct pending file of the sprite sheet.
|
||||||
* @param {string} content
|
* @param {string} content
|
||||||
* @param {{id: string, name: string}[]} attachments
|
* @param {{id: string, filename: string}[]} attachments
|
||||||
* @param {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles
|
* @param {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles
|
||||||
* @param {(mxc: string) => Promise<Buffer | undefined>} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock.
|
* @param {(mxc: string) => Promise<Buffer | undefined>} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock.
|
||||||
*/
|
*/
|
||||||
@@ -329,9 +329,9 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles,
|
|||||||
// Create a sprite sheet of known and unknown emojis from the end of the message
|
// Create a sprite sheet of known and unknown emojis from the end of the message
|
||||||
const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis, mxcDownloader)
|
const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis, mxcDownloader)
|
||||||
// Attach it
|
// Attach it
|
||||||
const name = "emojis.png"
|
const filename = "emojis.png"
|
||||||
attachments.push({id: String(attachments.length), name})
|
attachments.push({id: String(attachments.length), filename})
|
||||||
pendingFiles.push({name, buffer})
|
pendingFiles.push({name: filename, buffer})
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +486,7 @@ async function eventToMessage(event, guild, di) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content = event.content.body // ultimate fallback
|
let content = event.content.body // ultimate fallback
|
||||||
|
/** @type {{id: string, filename: string}[]} */
|
||||||
const attachments = []
|
const attachments = []
|
||||||
/** @type {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */
|
/** @type {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */
|
||||||
const pendingFiles = []
|
const pendingFiles = []
|
||||||
@@ -493,7 +494,45 @@ async function eventToMessage(event, guild, di) {
|
|||||||
const ensureJoined = []
|
const ensureJoined = []
|
||||||
|
|
||||||
// Convert content depending on what the message is
|
// Convert content depending on what the message is
|
||||||
if (event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")) {
|
// Handle images first - might need to handle their `body`/`formatted_body` as well, which will fall through to the text processor
|
||||||
|
let shouldProcessTextEvent = event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")
|
||||||
|
if (event.type === "m.room.message" && (event.content.msgtype === "m.file" || event.content.msgtype === "m.video" || event.content.msgtype === "m.audio" || event.content.msgtype === "m.image")) {
|
||||||
|
content = ""
|
||||||
|
const filename = event.content.filename || event.content.body
|
||||||
|
if ("url" in event.content) {
|
||||||
|
// Unencrypted
|
||||||
|
attachments.push({id: "0", filename})
|
||||||
|
pendingFiles.push({name: filename, mxc: event.content.url})
|
||||||
|
} else {
|
||||||
|
// Encrypted
|
||||||
|
assert.equal(event.content.file.key.alg, "A256CTR")
|
||||||
|
attachments.push({id: "0", filename})
|
||||||
|
pendingFiles.push({name: filename, mxc: event.content.file.url, key: event.content.file.key.k, iv: event.content.file.iv})
|
||||||
|
}
|
||||||
|
// Check if we also need to process a text event for this image - if it has a caption that's different from its filename
|
||||||
|
if ((event.content.body && event.content.filename && event.content.body !== event.content.filename) || event.content.formatted_body) {
|
||||||
|
shouldProcessTextEvent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.type === "m.sticker") {
|
||||||
|
content = ""
|
||||||
|
let filename = event.content.body
|
||||||
|
if (event.type === "m.sticker") {
|
||||||
|
let mimetype
|
||||||
|
if (event.content.info?.mimetype?.includes("/")) {
|
||||||
|
mimetype = event.content.info.mimetype
|
||||||
|
} else {
|
||||||
|
const res = await di.api.getMedia(event.content.url, {method: "HEAD"})
|
||||||
|
if (res.status === 200) {
|
||||||
|
mimetype = res.headers.get("content-type")
|
||||||
|
}
|
||||||
|
if (!mimetype) throw new Error(`Server error ${res.status} or missing content-type while detecting sticker mimetype`)
|
||||||
|
}
|
||||||
|
filename += "." + mimetype.split("/")[1]
|
||||||
|
}
|
||||||
|
attachments.push({id: "0", filename})
|
||||||
|
pendingFiles.push({name: filename, mxc: event.content.url})
|
||||||
|
} else if (shouldProcessTextEvent) {
|
||||||
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
|
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
|
||||||
// this event ---is an edit of--> original event ---is a reply to--> past event
|
// this event ---is an edit of--> original event ---is a reply to--> past event
|
||||||
await (async () => {
|
await (async () => {
|
||||||
@@ -780,40 +819,6 @@ async function eventToMessage(event, guild, di) {
|
|||||||
// @ts-ignore bad type from turndown
|
// @ts-ignore bad type from turndown
|
||||||
content = turndownService.escape(content)
|
content = turndownService.escape(content)
|
||||||
}
|
}
|
||||||
} else if (event.type === "m.room.message" && (event.content.msgtype === "m.file" || event.content.msgtype === "m.video" || event.content.msgtype === "m.audio" || event.content.msgtype === "m.image")) {
|
|
||||||
content = ""
|
|
||||||
const filename = event.content.filename || event.content.body
|
|
||||||
// A written `event.content.body` will be bridged to Discord's image `description` which is like alt text.
|
|
||||||
// Bridging as description rather than message content in order to match Matrix clients (Element, Neochat) which treat this as alt text or title text.
|
|
||||||
const description = (event.content.body !== event.content.filename && event.content.filename && event.content.body) || undefined
|
|
||||||
if ("url" in event.content) {
|
|
||||||
// Unencrypted
|
|
||||||
attachments.push({id: "0", description, filename})
|
|
||||||
pendingFiles.push({name: filename, mxc: event.content.url})
|
|
||||||
} else {
|
|
||||||
// Encrypted
|
|
||||||
assert.equal(event.content.file.key.alg, "A256CTR")
|
|
||||||
attachments.push({id: "0", description, filename})
|
|
||||||
pendingFiles.push({name: filename, mxc: event.content.file.url, key: event.content.file.key.k, iv: event.content.file.iv})
|
|
||||||
}
|
|
||||||
} else if (event.type === "m.sticker") {
|
|
||||||
content = ""
|
|
||||||
let filename = event.content.body
|
|
||||||
if (event.type === "m.sticker") {
|
|
||||||
let mimetype
|
|
||||||
if (event.content.info?.mimetype?.includes("/")) {
|
|
||||||
mimetype = event.content.info.mimetype
|
|
||||||
} else {
|
|
||||||
const res = await di.api.getMedia(event.content.url, {method: "HEAD"})
|
|
||||||
if (res.status === 200) {
|
|
||||||
mimetype = res.headers.get("content-type")
|
|
||||||
}
|
|
||||||
if (!mimetype) throw new Error(`Server error ${res.status} or missing content-type while detecting sticker mimetype`)
|
|
||||||
}
|
|
||||||
filename += "." + mimetype.split("/")[1]
|
|
||||||
}
|
|
||||||
attachments.push({id: "0", filename})
|
|
||||||
pendingFiles.push({name: filename, mxc: event.content.url})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content = displayNameRunoff + replyLine + content
|
content = displayNameRunoff + replyLine + content
|
||||||
|
@@ -3770,7 +3770,7 @@ test("event2message: text attachments work", async t => {
|
|||||||
username: "cadence [they]",
|
username: "cadence [they]",
|
||||||
content: "",
|
content: "",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
attachments: [{id: "0", description: undefined, filename: "chiki-powerups.txt"}],
|
attachments: [{id: "0", filename: "chiki-powerups.txt"}],
|
||||||
pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}]
|
pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -3806,14 +3806,14 @@ test("event2message: image attachments work", async t => {
|
|||||||
username: "cadence [they]",
|
username: "cadence [they]",
|
||||||
content: "",
|
content: "",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
attachments: [{id: "0", description: undefined, filename: "cool cat.png"}],
|
attachments: [{id: "0", filename: "cool cat.png"}],
|
||||||
pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
|
pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event2message: image attachments can have a custom description", async t => {
|
test("event2message: image attachments can have a plaintext caption", async t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
await eventToMessage({
|
await eventToMessage({
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
@@ -3840,10 +3840,62 @@ test("event2message: image attachments can have a custom description", async t =
|
|||||||
messagesToEdit: [],
|
messagesToEdit: [],
|
||||||
messagesToSend: [{
|
messagesToSend: [{
|
||||||
username: "cadence [they]",
|
username: "cadence [they]",
|
||||||
content: "",
|
content: "Cat emoji surrounded by pink hearts",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
attachments: [{id: "0", description: "Cat emoji surrounded by pink hearts", filename: "cool cat.png"}],
|
attachments: [{id: "0", filename: "cool cat.png"}],
|
||||||
pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
|
pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}],
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["users", "roles"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("event2message: image attachments can have a formatted caption", async t => {
|
||||||
|
t.deepEqual(
|
||||||
|
await eventToMessage({
|
||||||
|
content: {
|
||||||
|
body: "this event has `formatting`",
|
||||||
|
filename: "5740.jpg",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "this event has <code>formatting</code>",
|
||||||
|
info: {
|
||||||
|
h: 1340,
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
size: 226689,
|
||||||
|
thumbnail_info: {
|
||||||
|
h: 670,
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
size: 80157,
|
||||||
|
w: 540
|
||||||
|
},
|
||||||
|
thumbnail_url: "mxc://thomcat.rocks/XhLsOCDBYyearsLQgUUrbAvw",
|
||||||
|
w: 1080,
|
||||||
|
"xyz.amorgan.blurhash": "KHJQG*55ic-.}?0M58J.9v"
|
||||||
|
},
|
||||||
|
msgtype: "m.image",
|
||||||
|
url: "mxc://thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh"
|
||||||
|
},
|
||||||
|
origin_server_ts: 1740607766895,
|
||||||
|
sender: "@cadence:cadence.moe",
|
||||||
|
type: "m.room.message",
|
||||||
|
event_id: "$NqNqVgukiQm1nynm9vIr9FIq31hZpQ3udOd7cBIW46U",
|
||||||
|
room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ensureJoined: [],
|
||||||
|
messagesToDelete: [],
|
||||||
|
messagesToEdit: [],
|
||||||
|
messagesToSend: [{
|
||||||
|
username: "cadence [they]",
|
||||||
|
content: "this event has `formatting`",
|
||||||
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
|
attachments: [{id: "0", filename: "5740.jpg"}],
|
||||||
|
pendingFiles: [{name: "5740.jpg", mxc: "mxc://thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh"}],
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["users", "roles"]
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -3892,7 +3944,7 @@ test("event2message: encrypted image attachments work", async t => {
|
|||||||
username: "cadence [they]",
|
username: "cadence [they]",
|
||||||
content: "",
|
content: "",
|
||||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
attachments: [{id: "0", description: undefined, filename: "image.png"}],
|
attachments: [{id: "0", filename: "image.png"}],
|
||||||
pendingFiles: [{
|
pendingFiles: [{
|
||||||
name: "image.png",
|
name: "image.png",
|
||||||
mxc: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX",
|
mxc: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX",
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const mixin = require("@cloudrac3r/mixin-deep")
|
|
||||||
const stream = require("stream")
|
const stream = require("stream")
|
||||||
const streamWeb = require("stream/web")
|
const streamWeb = require("stream/web")
|
||||||
const getStream = require("get-stream")
|
const {buffer} = require("stream/consumers")
|
||||||
|
const mixin = require("@cloudrac3r/mixin-deep")
|
||||||
|
|
||||||
const {reg, writeRegistration} = require("./read-registration.js")
|
const {reg, writeRegistration} = require("./read-registration.js")
|
||||||
|
|
||||||
@@ -19,20 +19,33 @@ class MatrixServerError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {undefined | string | object | streamWeb.ReadableStream | stream.Readable} body
|
||||||
|
* @returns {Promise<string | streamWeb.ReadableStream | stream.Readable | Buffer>}
|
||||||
|
*/
|
||||||
|
async function _convertBody(body) {
|
||||||
|
if (body == undefined || Object.is(body.constructor, Object)) {
|
||||||
|
return JSON.stringify(body) // almost every POST request is going to follow this one
|
||||||
|
} else if (body instanceof stream.Readable && reg.ooye.content_length_workaround) {
|
||||||
|
return await buffer(body) // content length workaround is set, so convert to buffer. the buffer consumer accepts node streams.
|
||||||
|
} else if (body instanceof stream.Readable) {
|
||||||
|
return stream.Readable.toWeb(body) // native fetch can only consume web streams
|
||||||
|
} else if (body instanceof streamWeb.ReadableStream && reg.ooye.content_length_workaround) {
|
||||||
|
return await buffer(body) // content lenght workaround is set, so convert to buffer. the buffer consumer accepts async iterables, which web streams are.
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
/* c8 ignore start */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} method
|
* @param {string} method
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {string | object | streamWeb.ReadableStream | stream.Readable} [body]
|
* @param {string | object | streamWeb.ReadableStream | stream.Readable} [bodyIn]
|
||||||
* @param {any} [extra]
|
* @param {any} [extra]
|
||||||
*/
|
*/
|
||||||
async function mreq(method, url, body, extra = {}) {
|
async function mreq(method, url, bodyIn, extra = {}) {
|
||||||
if (body == undefined || Object.is(body.constructor, Object)) {
|
const body = await _convertBody(bodyIn)
|
||||||
body = JSON.stringify(body)
|
|
||||||
} else if (body instanceof stream.Readable && reg.ooye.content_length_workaround) {
|
|
||||||
body = await getStream.buffer(body)
|
|
||||||
} else if (body instanceof streamWeb.ReadableStream && reg.ooye.content_length_workaround) {
|
|
||||||
body = await stream.consumers.buffer(stream.Readable.fromWeb(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {RequestInit} */
|
/** @type {RequestInit} */
|
||||||
const opts = mixin({
|
const opts = mixin({
|
||||||
@@ -86,3 +99,4 @@ module.exports.MatrixServerError = MatrixServerError
|
|||||||
module.exports.baseUrl = baseUrl
|
module.exports.baseUrl = baseUrl
|
||||||
module.exports.mreq = mreq
|
module.exports.mreq = mreq
|
||||||
module.exports.withAccessToken = withAccessToken
|
module.exports.withAccessToken = withAccessToken
|
||||||
|
module.exports._convertBody = _convertBody
|
||||||
|
47
src/matrix/mreq.test.js
Normal file
47
src/matrix/mreq.test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert")
|
||||||
|
const stream = require("stream")
|
||||||
|
const streamWeb = require("stream/web")
|
||||||
|
const {buffer} = require("stream/consumers")
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const {_convertBody} = require("./mreq")
|
||||||
|
const {reg} = require("./read-registration")
|
||||||
|
|
||||||
|
async function *generator() {
|
||||||
|
yield "a"
|
||||||
|
yield "b"
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.ooye.content_length_workaround = false
|
||||||
|
|
||||||
|
test("convert body: converts object to string", async t => {
|
||||||
|
t.equal(await _convertBody({a: "1"}), `{"a":"1"}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert body: leaves undefined as undefined", async t => {
|
||||||
|
t.equal(await _convertBody(undefined), undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert body: leaves web readable as web readable", async t => {
|
||||||
|
const webReadable = stream.Readable.toWeb(stream.Readable.from(generator()))
|
||||||
|
t.equal(await _convertBody(webReadable), webReadable)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert body: converts node readable to web readable (for native fetch upload)", async t => {
|
||||||
|
const readable = stream.Readable.from(generator())
|
||||||
|
const webReadable = await _convertBody(readable)
|
||||||
|
assert(webReadable instanceof streamWeb.ReadableStream)
|
||||||
|
t.deepEqual(await buffer(webReadable), Buffer.from("ab"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert body: converts node readable to buffer", async t => {
|
||||||
|
reg.ooye.content_length_workaround = true
|
||||||
|
const readable = stream.Readable.from(generator())
|
||||||
|
t.deepEqual(await _convertBody(readable), Buffer.from("ab"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert body: converts web readable to buffer", async t => {
|
||||||
|
const webReadable = stream.Readable.toWeb(stream.Readable.from(generator()))
|
||||||
|
t.deepEqual(await _convertBody(webReadable), Buffer.from("ab"))
|
||||||
|
})
|
4
src/types.d.ts
vendored
4
src/types.d.ts
vendored
@@ -167,6 +167,8 @@ export namespace Event {
|
|||||||
export type M_Room_Message_File = {
|
export type M_Room_Message_File = {
|
||||||
msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
|
msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
|
||||||
body: string
|
body: string
|
||||||
|
format?: "org.matrix.custom.html"
|
||||||
|
formatted_body?: string
|
||||||
filename?: string
|
filename?: string
|
||||||
url: string
|
url: string
|
||||||
info?: any
|
info?: any
|
||||||
@@ -184,6 +186,8 @@ export namespace Event {
|
|||||||
export type M_Room_Message_Encrypted_File = {
|
export type M_Room_Message_Encrypted_File = {
|
||||||
msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
|
msgtype: "m.file" | "m.image" | "m.video" | "m.audio"
|
||||||
body: string
|
body: string
|
||||||
|
format?: "org.matrix.custom.html"
|
||||||
|
formatted_body?: string
|
||||||
filename?: string
|
filename?: string
|
||||||
file: {
|
file: {
|
||||||
url: string
|
url: string
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const streamWeb = require("stream/web")
|
const streamWeb = require("stream/web")
|
||||||
const {test} = require("supertape")
|
const {test} = require("../../test/web")
|
||||||
const {router} = require("../../test/web")
|
const {router} = require("../../test/web")
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
|
|
||||||
require("./server")
|
require("./server")
|
||||||
|
|
||||||
test("web server: can get home", async t => {
|
test("web server: can get home", async t => {
|
||||||
t.match(await router.test("get", "/", {}), /Add the bot to your Discord server./)
|
t.has(await router.test("get", "/", {}), /a bridge between the Discord and Matrix chat apps/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("web server: can get htmx", async t => {
|
test("web server: can get htmx", async t => {
|
||||||
|
@@ -29,6 +29,7 @@ reg.ooye.bridge_origin = "https://bridge.example.org"
|
|||||||
const sync = new HeatSync({watchFS: false})
|
const sync = new HeatSync({watchFS: false})
|
||||||
|
|
||||||
const discord = {
|
const discord = {
|
||||||
|
// @ts-ignore - only ignore guilds, because my data dump is missing random properties
|
||||||
guilds: new Map([
|
guilds: new Map([
|
||||||
[data.guild.general.id, data.guild.general],
|
[data.guild.general.id, data.guild.general],
|
||||||
[data.guild.fna.id, data.guild.fna],
|
[data.guild.fna.id, data.guild.fna],
|
||||||
@@ -130,6 +131,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
|||||||
require("../src/matrix/kstate.test")
|
require("../src/matrix/kstate.test")
|
||||||
require("../src/matrix/api.test")
|
require("../src/matrix/api.test")
|
||||||
require("../src/matrix/file.test")
|
require("../src/matrix/file.test")
|
||||||
|
require("../src/matrix/mreq.test")
|
||||||
require("../src/matrix/read-registration.test")
|
require("../src/matrix/read-registration.test")
|
||||||
require("../src/matrix/txnid.test")
|
require("../src/matrix/txnid.test")
|
||||||
require("../src/d2m/actions/create-room.test")
|
require("../src/d2m/actions/create-room.test")
|
||||||
|
Reference in New Issue
Block a user