m->d support encrypted files
This commit is contained in:
		| @@ -1,6 +1,9 @@ | |||||||
| // @ts-check | // @ts-check | ||||||
|  |  | ||||||
| const assert = require("assert").strict | const assert = require("assert").strict | ||||||
|  | const crypto = require("crypto") | ||||||
|  | const {pipeline} = require("stream") | ||||||
|  | const {promisify} = require("util") | ||||||
| const Ty = require("../../types") | const Ty = require("../../types") | ||||||
| const DiscordTypes = require("discord-api-types/v10") | const DiscordTypes = require("discord-api-types/v10") | ||||||
| const passthrough = require("../../passthrough") | const passthrough = require("../../passthrough") | ||||||
| @@ -14,16 +17,29 @@ const eventToMessage = sync.require("../converters/event-to-message") | |||||||
| const api = sync.require("../../matrix/api") | const api = sync.require("../../matrix/api") | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {pendingFiles?: {name: string, url: string}[]}} message |  * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string})[]}} message | ||||||
|  * @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}>} |  * @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}>} | ||||||
|  */ |  */ | ||||||
| async function resolvePendingFiles(message) { | async function resolvePendingFiles(message) { | ||||||
| 	if (!message.pendingFiles) return message | 	if (!message.pendingFiles) return message | ||||||
| 	const files = await Promise.all(message.pendingFiles.map(async p => { | 	const files = await Promise.all(message.pendingFiles.map(async p => { | ||||||
| 		const file = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x)) | 		let fileBuffer | ||||||
|  | 		if ("key" in p) { | ||||||
|  | 			// Encrypted | ||||||
|  | 			const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url")) | ||||||
|  | 			fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => { | ||||||
|  | 				return Buffer.concat([ | ||||||
|  | 					d.update(Buffer.from(x)), | ||||||
|  | 					d.final() | ||||||
|  | 				]) | ||||||
|  | 			}) | ||||||
|  | 		} else { | ||||||
|  | 			// Unencrypted | ||||||
|  | 			fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x)) | ||||||
|  | 		} | ||||||
| 		return { | 		return { | ||||||
| 			name: p.name, | 			name: p.name, | ||||||
| 			file | 			file: fileBuffer // TODO: Once SnowTransfer supports ReadableStreams for attachment uploads, pass in those instead of Buffers | ||||||
| 		} | 		} | ||||||
| 	})) | 	})) | ||||||
| 	const newMessage = { | 	const newMessage = { | ||||||
| @@ -34,7 +50,7 @@ async function resolvePendingFiles(message) { | |||||||
| 	return newMessage | 	return newMessage | ||||||
| } | } | ||||||
|  |  | ||||||
| /** @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event */ | /** @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker} event */ | ||||||
| async function sendEvent(event) { | async function sendEvent(event) { | ||||||
| 	// TODO: we just assume the bridge has already been created, is that really ok? | 	// TODO: we just assume the bridge has already been created, is that really ok? | ||||||
| 	const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id) | 	const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id) | ||||||
|   | |||||||
| @@ -125,7 +125,7 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File | Ty.Event.M_Outer_M_Sticker} event |  * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event | ||||||
|  * @param {import("discord-api-types/v10").APIGuild} guild |  * @param {import("discord-api-types/v10").APIGuild} guild | ||||||
|  * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API |  * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API | ||||||
|  */ |  */ | ||||||
| @@ -148,7 +148,7 @@ async function eventToMessage(event, guild, di) { | |||||||
|  |  | ||||||
| 	let content = event.content.body // ultimate fallback | 	let content = event.content.body // ultimate fallback | ||||||
| 	const attachments = [] | 	const attachments = [] | ||||||
| 	/** @type {{name: string, url: string}[]} */ | 	/** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string})[]} */ | ||||||
| 	const pendingFiles = [] | 	const pendingFiles = [] | ||||||
|  |  | ||||||
| 	// Convert content depending on what the message is | 	// Convert content depending on what the message is | ||||||
| @@ -281,10 +281,20 @@ async function eventToMessage(event, guild, di) { | |||||||
| 	} 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")) { | 	} 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 = "" | 		content = "" | ||||||
| 		const filename = event.content.body | 		const filename = event.content.body | ||||||
|  | 		if ("url" in event.content) { | ||||||
|  | 			// Unencrypted | ||||||
| 			const url = utils.getPublicUrlForMxc(event.content.url) | 			const url = utils.getPublicUrlForMxc(event.content.url) | ||||||
| 			assert(url) | 			assert(url) | ||||||
| 			attachments.push({id: "0", filename}) | 			attachments.push({id: "0", filename}) | ||||||
| 			pendingFiles.push({name: filename, url}) | 			pendingFiles.push({name: filename, url}) | ||||||
|  | 		} else { | ||||||
|  | 			// Encrypted | ||||||
|  | 			const url = utils.getPublicUrlForMxc(event.content.file.url) | ||||||
|  | 			assert(url) | ||||||
|  | 			assert.equal(event.content.file.key.alg, "A256CTR") | ||||||
|  | 			attachments.push({id: "0", filename}) | ||||||
|  | 			pendingFiles.push({name: filename, url, key: event.content.file.key.k, iv: event.content.file.iv}) | ||||||
|  | 		} | ||||||
| 	} else if (event.type === "m.sticker") { | 	} else if (event.type === "m.sticker") { | ||||||
| 		content = "" | 		content = "" | ||||||
| 		let filename = event.content.body | 		let filename = event.content.body | ||||||
|   | |||||||
| @@ -1176,6 +1176,60 @@ test("event2message: image attachments work", async t => { | |||||||
| 	) | 	) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | test("event2message: encrypted image attachments work", async t => { | ||||||
|  | 	t.deepEqual( | ||||||
|  | 		await eventToMessage({ | ||||||
|  | 			type: "m.room.message", | ||||||
|  | 			sender: "@cadence:cadence.moe", | ||||||
|  | 			content: { | ||||||
|  | 				info: { | ||||||
|  | 					mimetype: "image/png", | ||||||
|  | 					size: 105691, | ||||||
|  | 					w: 1192, | ||||||
|  | 					h: 309, | ||||||
|  | 					"xyz.amorgan.blurhash": "U17USN~q9FtQ-;Rjxuj[9FIUoMM|-=WB9Ft7" | ||||||
|  | 				}, | ||||||
|  | 				msgtype: "m.image", | ||||||
|  | 				body: "image.png", | ||||||
|  | 				file: { | ||||||
|  | 					v: "v2", | ||||||
|  | 					key: { | ||||||
|  | 						alg: "A256CTR", | ||||||
|  | 						ext: true, | ||||||
|  | 						k: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo", | ||||||
|  | 						key_ops: ["encrypt", "decrypt"], | ||||||
|  | 						kty: "oct" | ||||||
|  | 					}, | ||||||
|  | 					iv: "Va9SHZpIn5kAAAAAAAAAAA", | ||||||
|  | 					hashes: { | ||||||
|  | 						sha256: "OUZqZFBcANFt42iAKET9YXfWMCdT0BX7QO0Eyk9q4Js" | ||||||
|  | 					}, | ||||||
|  | 					url: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX", | ||||||
|  | 					mimetype: "image/png" | ||||||
|  | 				} | ||||||
|  | 			}, | ||||||
|  | 			event_id: "$JNhONhXO-5jrztZz8b7mbTMJasbU78TwQr4tog-3Mnk", | ||||||
|  | 			room_id: "!PnyBKvUBOhjuCucEfk:cadence.moe" | ||||||
|  | 		}), | ||||||
|  | 		{ | ||||||
|  | 			messagesToDelete: [], | ||||||
|  | 			messagesToEdit: [], | ||||||
|  | 			messagesToSend: [{ | ||||||
|  | 				username: "cadence [they]", | ||||||
|  | 				content: "", | ||||||
|  | 				avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", | ||||||
|  | 				attachments: [{id: "0", filename: "image.png"}], | ||||||
|  | 				pendingFiles: [{ | ||||||
|  | 					name: "image.png", | ||||||
|  | 					url: "https://matrix.cadence.moe/_matrix/media/r0/download/heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX", | ||||||
|  | 					key: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo", | ||||||
|  | 					iv: "Va9SHZpIn5kAAAAAAAAAAA" | ||||||
|  | 				}] | ||||||
|  | 			}] | ||||||
|  | 		} | ||||||
|  | 	) | ||||||
|  | }) | ||||||
|  |  | ||||||
| test("event2message: stickers work", async t => { | test("event2message: stickers work", async t => { | ||||||
| 	t.deepEqual( | 	t.deepEqual( | ||||||
| 		await eventToMessage({ | 		await eventToMessage({ | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ function guard(type, fn) { | |||||||
|  |  | ||||||
| sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message", | sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message", | ||||||
| /** | /** | ||||||
|  * @param {Ty.Event.M_Outer_M_Room_Message | Ty.Event.M_Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for |  * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for | ||||||
|  */ |  */ | ||||||
| async event => { | async event => { | ||||||
| 	if (utils.eventSenderIsFromDiscord(event.sender)) return | 	if (utils.eventSenderIsFromDiscord(event.sender)) return | ||||||
| @@ -63,7 +63,7 @@ async event => { | |||||||
|  |  | ||||||
| sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker", | sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker", | ||||||
| /** | /** | ||||||
|  * @param {Ty.Event.M_Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for |  * @param {Ty.Event.Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for | ||||||
|  */ |  */ | ||||||
| async event => { | async event => { | ||||||
| 	if (utils.eventSenderIsFromDiscord(event.sender)) return | 	if (utils.eventSenderIsFromDiscord(event.sender)) return | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -80,7 +80,7 @@ export namespace Event { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	export type M_Outer_M_Room_Message = Outer<M_Room_Message> & {type: "m.room.message"} | 	export type Outer_M_Room_Message = Outer<M_Room_Message> & {type: "m.room.message"} | ||||||
|  |  | ||||||
| 	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" | ||||||
| @@ -96,7 +96,39 @@ export namespace Event { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	export type M_Outer_M_Room_Message_File = Outer<M_Room_Message_File> & {type: "m.room.message"} | 	export type Outer_M_Room_Message_File = Outer<M_Room_Message_File> & {type: "m.room.message"} | ||||||
|  |  | ||||||
|  | 	export type M_Room_Message_Encrypted_File = { | ||||||
|  | 		msgtype: "m.file" | "m.image" | "m.video" | "m.audio" | ||||||
|  | 		body: string | ||||||
|  | 		file: { | ||||||
|  | 			url: string | ||||||
|  | 			iv: string | ||||||
|  | 			hashes: { | ||||||
|  | 				sha256: string | ||||||
|  | 			} | ||||||
|  | 			v: "v2" | ||||||
|  | 			key: { | ||||||
|  | 				/** :3 */ | ||||||
|  | 				kty: "oct" | ||||||
|  | 				/** must include at least "encrypt" and "decrypt" */ | ||||||
|  | 				key_ops: string[] | ||||||
|  | 				alg: "A256CTR" | ||||||
|  | 				k: string | ||||||
|  | 				ext: true | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		info?: any | ||||||
|  | 		"m.relates_to"?: { | ||||||
|  | 			"m.in_reply_to": { | ||||||
|  | 				event_id: string | ||||||
|  | 			} | ||||||
|  | 			rel_type?: "m.replace" | ||||||
|  | 			event_id?: string | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	export type Outer_M_Room_Message_Encrypted_File = Outer<M_Room_Message_Encrypted_File> & {type: "m.room.message"} | ||||||
|  |  | ||||||
| 	export type M_Sticker = { | 	export type M_Sticker = { | ||||||
| 		body: string | 		body: string | ||||||
| @@ -111,7 +143,7 @@ export namespace Event { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	export type M_Outer_M_Sticker = Outer<M_Sticker> & {type: "m.sticker"} | 	export type Outer_M_Sticker = Outer<M_Sticker> & {type: "m.sticker"} | ||||||
|  |  | ||||||
| 	export type M_Room_Member = { | 	export type M_Room_Member = { | ||||||
| 		membership: string | 		membership: string | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Cadence Ember
					Cadence Ember