refactor(api): 🎨 Refactor emojis into their own class

This commit is contained in:
Jesse Wierzbinski 2024-06-12 18:52:01 -10:00
parent c61f519a34
commit d8cb1d475b
No known key found for this signature in database
11 changed files with 327 additions and 278 deletions

View file

@ -1,11 +1,12 @@
import { Args } from "@oclif/core"; import { Args } from "@oclif/core";
import chalk from "chalk"; import chalk from "chalk";
import { and, eq, isNull } from "drizzle-orm";
import ora from "ora"; import ora from "ora";
import { BaseCommand } from "~/cli/base"; import { BaseCommand } from "~/cli/base";
import { getUrl } from "~/database/entities/attachment"; import { getUrl } from "~/database/entities/attachment";
import { db } from "~/drizzle/db";
import { Emojis } from "~/drizzle/schema"; import { Emojis } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Emoji } from "~/packages/database-interface/emoji";
import { MediaBackend } from "~/packages/media-manager"; import { MediaBackend } from "~/packages/media-manager";
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> { export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
@ -33,13 +34,12 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
const { args } = await this.parse(EmojiAdd); const { args } = await this.parse(EmojiAdd);
// Check if emoji already exists // Check if emoji already exists
const existingEmoji = await db.query.Emojis.findFirst({ const existingEmoji = await Emoji.fromSql(
where: (Emojis, { eq, and, isNull }) =>
and( and(
eq(Emojis.shortcode, args.shortcode), eq(Emojis.shortcode, args.shortcode),
isNull(Emojis.instanceId), isNull(Emojis.instanceId),
), ),
}); );
if (existingEmoji) { if (existingEmoji) {
this.log( this.log(
@ -115,24 +115,12 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
spinner.succeed(); spinner.succeed();
const emoji = await db await Emoji.insert({
.insert(Emojis)
.values({
shortcode: args.shortcode, shortcode: args.shortcode,
url: getUrl(uploaded.path, config), url: getUrl(uploaded.path, config),
visibleInPicker: true, visibleInPicker: true,
contentType: uploaded.uploadedFile.type, 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);
}
this.log( this.log(
`${chalk.green("✓")} Created emoji ${chalk.green( `${chalk.green("✓")} Created emoji ${chalk.green(

View file

@ -6,9 +6,9 @@ import ora from "ora";
import { unzip } from "unzipit"; import { unzip } from "unzipit";
import { BaseCommand } from "~/cli/base"; import { BaseCommand } from "~/cli/base";
import { getUrl } from "~/database/entities/attachment"; import { getUrl } from "~/database/entities/attachment";
import { db } from "~/drizzle/db";
import { Emojis } from "~/drizzle/schema"; import { Emojis } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Emoji } from "~/packages/database-interface/emoji";
import { MediaBackend } from "~/packages/media-manager"; import { MediaBackend } from "~/packages/media-manager";
type MetaType = { type MetaType = {
@ -130,10 +130,7 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
} as MetaType); } as MetaType);
// Get all emojis that already exist // Get all emojis that already exist
const existingEmojis = await db const existingEmojis = await Emoji.manyFromSql(
.select()
.from(Emojis)
.where(
and( and(
isNull(Emojis.instanceId), isNull(Emojis.instanceId),
inArray( inArray(
@ -145,13 +142,16 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
// Filter out existing emojis // Filter out existing emojis
const newEmojis = meta.emojis.filter( 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 && existingEmojis.length > 0 &&
this.log( this.log(
`${chalk.yellow("⚠")} Emojis with shortcode ${chalk.yellow( `${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`, )} already exist in the database and will not be imported`,
); );
@ -212,15 +212,12 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
continue; continue;
} }
await db await Emoji.insert({
.insert(Emojis)
.values({
shortcode: emoji.emoji.name, shortcode: emoji.emoji.name,
url: getUrl(uploaded.path, config), url: getUrl(uploaded.path, config),
visibleInPicker: true, visibleInPicker: true,
contentType: uploaded.uploadedFile.type, contentType: uploaded.uploadedFile.type,
}) });
.execute();
successfullyImported.push(emoji); successfullyImported.push(emoji);
} }

View file

@ -1,11 +1,7 @@
import { emojiValidatorWithColons } from "@/api"; import { emojiValidatorWithColons } from "@/api";
import { proxyUrl } from "@/response"; import { type InferSelectModel, inArray } from "drizzle-orm";
import type { EntityValidator } from "@lysand-org/federation"; import { Emojis, type Instances } from "~/drizzle/schema";
import { type InferSelectModel, and, eq } from "drizzle-orm"; import { Emoji } from "~/packages/database-interface/emoji";
import { db } from "~/drizzle/db";
import { Emojis, Instances } from "~/drizzle/schema";
import type { Emoji as apiEmoji } from "~/types/mastodon/emoji";
import { addInstanceIfNotExists } from "./instance";
export type EmojiWithInstance = InferSelectModel<typeof Emojis> & { export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
instance: InferSelectModel<typeof Instances> | null; instance: InferSelectModel<typeof Instances> | null;
@ -16,105 +12,16 @@ export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
* @param text The text to parse * @param text The text to parse
* @returns An array of emojis * @returns An array of emojis
*/ */
export const parseEmojis = async (text: string) => { export const parseEmojis = async (text: string): Promise<Emoji[]> => {
const matches = text.match(emojiValidatorWithColons); const matches = text.match(emojiValidatorWithColons);
if (!matches) { if (!matches || matches.length === 0) {
return []; return [];
} }
const emojis = await db.query.Emojis.findMany({
where: (emoji, { eq, or }) => return Emoji.manyFromSql(
or( inArray(
...matches Emojis.shortcode,
.map((match) => match.replace(/:/g, "")) 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<EmojiWithInstance> => {
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,
},
},
};
}; };

View file

@ -10,7 +10,7 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Attachments } from "~/drizzle/schema"; 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 type { Attachment as APIAttachment } from "~/types/mastodon/attachment";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
@ -21,7 +21,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
const reloaded = await Attachment.fromId(this.data.id); const reloaded = await Attachment.fromId(this.data.id);
if (!reloaded) { if (!reloaded) {
throw new Error("Failed to reload role"); throw new Error("Failed to reload attachment");
} }
this.data = reloaded.data; this.data = reloaded.data;
@ -83,7 +83,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
const updated = await Attachment.fromId(this.data.id); const updated = await Attachment.fromId(this.data.id);
if (!updated) { if (!updated) {
throw new Error("Failed to update role"); throw new Error("Failed to update attachment");
} }
this.data = updated.data; this.data = updated.data;
@ -111,13 +111,13 @@ export class Attachment extends BaseInterface<typeof Attachments> {
await db.insert(Attachments).values(data).returning() await db.insert(Attachments).values(data).returning()
)[0]; )[0];
const role = await Attachment.fromId(inserted.id); const attachment = await Attachment.fromId(inserted.id);
if (!role) { if (!attachment) {
throw new Error("Failed to insert role"); throw new Error("Failed to insert attachment");
} }
return role; return attachment;
} }
get id() { get id() {
@ -169,7 +169,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
}; };
} }
public toApi(): APIAttachment | apiAsyncAttachment { public toApi(): APIAttachment | APIAsyncAttachment {
return { return {
id: this.data.id, id: this.data.id,
type: this.getMastodonType(), type: this.getMastodonType(),

View file

@ -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<typeof Emojis, EmojiWithInstance> {
async reload(): Promise<void> {
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<Emoji | null> {
if (!id) {
return null;
}
return await Emoji.fromSql(eq(Emojis.id, id));
}
public static async fromIds(ids: string[]): Promise<Emoji[]> {
return await Emoji.manyFromSql(inArray(Emojis.id, ids));
}
public static async fromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Emojis.id),
): Promise<Emoji | null> {
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<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Emojis.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.Emojis.findMany>[0],
): Promise<Emoji[]> {
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<EmojiWithInstance>,
): Promise<EmojiWithInstance> {
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<EmojiWithInstance> {
return this.update(this.data);
}
async delete(ids: string[]): Promise<void>;
async delete(): Promise<void>;
async delete(ids?: unknown): Promise<void> {
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<typeof Emojis>,
): Promise<Emoji> {
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<Emoji> {
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<Emoji> {
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,
});
}
}

View file

@ -21,13 +21,7 @@ import {
type Application, type Application,
applicationToApi, applicationToApi,
} from "~/database/entities/application"; } from "~/database/entities/application";
import { import { parseEmojis } from "~/database/entities/emoji";
type EmojiWithInstance,
emojiToApi,
emojiToLysand,
fetchEmoji,
parseEmojis,
} from "~/database/entities/emoji";
import { localObjectUri } from "~/database/entities/federation"; import { localObjectUri } from "~/database/entities/federation";
import { import {
type StatusWithRelations, 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 type { Status as apiStatus } from "~/types/mastodon/status";
import { Attachment } from "./attachment"; import { Attachment } from "./attachment";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
import { Emoji } from "./emoji";
import { User } from "./user"; import { User } from "./user";
/** /**
@ -256,7 +251,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
visibility: apiStatus["visibility"]; visibility: apiStatus["visibility"];
isSensitive: boolean; isSensitive: boolean;
spoilerText: string; spoilerText: string;
emojis?: EmojiWithInstance[]; emojis?: Emoji[];
uri?: string; uri?: string;
mentions?: User[]; mentions?: User[];
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
@ -341,7 +336,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
visibility?: apiStatus["visibility"]; visibility?: apiStatus["visibility"];
isSensitive?: boolean; isSensitive?: boolean;
spoilerText?: string; spoilerText?: string;
emojis?: EmojiWithInstance[]; emojis?: Emoji[];
uri?: string; uri?: string;
mentions?: User[]; mentions?: User[];
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
@ -410,9 +405,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
return this; return this;
} }
public async recalculateDatabaseEmojis( public async recalculateDatabaseEmojis(emojis: Emoji[]): Promise<void> {
emojis: EmojiWithInstance[],
): Promise<void> {
// Fuse and deduplicate // Fuse and deduplicate
const fusedEmojis = emojis.filter( const fusedEmojis = emojis.filter(
(emoji, index, self) => (emoji, index, self) =>
@ -546,14 +539,16 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
for (const emoji of note.extensions?.["org.lysand:custom_emojis"] for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
?.emojis ?? []) { ?.emojis ?? []) {
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => { const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch(
(e) => {
dualLogger.logError( dualLogger.logError(
LogLevel.Error, LogLevel.Error,
"Federation.StatusResolver", "Federation.StatusResolver",
e, e,
); );
return null; return null;
}); },
);
if (resolvedEmoji) { if (resolvedEmoji) {
emojis.push(resolvedEmoji); emojis.push(resolvedEmoji);
@ -721,7 +716,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
: null, : null,
card: null, card: null,
content: replacedContent, content: replacedContent,
emojis: data.emojis.map((emoji) => emojiToApi(emoji)), emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()),
favourited: data.liked, favourited: data.liked,
favourites_count: data.likeCount, favourites_count: data.likeCount,
media_attachments: (data.attachments ?? []).map( media_attachments: (data.attachments ?? []).map(
@ -816,7 +811,9 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
| "direct", | "direct",
extensions: { extensions: {
"org.lysand:custom_emojis": { "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 // TODO: Add polls and reactions
}, },

View file

@ -20,11 +20,6 @@ import {
sql, sql,
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import {
emojiToApi,
emojiToLysand,
fetchEmoji,
} from "~/database/entities/emoji";
import { objectToInboxRequest } from "~/database/entities/federation"; import { objectToInboxRequest } from "~/database/entities/federation";
import { addInstanceIfNotExists } from "~/database/entities/instance"; import { addInstanceIfNotExists } from "~/database/entities/instance";
import { import {
@ -46,6 +41,7 @@ import { type Config, config } from "~/packages/config-manager";
import type { Account as apiAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import type { Mention as apiMention } from "~/types/mastodon/mention"; import type { Mention as apiMention } from "~/types/mastodon/mention";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
import { Emoji } from "./emoji";
import type { Note } from "./note"; import type { Note } from "./note";
import { Role } from "./role"; import { Role } from "./role";
@ -273,11 +269,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const instance = await addInstanceIfNotExists(data.uri); const instance = await addInstanceIfNotExists(data.uri);
const emojis = []; const emojis = await Promise.all(
userEmojis.map((emoji) => Emoji.fromLysand(emoji, instance.id)),
for (const emoji of userEmojis) { );
emojis.push(await fetchEmoji(emoji));
}
const user = await User.fromLysand(data, instance); const user = await User.fromLysand(data, instance);
@ -580,7 +574,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
followers_count: user.followerCount, followers_count: user.followerCount,
following_count: user.followingCount, following_count: user.followingCount,
statuses_count: user.statusCount, 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) => ({ fields: user.fields.map((field) => ({
name: htmlToText(getBestContentType(field.key).content), name: htmlToText(getBestContentType(field.key).content),
value: getBestContentType(field.value).content, value: getBestContentType(field.value).content,
@ -695,7 +689,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
}, },
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)), emojis: user.emojis.map((emoji) =>
new Emoji(emoji).toLysand(),
),
}, },
}, },
}; };

View file

@ -11,10 +11,11 @@ import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/attachment"; 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 { contentToHtml } from "~/database/entities/status";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { EmojiToUser, RolePermissions } from "~/drizzle/schema"; import { EmojiToUser, RolePermissions } from "~/drizzle/schema";
import type { Emoji } from "~/packages/database-interface/emoji";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({ export const meta = applyConfig({
@ -222,7 +223,7 @@ export default (app: Hono) =>
self.isDiscoverable = discoverable; self.isDiscoverable = discoverable;
} }
const fieldEmojis: EmojiWithInstance[] = []; const fieldEmojis: Emoji[] = [];
if (fields_attributes) { if (fields_attributes) {
self.fields = []; self.fields = [];
@ -280,7 +281,11 @@ export default (app: Hono) =>
const displaynameEmojis = await parseEmojis(sanitizedDisplayName); const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
const noteEmojis = await parseEmojis(self.note); const noteEmojis = await parseEmojis(self.note);
self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis]; self.emojis = [
...displaynameEmojis,
...noteEmojis,
...fieldEmojis,
].map((e) => e.data);
// Deduplicate emojis // Deduplicate emojis
self.emojis = self.emojis.filter( self.emojis = self.emojis.filter(

View file

@ -1,9 +1,9 @@
import { applyConfig, auth } from "@/api"; import { applyConfig, auth } from "@/api";
import { jsonResponse } from "@/response"; import { jsonResponse } from "@/response";
import { and, eq, isNull, or } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { emojiToApi } from "~/database/entities/emoji"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { db } from "~/drizzle/db"; import { Emoji } from "~/packages/database-interface/emoji";
import { RolePermissions } from "~/drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -28,22 +28,16 @@ export default (app: Hono) =>
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
const emojis = await db.query.Emojis.findMany({ const emojis = await Emoji.manyFromSql(
where: (emoji, { isNull, and, eq, or }) =>
and( and(
isNull(emoji.instanceId), isNull(Emojis.instanceId),
or( or(
isNull(emoji.ownerId), isNull(Emojis.ownerId),
user ? eq(emoji.ownerId, user.id) : undefined, user ? eq(Emojis.ownerId, user.id) : undefined,
), ),
), ),
with: { );
instance: true,
},
});
return jsonResponse( return jsonResponse(emojis.map((emoji) => emoji.toApi()));
await Promise.all(emojis.map((emoji) => emojiToApi(emoji))),
);
}, },
); );

View file

@ -12,10 +12,10 @@ import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/attachment"; import { getUrl } from "~/database/entities/attachment";
import { emojiToApi } from "~/database/entities/emoji";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis, RolePermissions } from "~/drizzle/schema"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Emoji } from "~/packages/database-interface/emoji";
import { MediaBackend } from "~/packages/media-manager"; import { MediaBackend } from "~/packages/media-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -83,12 +83,7 @@ export default (app: Hono) =>
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
const emoji = await db.query.Emojis.findFirst({ const emoji = await Emoji.fromId(id);
where: (emoji, { eq }) => eq(emoji.id, id),
with: {
instance: true,
},
});
if (!emoji) { if (!emoji) {
return errorResponse("Emoji not found", 404); return errorResponse("Emoji not found", 404);
@ -97,7 +92,7 @@ export default (app: Hono) =>
// Check if user is admin // Check if user is admin
if ( if (
!user.hasPermission(RolePermissions.ManageEmojis) && !user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.ownerId !== user.data.id emoji.data.ownerId !== user.data.id
) { ) {
return jsonResponse( return jsonResponse(
{ {
@ -114,7 +109,7 @@ export default (app: Hono) =>
config, config,
); );
await mediaBackend.deleteFileByUrl(emoji.url); await mediaBackend.deleteFileByUrl(emoji.data.url);
await db.delete(Emojis).where(eq(Emojis.id, id)); 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) { if (form.element) {
// Check of emoji is an image // Check of emoji is an image
let contentType = let contentType =
@ -188,35 +185,22 @@ export default (app: Hono) =>
url = form.element; url = form.element;
} }
emoji.url = getUrl(url, config); modified.url = getUrl(url, config);
emoji.contentType = contentType; modified.contentType = contentType;
} }
const newEmoji = ( modified.shortcode = form.shortcode ?? modified.shortcode;
await db modified.alt = form.alt ?? modified.alt;
.update(Emojis) modified.category = form.category ?? modified.category;
.set({ modified.ownerId = form.global ? null : user.data.id;
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];
return jsonResponse( await emoji.update(modified);
emojiToApi({
...newEmoji, return jsonResponse(emoji.toApi());
instance: null,
}),
);
} }
case "GET": { case "GET": {
return jsonResponse(emojiToApi(emoji)); return jsonResponse(emoji.toApi());
} }
} }
}, },

View file

@ -8,13 +8,13 @@ import {
import { mimeLookup } from "@/content_types"; import { mimeLookup } from "@/content_types";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq, isNull, or } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/attachment"; import { getUrl } from "~/database/entities/attachment";
import { emojiToApi } from "~/database/entities/emoji";
import { db } from "~/drizzle/db";
import { Emojis, RolePermissions } from "~/drizzle/schema"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Emoji } from "~/packages/database-interface/emoji";
import { MediaBackend } from "~/packages/media-manager"; import { MediaBackend } from "~/packages/media-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -84,14 +84,13 @@ export default (app: Hono) =>
} }
// Check if emoji already exists // Check if emoji already exists
const existing = await db.query.Emojis.findFirst({ const existing = await Emoji.fromSql(
where: (emoji, { eq, and, isNull, or }) =>
and( and(
eq(emoji.shortcode, shortcode), eq(Emojis.shortcode, shortcode),
isNull(emoji.instanceId), isNull(Emojis.instanceId),
or(eq(emoji.ownerId, user.id), isNull(emoji.ownerId)), or(eq(Emojis.ownerId, user.id), isNull(Emojis.ownerId)),
), ),
}); );
if (existing) { if (existing) {
return errorResponse( return errorResponse(
@ -129,10 +128,7 @@ export default (app: Hono) =>
url = element; url = element;
} }
const emoji = ( const emoji = await Emoji.insert({
await db
.insert(Emojis)
.values({
shortcode, shortcode,
url: getUrl(url, config), url: getUrl(url, config),
visibleInPicker: true, visibleInPicker: true,
@ -140,15 +136,8 @@ export default (app: Hono) =>
category, category,
contentType, contentType,
alt, alt,
}) });
.returning()
)[0];
return jsonResponse( return jsonResponse(emoji.toApi());
emojiToApi({
...emoji,
instance: null,
}),
);
}, },
); );