Move calls from getPublicUrlForMxc to getMedia

This commit is contained in:
Cadence Ember 2024-09-13 17:19:42 +12:00
parent b45d0f3038
commit c0d92ea66d
7 changed files with 64 additions and 52 deletions

View file

@ -8,6 +8,8 @@ const {sync} = require("../../passthrough")
/** @type {import("../converters/emoji-sheet")} */ /** @type {import("../converters/emoji-sheet")} */
const emojiSheetConverter = sync.require("../converters/emoji-sheet") const emojiSheetConverter = sync.require("../converters/emoji-sheet")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** /**
* Downloads the emoji from the web and converts to uncompressed PNG data. * Downloads the emoji from the web and converts to uncompressed PNG data.
@ -16,16 +18,12 @@ const emojiSheetConverter = sync.require("../converters/emoji-sheet")
*/ */
async function getAndConvertEmoji(mxc) { async function getAndConvertEmoji(mxc) {
const abortController = new AbortController() const abortController = new AbortController()
const url = utils.getPublicUrlForMxc(mxc)
assert(url)
/** @type {import("node-fetch").Response} */ /** @type {import("node-fetch").Response} */
// If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing. // If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing.
// If we were using connection pooling, we would be forced to download the entire GIF. // If we were using connection pooling, we would be forced to download the entire GIF.
// So we set no agent to ensure we are not connection pooling. // So we set no agent to ensure we are not connection pooling.
// @ts-ignore the signal is slightly different from the type it wants (still works fine) // @ts-ignore the signal is slightly different from the type it wants (still works fine)
const res = await fetch(url, {agent: false, signal: abortController.signal}) const res = await api.getMedia(mxc, {agent: false, signal: abortController.signal})
return emojiSheetConverter.convertImageStream(res.body, () => { return emojiSheetConverter.convertImageStream(res.body, () => {
abortController.abort() abortController.abort()
res.body.pause() res.body.pause()

View file

@ -23,7 +23,7 @@ const editMessage = sync.require("../../d2m/actions/edit-message")
const emojiSheet = sync.require("../actions/emoji-sheet") const emojiSheet = sync.require("../actions/emoji-sheet")
/** /**
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message
* @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]}>} * @returns {Promise<DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]}>}
*/ */
async function resolvePendingFiles(message) { async function resolvePendingFiles(message) {
@ -39,7 +39,7 @@ async function resolvePendingFiles(message) {
// Encrypted file // Encrypted file
const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url")) const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url"))
// @ts-ignore // @ts-ignore
fetch(p.url).then(res => res.body.pipe(d)) await api.getMedia(p.mxc).then(res => res.body.pipe(d))
return { return {
name: p.name, name: p.name,
file: d file: d
@ -47,7 +47,7 @@ async function resolvePendingFiles(message) {
} else { } else {
// Unencrypted file // Unencrypted file
/** @type {Readable} */ // @ts-ignore /** @type {Readable} */ // @ts-ignore
const body = await fetch(p.url).then(res => res.body) const body = await api.getMedia(p.mxc).then(res => res.body)
return { return {
name: p.name, name: p.name,
file: body file: body
@ -79,7 +79,7 @@ async function sendEvent(event) {
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch, mxcDownloader: emojiSheet.getAndConvertEmoji}) let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, mxcDownloader: emojiSheet.getAndConvertEmoji})
messagesToEdit = await Promise.all(messagesToEdit.map(async e => { messagesToEdit = await Promise.all(messagesToEdit.map(async e => {
e.message = await resolvePendingFiles(e.message) e.message = await resolvePendingFiles(e.message)

View file

@ -305,7 +305,7 @@ function getUserOrProxyOwnerID(mxid) {
* 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, name: string}[]} attachments
* @param {({name: string, url: string} | {name: string, url: 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.
*/ */
async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) { async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) {
@ -389,7 +389,7 @@ async function handleRoomOrMessageLinks(input, di) {
* @param {string} senderMxid * @param {string} senderMxid
* @param {string} roomID * @param {string} roomID
* @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuild} guild
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer}} di
*/ */
async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+
@ -440,7 +440,7 @@ const attachmentEmojis = new Map([
/** /**
* @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 {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"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"], mxcDownloader: (mxc: string) => Promise<Buffer | undefined>}} di simple-as-nails dependency injection for the matrix API * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, mxcDownloader: (mxc: string) => Promise<Buffer | undefined>}} di simple-as-nails dependency injection for the matrix API
*/ */
async function eventToMessage(event, guild, di) { async function eventToMessage(event, guild, di) {
let displayName = event.sender let displayName = event.sender
@ -466,7 +466,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} | {name: string, url: 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 = []
/** @type {DiscordTypes.APIUser[]} */ /** @type {DiscordTypes.APIUser[]} */
const ensureJoined = [] const ensureJoined = []
@ -767,29 +767,23 @@ async function eventToMessage(event, guild, di) {
const description = (event.content.body !== event.content.filename && event.content.filename && event.content.body) || undefined const description = (event.content.body !== event.content.filename && event.content.filename && event.content.body) || undefined
if ("url" in event.content) { if ("url" in event.content) {
// Unencrypted // Unencrypted
const url = mxUtils.getPublicUrlForMxc(event.content.url)
assert(url)
attachments.push({id: "0", description, filename}) attachments.push({id: "0", description, filename})
pendingFiles.push({name: filename, url}) pendingFiles.push({name: filename, mxc: event.content.url})
} else { } else {
// Encrypted // Encrypted
const url = mxUtils.getPublicUrlForMxc(event.content.file.url)
assert(url)
assert.equal(event.content.file.key.alg, "A256CTR") assert.equal(event.content.file.key.alg, "A256CTR")
attachments.push({id: "0", description, filename}) attachments.push({id: "0", description, filename})
pendingFiles.push({name: filename, url, key: event.content.file.key.k, iv: event.content.file.iv}) 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") { } else if (event.type === "m.sticker") {
content = "" content = ""
const url = mxUtils.getPublicUrlForMxc(event.content.url)
assert(url)
let filename = event.content.body let filename = event.content.body
if (event.type === "m.sticker") { if (event.type === "m.sticker") {
let mimetype let mimetype
if (event.content.info?.mimetype?.includes("/")) { if (event.content.info?.mimetype?.includes("/")) {
mimetype = event.content.info.mimetype mimetype = event.content.info.mimetype
} else { } else {
const res = await di.fetch(url, {method: "HEAD"}) const res = await di.api.getMedia(event.content.url, {method: "HEAD"})
if (res.status === 200) { if (res.status === 200) {
mimetype = res.headers.get("content-type") mimetype = res.headers.get("content-type")
} }
@ -798,7 +792,7 @@ async function eventToMessage(event, guild, di) {
filename += "." + mimetype.split("/")[1] filename += "." + mimetype.split("/")[1]
} }
attachments.push({id: "0", filename}) attachments.push({id: "0", filename})
pendingFiles.push({name: filename, url}) pendingFiles.push({name: filename, mxc: event.content.url})
} }
content = displayNameRunoff + replyLine + content content = displayNameRunoff + replyLine + content

View file

@ -3550,7 +3550,7 @@ test("event2message: text attachments work", async t => {
content: "", content: "",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
attachments: [{id: "0", description: undefined, filename: "chiki-powerups.txt"}], attachments: [{id: "0", description: undefined, filename: "chiki-powerups.txt"}],
pendingFiles: [{name: "chiki-powerups.txt", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}] pendingFiles: [{name: "chiki-powerups.txt", mxc: "mxc://cadence.moe/zyThGlYQxvlvBVbVgKDDbiHH"}]
}] }]
} }
) )
@ -3586,7 +3586,7 @@ test("event2message: image attachments work", async t => {
content: "", content: "",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
attachments: [{id: "0", description: undefined, filename: "cool cat.png"}], attachments: [{id: "0", description: undefined, filename: "cool cat.png"}],
pendingFiles: [{name: "cool cat.png", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
}] }]
} }
) )
@ -3622,7 +3622,7 @@ test("event2message: image attachments can have a custom description", async t =
content: "", content: "",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
attachments: [{id: "0", description: "Cat emoji surrounded by pink hearts", filename: "cool cat.png"}], attachments: [{id: "0", description: "Cat emoji surrounded by pink hearts", filename: "cool cat.png"}],
pendingFiles: [{name: "cool cat.png", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}] pendingFiles: [{name: "cool cat.png", url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
}] }]
} }
) )
@ -3674,7 +3674,7 @@ test("event2message: encrypted image attachments work", async t => {
attachments: [{id: "0", description: undefined, filename: "image.png"}], attachments: [{id: "0", description: undefined, filename: "image.png"}],
pendingFiles: [{ pendingFiles: [{
name: "image.png", name: "image.png",
url: "https://matrix.cadence.moe/_matrix/media/r0/download/heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX", mxc: "mxc://heyquark.com/LOGkUTlVFrqfiExlGZNgCJJX",
key: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo", key: "QTo-oMPnN1Rbc7vBFg9WXMgoctscdyxdFEIYm8NYceo",
iv: "Va9SHZpIn5kAAAAAAAAAAA" iv: "Va9SHZpIn5kAAAAAAAAAAA"
}] }]
@ -3717,7 +3717,7 @@ test("event2message: stickers work", async t => {
content: "", content: "",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
attachments: [{id: "0", filename: "get_real2.gif"}], attachments: [{id: "0", filename: "get_real2.gif"}],
pendingFiles: [{name: "get_real2.gif", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/NyMXQFAAdniImbHzsygScbmN"}] pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}]
}] }]
} }
) )
@ -3736,9 +3736,10 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {}, { }, {}, {
async fetch(url, options) { api: {
async getMedia(mxc, options) {
called++ called++
t.equal(url, "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf") t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf")
t.equal(options.method, "HEAD") t.equal(options.method, "HEAD")
return { return {
status: 200, status: 200,
@ -3747,6 +3748,7 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
]) ])
} }
} }
}
}), }),
{ {
ensureJoined: [], ensureJoined: [],
@ -3757,7 +3759,7 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
content: "", content: "",
avatar_url: undefined, avatar_url: undefined,
attachments: [{id: "0", filename: "YESYESYES.gif"}], attachments: [{id: "0", filename: "YESYESYES.gif"}],
pendingFiles: [{name: "YESYESYES.gif", url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}] pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}]
}] }]
} }
) )
@ -3777,9 +3779,10 @@ test("event2message: stickers with unknown mimetype are not allowed", async t =>
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0", event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {}, { }, {}, {
async fetch(url, options) { api: {
async getMedia(mxc, options) {
called++ called++
t.equal(url, "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe") t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe")
t.equal(options.method, "HEAD") t.equal(options.method, "HEAD")
return { return {
status: 404, status: 404,
@ -3788,6 +3791,7 @@ test("event2message: stickers with unknown mimetype are not allowed", async t =>
]) ])
} }
} }
}
}) })
/* c8 ignore next */ /* c8 ignore next */
t.fail("should throw an error") t.fail("should throw an error")

View file

@ -223,10 +223,10 @@ async function getViaServersQuery(roomID, api) {
*/ */
function getPublicUrlForMxc(mxc) { function getPublicUrlForMxc(mxc) {
assert(hasher, "xxhash is not ready yet") assert(hasher, "xxhash is not ready yet")
const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!avatarURLParts) return null if (!mediaParts) return null
const serverAndMediaID = `${avatarURLParts[1]}/${avatarURLParts[2]}` const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
const unsignedHash = hasher.h64(serverAndMediaID) const unsignedHash = hasher.h64(serverAndMediaID)
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)

View file

@ -297,6 +297,7 @@ async function setUserPowerCascade(roomID, mxid, power) {
} }
async function ping() { async function ping() {
// not using mreq so that we can read the status code
const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, { const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, {
method: "POST", method: "POST",
headers: { headers: {
@ -312,6 +313,21 @@ async function ping() {
} }
} }
/**
* @param {string} mxc
* @param {RequestInit} [init]
*/
function getMedia(mxc, init = {}) {
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
assert(mediaParts)
return fetch(`${mreq.baseUrl}/client/v1/media/download/${mediaParts[1]}/${mediaParts[2]}`, {
headers: {
Authorization: `Bearer ${reg.as_token}`
},
...init
})
}
module.exports.path = path module.exports.path = path
module.exports.register = register module.exports.register = register
module.exports.createRoom = createRoom module.exports.createRoom = createRoom
@ -336,3 +352,4 @@ module.exports.profileSetAvatarUrl = profileSetAvatarUrl
module.exports.setUserPower = setUserPower module.exports.setUserPower = setUserPower
module.exports.setUserPowerCascade = setUserPowerCascade module.exports.setUserPowerCascade = setUserPowerCascade
module.exports.ping = ping module.exports.ping = ping
module.exports.getMedia = getMedia

View file

@ -217,9 +217,8 @@ const commands = [{
} else { } else {
// Upload it to Discord and have the bridge sync it back to Matrix again // Upload it to Discord and have the bridge sync it back to Matrix again
for (const e of toUpload) { for (const e of toUpload) {
const publicUrl = mxUtils.getPublicUrlForMxc(e.url)
// @ts-ignore // @ts-ignore
const resizeInput = await fetch(publicUrl, {agent: false}).then(res => res.arrayBuffer()) const resizeInput = await api.getMedia(e.url, {agent: false}).then(res => res.arrayBuffer())
const resizeOutput = await sharp(resizeInput) const resizeOutput = await sharp(resizeInput)
.resize(EMOJI_SIZE, EMOJI_SIZE, {fit: "inside", withoutEnlargement: true, background: {r: 0, g: 0, b: 0, alpha: 0}}) .resize(EMOJI_SIZE, EMOJI_SIZE, {fit: "inside", withoutEnlargement: true, background: {r: 0, g: 0, b: 0, alpha: 0}})
.png() .png()