import { proxyUrl } from "@/response"; import type { AsyncAttachment as ApiAsyncAttachment, Attachment as ApiAttachment, } from "@lysand-org/client/types"; import type { ContentFormat } from "@lysand-org/federation/types"; import { type InferInsertModel, type InferSelectModel, type SQL, desc, eq, inArray, } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { Attachments } from "~/drizzle/schema"; import { MediaBackendType } from "~/packages/config-manager/config.type"; import { config } from "~/packages/config-manager/index"; import { BaseInterface } from "./base"; export type AttachmentType = InferSelectModel; export class Attachment extends BaseInterface { async reload(): Promise { const reloaded = await Attachment.fromId(this.data.id); if (!reloaded) { throw new Error("Failed to reload attachment"); } this.data = reloaded.data; } public static async fromId(id: string | null): Promise { if (!id) { return null; } return await Attachment.fromSql(eq(Attachments.id, id)); } public static async fromIds(ids: string[]): Promise { return await Attachment.manyFromSql(inArray(Attachments.id, ids)); } public static async fromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Attachments.id), ): Promise { const found = await db.query.Attachments.findFirst({ where: sql, orderBy, }); if (!found) { return null; } return new Attachment(found); } public static async manyFromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Attachments.id), limit?: number, offset?: number, extra?: Parameters[0], ): Promise { const found = await db.query.Attachments.findMany({ where: sql, orderBy, limit, offset, with: extra?.with, }); return found.map((s) => new Attachment(s)); } async update( newAttachment: Partial, ): Promise { await db .update(Attachments) .set(newAttachment) .where(eq(Attachments.id, this.id)); const updated = await Attachment.fromId(this.data.id); if (!updated) { throw new Error("Failed to update attachment"); } this.data = updated.data; return updated.data; } save(): Promise { return this.update(this.data); } async delete(ids: string[]): Promise; async delete(): Promise; async delete(ids?: unknown): Promise { if (Array.isArray(ids)) { await db.delete(Attachments).where(inArray(Attachments.id, ids)); } else { await db.delete(Attachments).where(eq(Attachments.id, this.id)); } } public static async insert( data: InferInsertModel, ): Promise { const inserted = ( await db.insert(Attachments).values(data).returning() )[0]; const attachment = await Attachment.fromId(inserted.id); if (!attachment) { throw new Error("Failed to insert attachment"); } return attachment; } get id() { return this.data.id; } public static getUrl(name: string) { if (config.media.backend === MediaBackendType.Local) { return new URL(`/media/${name}`, config.http.base_url).toString(); } if (config.media.backend === MediaBackendType.S3) { return new URL(`/${name}`, config.s3.public_url).toString(); } return ""; } public getMastodonType(): ApiAttachment["type"] { if (this.data.mimeType.startsWith("image/")) { return "image"; } if (this.data.mimeType.startsWith("video/")) { return "video"; } if (this.data.mimeType.startsWith("audio/")) { return "audio"; } return "unknown"; } public toApiMeta(): ApiAttachment["meta"] { return { width: this.data.width || undefined, height: this.data.height || undefined, fps: this.data.fps || undefined, size: this.data.width && this.data.height ? `${this.data.width}x${this.data.height}` : undefined, duration: this.data.duration || undefined, length: undefined, aspect: this.data.width && this.data.height ? this.data.width / this.data.height : undefined, original: { width: this.data.width || undefined, height: this.data.height || undefined, size: this.data.width && this.data.height ? `${this.data.width}x${this.data.height}` : undefined, aspect: this.data.width && this.data.height ? this.data.width / this.data.height : undefined, }, // Idk whether size or length is the right value }; } public toApi(): ApiAttachment | ApiAsyncAttachment { return { id: this.data.id, type: this.getMastodonType(), url: proxyUrl(this.data.url) ?? "", remote_url: proxyUrl(this.data.remoteUrl), preview_url: proxyUrl(this.data.thumbnailUrl || this.data.url), text_url: null, meta: this.toApiMeta(), description: this.data.description, blurhash: this.data.blurhash, }; } public toVersia(): ContentFormat { return { [this.data.mimeType]: { content: this.data.url, blurhash: this.data.blurhash ?? undefined, description: this.data.description ?? undefined, duration: this.data.duration ?? undefined, fps: this.data.fps ?? undefined, height: this.data.height ?? undefined, size: this.data.size ?? undefined, hash: this.data.sha256 ? { sha256: this.data.sha256, } : undefined, width: this.data.width ?? undefined, }, }; } public static fromVersia( attachmentToConvert: ContentFormat, ): Promise { const key = Object.keys(attachmentToConvert)[0]; const value = attachmentToConvert[key]; return Attachment.insert({ mimeType: key, url: value.content, description: value.description || undefined, duration: value.duration || undefined, fps: value.fps || undefined, height: value.height || undefined, size: value.size || undefined, width: value.width || undefined, sha256: value.hash?.sha256 || undefined, blurhash: value.blurhash || undefined, }); } }