mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(api): 🎨 Refactor emojis into their own class
This commit is contained in:
parent
c61f519a34
commit
d8cb1d475b
|
|
@ -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)
|
shortcode: args.shortcode,
|
||||||
.values({
|
url: getUrl(uploaded.path, config),
|
||||||
shortcode: args.shortcode,
|
visibleInPicker: true,
|
||||||
url: getUrl(uploaded.path, config),
|
contentType: uploaded.uploadedFile.type,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(
|
this.log(
|
||||||
`${chalk.green("✓")} Created emoji ${chalk.green(
|
`${chalk.green("✓")} Created emoji ${chalk.green(
|
||||||
|
|
|
||||||
|
|
@ -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,28 +130,28 @@ 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()
|
and(
|
||||||
.from(Emojis)
|
isNull(Emojis.instanceId),
|
||||||
.where(
|
inArray(
|
||||||
and(
|
Emojis.shortcode,
|
||||||
isNull(Emojis.instanceId),
|
meta.emojis.map((e) => e.emoji.name),
|
||||||
inArray(
|
|
||||||
Emojis.shortcode,
|
|
||||||
meta.emojis.map((e) => e.emoji.name),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 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)
|
shortcode: emoji.emoji.name,
|
||||||
.values({
|
url: getUrl(uploaded.path, config),
|
||||||
shortcode: emoji.emoji.name,
|
visibleInPicker: true,
|
||||||
url: getUrl(uploaded.path, config),
|
contentType: uploaded.uploadedFile.type,
|
||||||
visibleInPicker: true,
|
});
|
||||||
contentType: uploaded.uploadedFile.type,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
successfullyImported.push(emoji);
|
successfullyImported.push(emoji);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }) =>
|
|
||||||
or(
|
|
||||||
...matches
|
|
||||||
.map((match) => match.replace(/:/g, ""))
|
|
||||||
.map((match) => eq(emoji.shortcode, match)),
|
|
||||||
),
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return emojis;
|
return Emoji.manyFromSql(
|
||||||
};
|
inArray(
|
||||||
|
Emojis.shortcode,
|
||||||
/**
|
matches.map((match) => match.replace(/:/g, "")),
|
||||||
* 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
192
packages/database-interface/emoji.ts
Normal file
192
packages/database-interface/emoji.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
dualLogger.logError(
|
(e) => {
|
||||||
LogLevel.Error,
|
dualLogger.logError(
|
||||||
"Federation.StatusResolver",
|
LogLevel.Error,
|
||||||
e,
|
"Federation.StatusResolver",
|
||||||
);
|
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
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(Emojis.instanceId),
|
||||||
isNull(emoji.instanceId),
|
or(
|
||||||
or(
|
isNull(Emojis.ownerId),
|
||||||
isNull(emoji.ownerId),
|
user ? eq(Emojis.ownerId, user.id) : undefined,
|
||||||
user ? eq(emoji.ownerId, user.id) : undefined,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
with: {
|
),
|
||||||
instance: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return jsonResponse(
|
|
||||||
await Promise.all(emojis.map((emoji) => emojiToApi(emoji))),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return jsonResponse(emojis.map((emoji) => emoji.toApi()));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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(Emojis.shortcode, shortcode),
|
||||||
eq(emoji.shortcode, shortcode),
|
isNull(Emojis.instanceId),
|
||||||
isNull(emoji.instanceId),
|
or(eq(Emojis.ownerId, user.id), isNull(Emojis.ownerId)),
|
||||||
or(eq(emoji.ownerId, user.id), isNull(emoji.ownerId)),
|
),
|
||||||
),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
|
|
@ -129,26 +128,16 @@ export default (app: Hono) =>
|
||||||
url = element;
|
url = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emoji = (
|
const emoji = await Emoji.insert({
|
||||||
await db
|
shortcode,
|
||||||
.insert(Emojis)
|
url: getUrl(url, config),
|
||||||
.values({
|
visibleInPicker: true,
|
||||||
shortcode,
|
ownerId: global ? null : user.id,
|
||||||
url: getUrl(url, config),
|
category,
|
||||||
visibleInPicker: true,
|
contentType,
|
||||||
ownerId: global ? null : user.id,
|
alt,
|
||||||
category,
|
});
|
||||||
contentType,
|
|
||||||
alt,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(emoji.toApi());
|
||||||
emojiToApi({
|
|
||||||
...emoji,
|
|
||||||
instance: null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue