From d8cb1d475b7e0203f77b2c0bd09574a50f57c211 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 12 Jun 2024 18:52:01 -1000 Subject: [PATCH] refactor(api): :art: Refactor emojis into their own class --- cli/commands/emoji/add.ts | 40 ++-- cli/commands/emoji/import.ts | 43 ++-- database/entities/emoji.ts | 115 +---------- packages/database-interface/attachment.ts | 16 +- packages/database-interface/emoji.ts | 192 ++++++++++++++++++ packages/database-interface/note.ts | 41 ++-- packages/database-interface/user.ts | 20 +- .../v1/accounts/update_credentials/index.ts | 11 +- server/api/api/v1/custom_emojis/index.ts | 30 ++- server/api/api/v1/emojis/:id/index.ts | 48 ++--- server/api/api/v1/emojis/index.ts | 49 ++--- 11 files changed, 327 insertions(+), 278 deletions(-) create mode 100644 packages/database-interface/emoji.ts diff --git a/cli/commands/emoji/add.ts b/cli/commands/emoji/add.ts index 9757178c..ff8a9dd8 100644 --- a/cli/commands/emoji/add.ts +++ b/cli/commands/emoji/add.ts @@ -1,11 +1,12 @@ import { Args } from "@oclif/core"; import chalk from "chalk"; +import { and, eq, isNull } from "drizzle-orm"; import ora from "ora"; import { BaseCommand } from "~/cli/base"; import { getUrl } from "~/database/entities/attachment"; -import { db } from "~/drizzle/db"; import { Emojis } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Emoji } from "~/packages/database-interface/emoji"; import { MediaBackend } from "~/packages/media-manager"; export default class EmojiAdd extends BaseCommand { @@ -33,13 +34,12 @@ export default class EmojiAdd extends BaseCommand { const { args } = await this.parse(EmojiAdd); // Check if emoji already exists - const existingEmoji = await db.query.Emojis.findFirst({ - where: (Emojis, { eq, and, isNull }) => - and( - eq(Emojis.shortcode, args.shortcode), - isNull(Emojis.instanceId), - ), - }); + const existingEmoji = await Emoji.fromSql( + and( + eq(Emojis.shortcode, args.shortcode), + isNull(Emojis.instanceId), + ), + ); if (existingEmoji) { this.log( @@ -115,24 +115,12 @@ export default class EmojiAdd extends BaseCommand { spinner.succeed(); - const emoji = await db - .insert(Emojis) - .values({ - shortcode: args.shortcode, - url: getUrl(uploaded.path, config), - visibleInPicker: true, - contentType: uploaded.uploadedFile.type, - }) - .returning(); - - if (!emoji || emoji.length === 0) { - this.log( - `${chalk.red("✗")} Failed to create emoji ${chalk.red( - args.shortcode, - )}`, - ); - this.exit(1); - } + await Emoji.insert({ + shortcode: args.shortcode, + url: getUrl(uploaded.path, config), + visibleInPicker: true, + contentType: uploaded.uploadedFile.type, + }); this.log( `${chalk.green("✓")} Created emoji ${chalk.green( diff --git a/cli/commands/emoji/import.ts b/cli/commands/emoji/import.ts index 9eae7cf0..19fa5a95 100644 --- a/cli/commands/emoji/import.ts +++ b/cli/commands/emoji/import.ts @@ -6,9 +6,9 @@ import ora from "ora"; import { unzip } from "unzipit"; import { BaseCommand } from "~/cli/base"; import { getUrl } from "~/database/entities/attachment"; -import { db } from "~/drizzle/db"; import { Emojis } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Emoji } from "~/packages/database-interface/emoji"; import { MediaBackend } from "~/packages/media-manager"; type MetaType = { @@ -130,28 +130,28 @@ export default class EmojiImport extends BaseCommand { } as MetaType); // Get all emojis that already exist - const existingEmojis = await db - .select() - .from(Emojis) - .where( - and( - isNull(Emojis.instanceId), - inArray( - Emojis.shortcode, - meta.emojis.map((e) => e.emoji.name), - ), + const existingEmojis = await Emoji.manyFromSql( + and( + isNull(Emojis.instanceId), + inArray( + Emojis.shortcode, + meta.emojis.map((e) => e.emoji.name), ), - ); + ), + ); // Filter out existing emojis const newEmojis = meta.emojis.filter( - (e) => !existingEmojis.find((ee) => ee.shortcode === e.emoji.name), + (e) => + !existingEmojis.find( + (ee) => ee.data.shortcode === e.emoji.name, + ), ); existingEmojis.length > 0 && this.log( `${chalk.yellow("⚠")} Emojis with shortcode ${chalk.yellow( - existingEmojis.map((e) => e.shortcode).join(", "), + existingEmojis.map((e) => e.data.shortcode).join(", "), )} already exist in the database and will not be imported`, ); @@ -212,15 +212,12 @@ export default class EmojiImport extends BaseCommand { continue; } - await db - .insert(Emojis) - .values({ - shortcode: emoji.emoji.name, - url: getUrl(uploaded.path, config), - visibleInPicker: true, - contentType: uploaded.uploadedFile.type, - }) - .execute(); + await Emoji.insert({ + shortcode: emoji.emoji.name, + url: getUrl(uploaded.path, config), + visibleInPicker: true, + contentType: uploaded.uploadedFile.type, + }); successfullyImported.push(emoji); } diff --git a/database/entities/emoji.ts b/database/entities/emoji.ts index 2bbdd034..a2ee0d28 100644 --- a/database/entities/emoji.ts +++ b/database/entities/emoji.ts @@ -1,11 +1,7 @@ import { emojiValidatorWithColons } from "@/api"; -import { proxyUrl } from "@/response"; -import type { EntityValidator } from "@lysand-org/federation"; -import { type InferSelectModel, and, eq } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Emojis, Instances } from "~/drizzle/schema"; -import type { Emoji as apiEmoji } from "~/types/mastodon/emoji"; -import { addInstanceIfNotExists } from "./instance"; +import { type InferSelectModel, inArray } from "drizzle-orm"; +import { Emojis, type Instances } from "~/drizzle/schema"; +import { Emoji } from "~/packages/database-interface/emoji"; export type EmojiWithInstance = InferSelectModel & { instance: InferSelectModel | null; @@ -16,105 +12,16 @@ export type EmojiWithInstance = InferSelectModel & { * @param text The text to parse * @returns An array of emojis */ -export const parseEmojis = async (text: string) => { +export const parseEmojis = async (text: string): Promise => { const matches = text.match(emojiValidatorWithColons); - if (!matches) { + if (!matches || matches.length === 0) { return []; } - const emojis = await db.query.Emojis.findMany({ - where: (emoji, { eq, or }) => - or( - ...matches - .map((match) => match.replace(/:/g, "")) - .map((match) => eq(emoji.shortcode, match)), - ), - with: { - instance: true, - }, - }); - return emojis; -}; - -/** - * Gets an emoji from the database, and fetches it from the remote instance if it doesn't exist. - * @param emoji Emoji to fetch - * @param host Host to fetch the emoji from if remote - * @returns The emoji - */ -export const fetchEmoji = async ( - emojiToFetch: (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0], - host?: string, -): Promise => { - const existingEmoji = await db - .select() - .from(Emojis) - .innerJoin(Instances, eq(Emojis.instanceId, Instances.id)) - .where( - and( - eq(Emojis.shortcode, emojiToFetch.name), - host ? eq(Instances.baseUrl, host) : undefined, - ), - ) - .limit(1); - - if (existingEmoji[0]) { - return { - ...existingEmoji[0].Emojis, - instance: existingEmoji[0].Instances, - }; - } - - const foundInstance = host ? await addInstanceIfNotExists(host) : null; - - const result = ( - await db - .insert(Emojis) - .values({ - shortcode: emojiToFetch.name, - url: Object.entries(emojiToFetch.url)[0][1].content, - alt: - Object.entries(emojiToFetch.url)[0][1].description || - undefined, - contentType: Object.keys(emojiToFetch.url)[0], - visibleInPicker: true, - instanceId: foundInstance?.id, - }) - .returning() - )[0]; - - return { - ...result, - instance: foundInstance, - }; -}; - -/** - * Converts the emoji to an APIEmoji object. - * @returns The APIEmoji object. - */ -export const emojiToApi = (emoji: EmojiWithInstance): apiEmoji => { - return { - // @ts-expect-error ID is not in regular Mastodon API - id: emoji.id, - shortcode: emoji.shortcode, - static_url: proxyUrl(emoji.url) ?? "", // TODO: Add static version - url: proxyUrl(emoji.url) ?? "", - visible_in_picker: emoji.visibleInPicker, - category: emoji.category ?? undefined, - }; -}; - -export const emojiToLysand = ( - emoji: EmojiWithInstance, -): (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0] => { - return { - name: emoji.shortcode, - url: { - [emoji.contentType]: { - content: emoji.url, - description: emoji.alt || undefined, - }, - }, - }; + return Emoji.manyFromSql( + inArray( + Emojis.shortcode, + matches.map((match) => match.replace(/:/g, "")), + ), + ); }; diff --git a/packages/database-interface/attachment.ts b/packages/database-interface/attachment.ts index cb00c458..f4456054 100644 --- a/packages/database-interface/attachment.ts +++ b/packages/database-interface/attachment.ts @@ -10,7 +10,7 @@ import { } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { Attachments } from "~/drizzle/schema"; -import type { AsyncAttachment as apiAsyncAttachment } from "~/types/mastodon/async_attachment"; +import type { AsyncAttachment as APIAsyncAttachment } from "~/types/mastodon/async_attachment"; import type { Attachment as APIAttachment } from "~/types/mastodon/attachment"; import { BaseInterface } from "./base"; @@ -21,7 +21,7 @@ export class Attachment extends BaseInterface { const reloaded = await Attachment.fromId(this.data.id); if (!reloaded) { - throw new Error("Failed to reload role"); + throw new Error("Failed to reload attachment"); } this.data = reloaded.data; @@ -83,7 +83,7 @@ export class Attachment extends BaseInterface { const updated = await Attachment.fromId(this.data.id); if (!updated) { - throw new Error("Failed to update role"); + throw new Error("Failed to update attachment"); } this.data = updated.data; @@ -111,13 +111,13 @@ export class Attachment extends BaseInterface { await db.insert(Attachments).values(data).returning() )[0]; - const role = await Attachment.fromId(inserted.id); + const attachment = await Attachment.fromId(inserted.id); - if (!role) { - throw new Error("Failed to insert role"); + if (!attachment) { + throw new Error("Failed to insert attachment"); } - return role; + return attachment; } get id() { @@ -169,7 +169,7 @@ export class Attachment extends BaseInterface { }; } - public toApi(): APIAttachment | apiAsyncAttachment { + public toApi(): APIAttachment | APIAsyncAttachment { return { id: this.data.id, type: this.getMastodonType(), diff --git a/packages/database-interface/emoji.ts b/packages/database-interface/emoji.ts new file mode 100644 index 00000000..5a232264 --- /dev/null +++ b/packages/database-interface/emoji.ts @@ -0,0 +1,192 @@ +import { proxyUrl } from "@/response"; +import type { EntityValidator } from "@lysand-org/federation"; +import { + type InferInsertModel, + type SQL, + and, + desc, + eq, + inArray, +} from "drizzle-orm"; +import type { EmojiWithInstance } from "~/database/entities/emoji"; +import { addInstanceIfNotExists } from "~/database/entities/instance"; +import { db } from "~/drizzle/db"; +import { Emojis, Instances } from "~/drizzle/schema"; +import type { Emoji as APIEmoji } from "~/types/mastodon/emoji"; +import { BaseInterface } from "./base"; + +export class Emoji extends BaseInterface { + async reload(): Promise { + const reloaded = await Emoji.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload emoji"); + } + + this.data = reloaded.data; + } + + public static async fromId(id: string | null): Promise { + if (!id) { + return null; + } + + return await Emoji.fromSql(eq(Emojis.id, id)); + } + + public static async fromIds(ids: string[]): Promise { + return await Emoji.manyFromSql(inArray(Emojis.id, ids)); + } + + public static async fromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Emojis.id), + ): Promise { + const found = await db.query.Emojis.findFirst({ + where: sql, + orderBy, + with: { + instance: true, + }, + }); + + if (!found) { + return null; + } + return new Emoji(found); + } + + public static async manyFromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Emojis.id), + limit?: number, + offset?: number, + extra?: Parameters[0], + ): Promise { + const found = await db.query.Emojis.findMany({ + where: sql, + orderBy, + limit, + offset, + with: { ...extra?.with, instance: true }, + }); + + return found.map((s) => new Emoji(s)); + } + + async update( + newEmoji: Partial, + ): Promise { + await db.update(Emojis).set(newEmoji).where(eq(Emojis.id, this.id)); + + const updated = await Emoji.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update emoji"); + } + + 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(Emojis).where(inArray(Emojis.id, ids)); + } else { + await db.delete(Emojis).where(eq(Emojis.id, this.id)); + } + } + + public static async insert( + data: InferInsertModel, + ): Promise { + const inserted = (await db.insert(Emojis).values(data).returning())[0]; + + const emoji = await Emoji.fromId(inserted.id); + + if (!emoji) { + throw new Error("Failed to insert emoji"); + } + + return emoji; + } + + public static async fetchFromRemote( + emojiToFetch: (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0], + host?: string, + ): Promise { + const existingEmoji = await db + .select() + .from(Emojis) + .innerJoin(Instances, eq(Emojis.instanceId, Instances.id)) + .where( + and( + eq(Emojis.shortcode, emojiToFetch.name), + host ? eq(Instances.baseUrl, host) : undefined, + ), + ) + .limit(1); + + if (existingEmoji[0]) { + const found = await Emoji.fromId(existingEmoji[0].Emojis.id); + + if (!found) { + throw new Error("Failed to fetch emoji"); + } + + return found; + } + + const foundInstance = host ? await addInstanceIfNotExists(host) : null; + + return await Emoji.fromLysand(emojiToFetch, foundInstance?.id ?? null); + } + + get id() { + return this.data.id; + } + + public toApi(): APIEmoji { + return { + // @ts-expect-error ID is not in regular Mastodon API + id: this.id, + shortcode: this.data.shortcode, + static_url: proxyUrl(this.data.url) ?? "", // TODO: Add static version + url: proxyUrl(this.data.url) ?? "", + visible_in_picker: this.data.visibleInPicker, + category: this.data.category ?? undefined, + }; + } + + public toLysand(): (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0] { + return { + name: this.data.shortcode, + url: { + [this.data.contentType]: { + content: this.data.url, + description: this.data.alt || undefined, + }, + }, + }; + } + + public static fromLysand( + emoji: (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0], + instanceId: string | null, + ): Promise { + return Emoji.insert({ + shortcode: emoji.name, + url: Object.entries(emoji.url)[0][1].content, + alt: Object.entries(emoji.url)[0][1].description || undefined, + contentType: Object.keys(emoji.url)[0], + visibleInPicker: true, + instanceId: instanceId, + }); + } +} diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 9afe2216..7c3454c6 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -21,13 +21,7 @@ import { type Application, applicationToApi, } from "~/database/entities/application"; -import { - type EmojiWithInstance, - emojiToApi, - emojiToLysand, - fetchEmoji, - parseEmojis, -} from "~/database/entities/emoji"; +import { parseEmojis } from "~/database/entities/emoji"; import { localObjectUri } from "~/database/entities/federation"; import { type StatusWithRelations, @@ -49,6 +43,7 @@ import type { Attachment as apiAttachment } from "~/types/mastodon/attachment"; import type { Status as apiStatus } from "~/types/mastodon/status"; import { Attachment } from "./attachment"; import { BaseInterface } from "./base"; +import { Emoji } from "./emoji"; import { User } from "./user"; /** @@ -256,7 +251,7 @@ export class Note extends BaseInterface { visibility: apiStatus["visibility"]; isSensitive: boolean; spoilerText: string; - emojis?: EmojiWithInstance[]; + emojis?: Emoji[]; uri?: string; mentions?: User[]; /** List of IDs of database Attachment objects */ @@ -341,7 +336,7 @@ export class Note extends BaseInterface { visibility?: apiStatus["visibility"]; isSensitive?: boolean; spoilerText?: string; - emojis?: EmojiWithInstance[]; + emojis?: Emoji[]; uri?: string; mentions?: User[]; /** List of IDs of database Attachment objects */ @@ -410,9 +405,7 @@ export class Note extends BaseInterface { return this; } - public async recalculateDatabaseEmojis( - emojis: EmojiWithInstance[], - ): Promise { + public async recalculateDatabaseEmojis(emojis: Emoji[]): Promise { // Fuse and deduplicate const fusedEmojis = emojis.filter( (emoji, index, self) => @@ -546,14 +539,16 @@ export class Note extends BaseInterface { for (const emoji of note.extensions?.["org.lysand:custom_emojis"] ?.emojis ?? []) { - const resolvedEmoji = await fetchEmoji(emoji).catch((e) => { - dualLogger.logError( - LogLevel.Error, - "Federation.StatusResolver", - e, - ); - return null; - }); + const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch( + (e) => { + dualLogger.logError( + LogLevel.Error, + "Federation.StatusResolver", + e, + ); + return null; + }, + ); if (resolvedEmoji) { emojis.push(resolvedEmoji); @@ -721,7 +716,7 @@ export class Note extends BaseInterface { : null, card: null, content: replacedContent, - emojis: data.emojis.map((emoji) => emojiToApi(emoji)), + emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()), favourited: data.liked, favourites_count: data.likeCount, media_attachments: (data.attachments ?? []).map( @@ -816,7 +811,9 @@ export class Note extends BaseInterface { | "direct", extensions: { "org.lysand:custom_emojis": { - emojis: status.emojis.map((emoji) => emojiToLysand(emoji)), + emojis: status.emojis.map((emoji) => + new Emoji(emoji).toLysand(), + ), }, // TODO: Add polls and reactions }, diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 10ca3d0f..246ec50d 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -20,11 +20,6 @@ import { sql, } from "drizzle-orm"; import { htmlToText } from "html-to-text"; -import { - emojiToApi, - emojiToLysand, - fetchEmoji, -} from "~/database/entities/emoji"; import { objectToInboxRequest } from "~/database/entities/federation"; import { addInstanceIfNotExists } from "~/database/entities/instance"; import { @@ -46,6 +41,7 @@ import { type Config, config } from "~/packages/config-manager"; import type { Account as apiAccount } from "~/types/mastodon/account"; import type { Mention as apiMention } from "~/types/mastodon/mention"; import { BaseInterface } from "./base"; +import { Emoji } from "./emoji"; import type { Note } from "./note"; import { Role } from "./role"; @@ -273,11 +269,9 @@ export class User extends BaseInterface { const instance = await addInstanceIfNotExists(data.uri); - const emojis = []; - - for (const emoji of userEmojis) { - emojis.push(await fetchEmoji(emoji)); - } + const emojis = await Promise.all( + userEmojis.map((emoji) => Emoji.fromLysand(emoji, instance.id)), + ); const user = await User.fromLysand(data, instance); @@ -580,7 +574,7 @@ export class User extends BaseInterface { followers_count: user.followerCount, following_count: user.followingCount, statuses_count: user.statusCount, - emojis: user.emojis.map((emoji) => emojiToApi(emoji)), + emojis: user.emojis.map((emoji) => new Emoji(emoji).toApi()), fields: user.fields.map((field) => ({ name: htmlToText(getBestContentType(field.key).content), value: getBestContentType(field.value).content, @@ -695,7 +689,9 @@ export class User extends BaseInterface { }, extensions: { "org.lysand:custom_emojis": { - emojis: user.emojis.map((emoji) => emojiToLysand(emoji)), + emojis: user.emojis.map((emoji) => + new Emoji(emoji).toLysand(), + ), }, }, }; diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 06ed7587..0e505c4e 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -11,10 +11,11 @@ import type { MediaBackend } from "media-manager"; import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { z } from "zod"; import { getUrl } from "~/database/entities/attachment"; -import { type EmojiWithInstance, parseEmojis } from "~/database/entities/emoji"; +import { parseEmojis } from "~/database/entities/emoji"; import { contentToHtml } from "~/database/entities/status"; import { db } from "~/drizzle/db"; import { EmojiToUser, RolePermissions } from "~/drizzle/schema"; +import type { Emoji } from "~/packages/database-interface/emoji"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -222,7 +223,7 @@ export default (app: Hono) => self.isDiscoverable = discoverable; } - const fieldEmojis: EmojiWithInstance[] = []; + const fieldEmojis: Emoji[] = []; if (fields_attributes) { self.fields = []; @@ -280,7 +281,11 @@ export default (app: Hono) => const displaynameEmojis = await parseEmojis(sanitizedDisplayName); const noteEmojis = await parseEmojis(self.note); - self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis]; + self.emojis = [ + ...displaynameEmojis, + ...noteEmojis, + ...fieldEmojis, + ].map((e) => e.data); // Deduplicate emojis self.emojis = self.emojis.filter( diff --git a/server/api/api/v1/custom_emojis/index.ts b/server/api/api/v1/custom_emojis/index.ts index 7b2646ff..1c5c74fb 100644 --- a/server/api/api/v1/custom_emojis/index.ts +++ b/server/api/api/v1/custom_emojis/index.ts @@ -1,9 +1,9 @@ import { applyConfig, auth } from "@/api"; import { jsonResponse } from "@/response"; +import { and, eq, isNull, or } from "drizzle-orm"; import type { Hono } from "hono"; -import { emojiToApi } from "~/database/entities/emoji"; -import { db } from "~/drizzle/db"; -import { RolePermissions } from "~/drizzle/schema"; +import { Emojis, RolePermissions } from "~/drizzle/schema"; +import { Emoji } from "~/packages/database-interface/emoji"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -28,22 +28,16 @@ export default (app: Hono) => async (context) => { const { user } = context.req.valid("header"); - const emojis = await db.query.Emojis.findMany({ - where: (emoji, { isNull, and, eq, or }) => - and( - isNull(emoji.instanceId), - or( - isNull(emoji.ownerId), - user ? eq(emoji.ownerId, user.id) : undefined, - ), + const emojis = await Emoji.manyFromSql( + and( + isNull(Emojis.instanceId), + or( + isNull(Emojis.ownerId), + user ? eq(Emojis.ownerId, user.id) : undefined, ), - with: { - instance: true, - }, - }); - - return jsonResponse( - await Promise.all(emojis.map((emoji) => emojiToApi(emoji))), + ), ); + + return jsonResponse(emojis.map((emoji) => emoji.toApi())); }, ); diff --git a/server/api/api/v1/emojis/:id/index.ts b/server/api/api/v1/emojis/:id/index.ts index 2ee61b37..9709a41e 100644 --- a/server/api/api/v1/emojis/:id/index.ts +++ b/server/api/api/v1/emojis/:id/index.ts @@ -12,10 +12,10 @@ import { eq } from "drizzle-orm"; import type { Hono } from "hono"; import { z } from "zod"; import { getUrl } from "~/database/entities/attachment"; -import { emojiToApi } from "~/database/entities/emoji"; import { db } from "~/drizzle/db"; import { Emojis, RolePermissions } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Emoji } from "~/packages/database-interface/emoji"; import { MediaBackend } from "~/packages/media-manager"; export const meta = applyConfig({ @@ -83,12 +83,7 @@ export default (app: Hono) => return errorResponse("Unauthorized", 401); } - const emoji = await db.query.Emojis.findFirst({ - where: (emoji, { eq }) => eq(emoji.id, id), - with: { - instance: true, - }, - }); + const emoji = await Emoji.fromId(id); if (!emoji) { return errorResponse("Emoji not found", 404); @@ -97,7 +92,7 @@ export default (app: Hono) => // Check if user is admin if ( !user.hasPermission(RolePermissions.ManageEmojis) && - emoji.ownerId !== user.data.id + emoji.data.ownerId !== user.data.id ) { return jsonResponse( { @@ -114,7 +109,7 @@ export default (app: Hono) => config, ); - await mediaBackend.deleteFileByUrl(emoji.url); + await mediaBackend.deleteFileByUrl(emoji.data.url); await db.delete(Emojis).where(eq(Emojis.id, id)); @@ -156,6 +151,8 @@ export default (app: Hono) => ); } + const modified = structuredClone(emoji.data); + if (form.element) { // Check of emoji is an image let contentType = @@ -188,35 +185,22 @@ export default (app: Hono) => url = form.element; } - emoji.url = getUrl(url, config); - emoji.contentType = contentType; + modified.url = getUrl(url, config); + modified.contentType = contentType; } - const newEmoji = ( - await db - .update(Emojis) - .set({ - shortcode: form.shortcode ?? emoji.shortcode, - alt: form.alt ?? emoji.alt, - url: emoji.url, - ownerId: form.global ? null : user.id, - contentType: emoji.contentType, - category: form.category ?? emoji.category, - }) - .where(eq(Emojis.id, id)) - .returning() - )[0]; + modified.shortcode = form.shortcode ?? modified.shortcode; + modified.alt = form.alt ?? modified.alt; + modified.category = form.category ?? modified.category; + modified.ownerId = form.global ? null : user.data.id; - return jsonResponse( - emojiToApi({ - ...newEmoji, - instance: null, - }), - ); + await emoji.update(modified); + + return jsonResponse(emoji.toApi()); } case "GET": { - return jsonResponse(emojiToApi(emoji)); + return jsonResponse(emoji.toApi()); } } }, diff --git a/server/api/api/v1/emojis/index.ts b/server/api/api/v1/emojis/index.ts index 48c8cd24..32b29029 100644 --- a/server/api/api/v1/emojis/index.ts +++ b/server/api/api/v1/emojis/index.ts @@ -8,13 +8,13 @@ import { import { mimeLookup } from "@/content_types"; import { errorResponse, jsonResponse } from "@/response"; import { zValidator } from "@hono/zod-validator"; +import { and, eq, isNull, or } from "drizzle-orm"; import type { Hono } from "hono"; import { z } from "zod"; import { getUrl } from "~/database/entities/attachment"; -import { emojiToApi } from "~/database/entities/emoji"; -import { db } from "~/drizzle/db"; import { Emojis, RolePermissions } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Emoji } from "~/packages/database-interface/emoji"; import { MediaBackend } from "~/packages/media-manager"; export const meta = applyConfig({ @@ -84,14 +84,13 @@ export default (app: Hono) => } // Check if emoji already exists - const existing = await db.query.Emojis.findFirst({ - where: (emoji, { eq, and, isNull, or }) => - and( - eq(emoji.shortcode, shortcode), - isNull(emoji.instanceId), - or(eq(emoji.ownerId, user.id), isNull(emoji.ownerId)), - ), - }); + const existing = await Emoji.fromSql( + and( + eq(Emojis.shortcode, shortcode), + isNull(Emojis.instanceId), + or(eq(Emojis.ownerId, user.id), isNull(Emojis.ownerId)), + ), + ); if (existing) { return errorResponse( @@ -129,26 +128,16 @@ export default (app: Hono) => url = element; } - const emoji = ( - await db - .insert(Emojis) - .values({ - shortcode, - url: getUrl(url, config), - visibleInPicker: true, - ownerId: global ? null : user.id, - category, - contentType, - alt, - }) - .returning() - )[0]; + const emoji = await Emoji.insert({ + shortcode, + url: getUrl(url, config), + visibleInPicker: true, + ownerId: global ? null : user.id, + category, + contentType, + alt, + }); - return jsonResponse( - emojiToApi({ - ...emoji, - instance: null, - }), - ); + return jsonResponse(emoji.toApi()); }, );