2024-08-26 19:27:40 +02:00
|
|
|
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
2024-06-13 06:52:01 +02:00
|
|
|
import { proxyUrl } from "@/response";
|
2024-11-29 21:49:41 +01:00
|
|
|
import type { Emoji as APIEmoji } from "@versia/client/types";
|
2024-08-26 19:06:49 +02:00
|
|
|
import type { CustomEmojiExtension } from "@versia/federation/types";
|
2025-01-28 17:43:43 +01:00
|
|
|
import { type Instance, Media, db } from "@versia/kit/db";
|
|
|
|
|
import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
|
2024-06-13 06:52:01 +02:00
|
|
|
import {
|
|
|
|
|
type InferInsertModel,
|
2024-07-26 18:51:39 +02:00
|
|
|
type InferSelectModel,
|
2024-06-13 06:52:01 +02:00
|
|
|
type SQL,
|
|
|
|
|
and,
|
|
|
|
|
desc,
|
|
|
|
|
eq,
|
|
|
|
|
inArray,
|
2024-12-09 13:11:23 +01:00
|
|
|
isNull,
|
2024-06-13 06:52:01 +02:00
|
|
|
} from "drizzle-orm";
|
2024-08-27 17:40:58 +02:00
|
|
|
import { z } from "zod";
|
2024-10-04 15:22:48 +02:00
|
|
|
import { BaseInterface } from "./base.ts";
|
2024-06-13 06:52:01 +02:00
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
type EmojiType = InferSelectModel<typeof Emojis> & {
|
|
|
|
|
media: InferSelectModel<typeof Medias>;
|
2024-07-26 18:51:39 +02:00
|
|
|
instance: InferSelectModel<typeof Instances> | null;
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
2024-11-01 21:20:12 +01:00
|
|
|
public static schema = z.object({
|
2024-11-10 15:24:34 +01:00
|
|
|
id: z.string(),
|
2024-08-27 17:40:58 +02:00
|
|
|
shortcode: z.string(),
|
|
|
|
|
url: z.string(),
|
|
|
|
|
visible_in_picker: z.boolean(),
|
|
|
|
|
category: z.string().optional(),
|
|
|
|
|
static_url: z.string(),
|
2024-12-02 15:07:05 +01:00
|
|
|
global: z.boolean(),
|
2024-08-27 17:40:58 +02:00
|
|
|
});
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
public static $type: EmojiType;
|
|
|
|
|
public media: Media;
|
|
|
|
|
|
|
|
|
|
public constructor(data: EmojiType) {
|
|
|
|
|
super(data);
|
|
|
|
|
this.media = new Media(data.media);
|
|
|
|
|
}
|
2024-11-04 14:58:17 +01:00
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public async reload(): Promise<void> {
|
2024-06-13 06:52:01 +02:00
|
|
|
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,
|
2025-01-28 17:43:43 +01:00
|
|
|
media: true,
|
2024-06-13 06:52:01 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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,
|
2025-01-28 17:43:43 +01:00
|
|
|
with: { ...extra?.with, instance: true, media: true },
|
2024-06-13 06:52:01 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return found.map((s) => new Emoji(s));
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
public async update(newEmoji: Partial<EmojiType>): Promise<EmojiType> {
|
2024-06-13 06:52:01 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
public save(): Promise<EmojiType> {
|
2024-06-13 06:52:01 +02:00
|
|
|
return this.update(this.data);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public async delete(ids?: string[]): Promise<void> {
|
2024-06-13 06:52:01 +02:00
|
|
|
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(
|
2024-06-20 01:21:02 +02:00
|
|
|
emojiToFetch: CustomEmojiExtension["emojis"][0],
|
2024-12-09 13:11:23 +01:00
|
|
|
instance: Instance,
|
2024-06-13 06:52:01 +02:00
|
|
|
): Promise<Emoji> {
|
2024-12-09 15:01:19 +01:00
|
|
|
const existingEmoji = await Emoji.fromSql(
|
|
|
|
|
and(
|
|
|
|
|
eq(Emojis.shortcode, emojiToFetch.name),
|
|
|
|
|
eq(Emojis.instanceId, instance.id),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingEmoji) {
|
|
|
|
|
return existingEmoji;
|
2024-06-13 06:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-09 13:11:23 +01:00
|
|
|
return await Emoji.fromVersia(emojiToFetch, instance);
|
2024-06-13 06:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
public get id(): string {
|
2024-06-13 06:52:01 +02:00
|
|
|
return this.data.id;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-26 18:51:39 +02:00
|
|
|
/**
|
|
|
|
|
* Parse emojis from text
|
|
|
|
|
*
|
|
|
|
|
* @param text The text to parse
|
|
|
|
|
* @returns An array of emojis
|
|
|
|
|
*/
|
2024-10-03 11:43:16 +02:00
|
|
|
public static parseFromText(text: string): Promise<Emoji[]> {
|
2024-07-26 18:51:39 +02:00
|
|
|
const matches = text.match(emojiValidatorWithColons);
|
|
|
|
|
if (!matches || matches.length === 0) {
|
2024-10-03 11:43:16 +02:00
|
|
|
return Promise.resolve([]);
|
2024-07-26 18:51:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Emoji.manyFromSql(
|
2024-12-09 13:11:23 +01:00
|
|
|
and(
|
|
|
|
|
inArray(
|
|
|
|
|
Emojis.shortcode,
|
|
|
|
|
matches.map((match) => match.replace(/:/g, "")),
|
|
|
|
|
),
|
|
|
|
|
isNull(Emojis.instanceId),
|
2024-07-26 18:51:39 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 21:49:41 +01:00
|
|
|
public toApi(): APIEmoji {
|
2024-06-13 06:52:01 +02:00
|
|
|
return {
|
|
|
|
|
id: this.id,
|
|
|
|
|
shortcode: this.data.shortcode,
|
2025-01-28 17:43:43 +01:00
|
|
|
static_url: proxyUrl(this.media.getUrl()) ?? "", // TODO: Add static version
|
|
|
|
|
url: proxyUrl(this.media.getUrl()) ?? "",
|
2024-06-13 06:52:01 +02:00
|
|
|
visible_in_picker: this.data.visibleInPicker,
|
|
|
|
|
category: this.data.category ?? undefined,
|
2024-11-29 21:49:41 +01:00
|
|
|
global: this.data.ownerId === null,
|
2025-01-28 17:43:43 +01:00
|
|
|
description:
|
|
|
|
|
this.media.data.content[this.media.getPreferredMimeType()]
|
|
|
|
|
.description ?? undefined,
|
2024-06-13 06:52:01 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-19 15:16:01 +02:00
|
|
|
public toVersia(): CustomEmojiExtension["emojis"][0] {
|
2024-06-13 06:52:01 +02:00
|
|
|
return {
|
2024-08-26 19:27:40 +02:00
|
|
|
name: `:${this.data.shortcode}:`,
|
2025-01-28 17:43:43 +01:00
|
|
|
url: this.media.toVersia(),
|
2024-06-13 06:52:01 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
public static async fromVersia(
|
2024-06-20 01:21:02 +02:00
|
|
|
emoji: CustomEmojiExtension["emojis"][0],
|
2024-12-09 13:11:23 +01:00
|
|
|
instance: Instance,
|
2024-06-13 06:52:01 +02:00
|
|
|
): Promise<Emoji> {
|
2024-08-26 19:27:40 +02:00
|
|
|
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
|
|
|
|
const shortcode = [
|
|
|
|
|
...emoji.name.matchAll(emojiValidatorWithIdentifiers),
|
|
|
|
|
][0].groups.shortcode;
|
|
|
|
|
|
|
|
|
|
if (!shortcode) {
|
|
|
|
|
throw new Error("Could not extract shortcode from emoji name");
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 17:43:43 +01:00
|
|
|
const media = await Media.fromVersia(emoji.url);
|
|
|
|
|
|
2024-06-13 06:52:01 +02:00
|
|
|
return Emoji.insert({
|
2024-08-26 19:27:40 +02:00
|
|
|
shortcode,
|
2025-01-28 17:43:43 +01:00
|
|
|
mediaId: media.id,
|
2024-06-13 06:52:01 +02:00
|
|
|
visibleInPicker: true,
|
2024-12-09 13:11:23 +01:00
|
|
|
instanceId: instance.id,
|
2024-06-13 06:52:01 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|