import { z } from "@hono/zod-openapi"; import { RolePermission } from "@versia/client/types"; import type { Delete, LikeExtension } from "@versia/federation/types"; import { db } from "@versia/kit/db"; import { Likes, type Notes, Notifications, type Users, } from "@versia/kit/tables"; import { type InferInsertModel, type InferSelectModel, type SQL, and, desc, eq, inArray, } from "drizzle-orm"; import { config } from "~/config.ts"; import { BaseInterface } from "./base.ts"; import { User } from "./user.ts"; type LikeType = InferSelectModel & { liker: InferSelectModel; liked: InferSelectModel; }; export class Like extends BaseInterface { public static schema = z.object({ id: z.string(), name: z.string(), permissions: z.array(z.nativeEnum(RolePermission)), priority: z.number(), description: z.string().nullable(), visible: z.boolean(), icon: z.string().nullable(), }); public static $type: LikeType; public async reload(): Promise { const reloaded = await Like.fromId(this.data.id); if (!reloaded) { throw new Error("Failed to reload like"); } this.data = reloaded.data; } public static async fromId(id: string | null): Promise { if (!id) { return null; } return await Like.fromSql(eq(Likes.id, id)); } public static async fromIds(ids: string[]): Promise { return await Like.manyFromSql(inArray(Likes.id, ids)); } public static async fromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Likes.id), ): Promise { const found = await db.query.Likes.findFirst({ where: sql, orderBy, with: { liked: true, liker: true, }, }); if (!found) { return null; } return new Like(found); } public static async manyFromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Likes.id), limit?: number, offset?: number, extra?: Parameters[0], ): Promise { const found = await db.query.Likes.findMany({ where: sql, orderBy, limit, offset, with: { liked: true, liker: true, ...extra?.with, }, }); return found.map((s) => new Like(s)); } public async update(newRole: Partial): Promise { await db.update(Likes).set(newRole).where(eq(Likes.id, this.id)); const updated = await Like.fromId(this.data.id); if (!updated) { throw new Error("Failed to update like"); } return updated.data; } public save(): Promise { return this.update(this.data); } public async delete(ids?: string[]): Promise { if (Array.isArray(ids)) { await db.delete(Likes).where(inArray(Likes.id, ids)); } else { await db.delete(Likes).where(eq(Likes.id, this.id)); } } public static async insert( data: InferInsertModel, ): Promise { const inserted = (await db.insert(Likes).values(data).returning())[0]; const role = await Like.fromId(inserted.id); if (!role) { throw new Error("Failed to insert like"); } return role; } public get id(): string { return this.data.id; } public async clearRelatedNotifications(): Promise { await db .delete(Notifications) .where( and( eq(Notifications.accountId, this.id), eq(Notifications.type, "favourite"), eq(Notifications.notifiedId, this.data.liked.authorId), eq(Notifications.noteId, this.data.liked.id), ), ); } public getUri(): URL { return new URL(`/objects/${this.data.id}`, config.http.base_url); } public toVersia(): LikeExtension { return { id: this.data.id, author: User.getUri( this.data.liker.id, this.data.liker.uri ? new URL(this.data.liker.uri) : null, ).toString(), type: "pub.versia:likes/Like", created_at: new Date(this.data.createdAt).toISOString(), liked: this.data.liked.uri ?? new URL(`/notes/${this.data.liked.id}`, config.http.base_url) .href, uri: this.getUri().toString(), }; } public unlikeToVersia(unliker?: User): Delete { return { type: "Delete", id: crypto.randomUUID(), created_at: new Date().toISOString(), author: User.getUri( unliker?.id ?? this.data.liker.id, unliker?.data.uri ? new URL(unliker.data.uri) : this.data.liker.uri ? new URL(this.data.liker.uri) : null, ).toString(), deleted_type: "pub.versia:likes/Like", deleted: this.getUri().toString(), }; } }