2023-05-04 20:25:00 +00:00
|
|
|
// @ts-check
|
|
|
|
|
2023-05-05 05:29:08 +00:00
|
|
|
const fetch = require("node-fetch").default
|
2023-05-04 20:25:00 +00:00
|
|
|
|
|
|
|
const passthrough = require("../passthrough")
|
2023-09-18 10:51:59 +00:00
|
|
|
const {sync, db, select} = passthrough
|
2023-05-04 20:25:00 +00:00
|
|
|
/** @type {import("./mreq")} */
|
|
|
|
const mreq = sync.require("./mreq")
|
|
|
|
|
|
|
|
const DISCORD_IMAGES_BASE = "https://cdn.discordapp.com"
|
|
|
|
const IMAGE_SIZE = 1024
|
|
|
|
|
|
|
|
/** @type {Map<string, Promise<string>>} */
|
|
|
|
const inflight = new Map()
|
|
|
|
|
2023-09-30 12:27:03 +00:00
|
|
|
/**
|
|
|
|
* @param {string} url
|
|
|
|
*/
|
|
|
|
function _removeExpiryParams(url) {
|
|
|
|
return url.replace(/\?(?:(?:ex|is|sg|hm)=[a-f0-9]+&?)*$/, "")
|
|
|
|
}
|
|
|
|
|
2023-05-04 20:25:00 +00:00
|
|
|
/**
|
2023-10-06 04:00:21 +00:00
|
|
|
* @param {string} path or full URL if it's not a Discord CDN file
|
2023-05-04 20:25:00 +00:00
|
|
|
*/
|
|
|
|
async function uploadDiscordFileToMxc(path) {
|
2023-05-10 11:17:37 +00:00
|
|
|
let url
|
|
|
|
if (path.startsWith("http")) {
|
|
|
|
url = path
|
|
|
|
} else {
|
|
|
|
url = DISCORD_IMAGES_BASE + path
|
|
|
|
}
|
2023-05-04 20:25:00 +00:00
|
|
|
|
2023-09-30 12:27:03 +00:00
|
|
|
// Discord attachment content is always the same no matter what their ?ex parameter is.
|
|
|
|
const urlNoExpiry = _removeExpiryParams(url)
|
|
|
|
|
2023-05-04 20:25:00 +00:00
|
|
|
// Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution
|
2023-09-30 12:27:03 +00:00
|
|
|
const existingInflight = inflight.get(urlNoExpiry)
|
2023-08-24 23:44:58 +00:00
|
|
|
if (existingInflight) {
|
|
|
|
return existingInflight
|
2023-05-04 20:25:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Has this file already been uploaded in the past? Grab the existing copy from the database.
|
2023-10-05 23:31:10 +00:00
|
|
|
const existingFromDb = select("file", "mxc_url", {discord_url: urlNoExpiry}).pluck().get()
|
2023-08-24 23:44:58 +00:00
|
|
|
if (typeof existingFromDb === "string") {
|
|
|
|
return existingFromDb
|
2023-05-04 20:25:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Download from Discord
|
|
|
|
const promise = fetch(url, {}).then(/** @param {import("node-fetch").Response} res */ async res => {
|
|
|
|
// Upload to Matrix
|
2023-09-30 12:27:03 +00:00
|
|
|
const root = await module.exports._actuallyUploadDiscordFileToMxc(urlNoExpiry, res)
|
2023-05-04 20:25:00 +00:00
|
|
|
|
|
|
|
// Store relationship in database
|
2023-09-30 12:27:03 +00:00
|
|
|
db.prepare("INSERT INTO file (discord_url, mxc_url) VALUES (?, ?)").run(urlNoExpiry, root.content_uri)
|
|
|
|
inflight.delete(urlNoExpiry)
|
2023-05-04 20:25:00 +00:00
|
|
|
|
|
|
|
return root.content_uri
|
|
|
|
})
|
2023-09-30 12:27:03 +00:00
|
|
|
inflight.set(urlNoExpiry, promise)
|
2023-05-04 20:25:00 +00:00
|
|
|
|
|
|
|
return promise
|
|
|
|
}
|
|
|
|
|
2023-08-18 05:00:40 +00:00
|
|
|
async function _actuallyUploadDiscordFileToMxc(url, res) {
|
|
|
|
const body = res.body
|
|
|
|
/** @type {import("../types").R.FileUploaded} */
|
|
|
|
const root = await mreq.mreq("POST", "/media/v3/upload", body, {
|
|
|
|
headers: {
|
|
|
|
"Content-Type": res.headers.get("content-type")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
2023-05-04 20:25:00 +00:00
|
|
|
function guildIcon(guild) {
|
2023-05-05 05:29:08 +00:00
|
|
|
return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}`
|
2023-05-04 20:25:00 +00:00
|
|
|
}
|
|
|
|
|
2023-05-10 10:15:20 +00:00
|
|
|
function userAvatar(user) {
|
|
|
|
return `/avatars/${user.id}/${user.avatar}.png?size=${IMAGE_SIZE}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function memberAvatar(guildID, user, member) {
|
|
|
|
if (!member.avatar) return userAvatar(user)
|
|
|
|
return `/guilds/${guildID}/users/${user.id}/avatars/${member.avatar}.png?size=${IMAGE_SIZE}`
|
|
|
|
}
|
|
|
|
|
2023-05-12 05:35:37 +00:00
|
|
|
function emoji(emojiID, animated) {
|
|
|
|
const base = `/emojis/${emojiID}`
|
|
|
|
if (animated) return base + ".gif"
|
|
|
|
else return base + ".png"
|
|
|
|
}
|
|
|
|
|
2023-05-15 05:25:05 +00:00
|
|
|
const stickerFormat = new Map([
|
|
|
|
[1, {label: "PNG", ext: "png", mime: "image/png"}],
|
|
|
|
[2, {label: "APNG", ext: "png", mime: "image/apng"}],
|
2023-09-10 09:35:51 +00:00
|
|
|
[3, {label: "LOTTIE", ext: "json", mime: "lottie"}],
|
2023-05-15 05:25:05 +00:00
|
|
|
[4, {label: "GIF", ext: "gif", mime: "image/gif"}]
|
|
|
|
])
|
|
|
|
|
2023-06-28 12:06:56 +00:00
|
|
|
/** @param {{id: string, format_type: number}} sticker */
|
2023-05-15 05:25:05 +00:00
|
|
|
function sticker(sticker) {
|
|
|
|
const format = stickerFormat.get(sticker.format_type)
|
|
|
|
if (!format) throw new Error(`No such format ${sticker.format_type} for sticker ${JSON.stringify(sticker)}`)
|
|
|
|
const ext = format.ext
|
|
|
|
return `/stickers/${sticker.id}.${ext}`
|
|
|
|
}
|
|
|
|
|
2023-09-07 00:07:56 +00:00
|
|
|
module.exports.DISCORD_IMAGES_BASE = DISCORD_IMAGES_BASE
|
2023-05-04 20:25:00 +00:00
|
|
|
module.exports.guildIcon = guildIcon
|
2023-05-10 10:15:20 +00:00
|
|
|
module.exports.userAvatar = userAvatar
|
|
|
|
module.exports.memberAvatar = memberAvatar
|
2023-05-12 05:35:37 +00:00
|
|
|
module.exports.emoji = emoji
|
2023-05-15 05:25:05 +00:00
|
|
|
module.exports.stickerFormat = stickerFormat
|
|
|
|
module.exports.sticker = sticker
|
2023-05-04 20:25:00 +00:00
|
|
|
module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc
|
2023-08-18 05:00:40 +00:00
|
|
|
module.exports._actuallyUploadDiscordFileToMxc = _actuallyUploadDiscordFileToMxc
|
2023-09-30 12:27:03 +00:00
|
|
|
module.exports._removeExpiryParams = _removeExpiryParams
|