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

@ -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<typeof Attachments> {
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<typeof Attachments> {
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<typeof Attachments> {
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<typeof Attachments> {
};
}
public toApi(): APIAttachment | apiAsyncAttachment {
public toApi(): APIAttachment | APIAsyncAttachment {
return {
id: this.data.id,
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,
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<typeof Notes, StatusWithRelations> {
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<typeof Notes, StatusWithRelations> {
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<typeof Notes, StatusWithRelations> {
return this;
}
public async recalculateDatabaseEmojis(
emojis: EmojiWithInstance[],
): Promise<void> {
public async recalculateDatabaseEmojis(emojis: Emoji[]): Promise<void> {
// Fuse and deduplicate
const fusedEmojis = emojis.filter(
(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"]
?.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<typeof Notes, StatusWithRelations> {
: 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<typeof Notes, StatusWithRelations> {
| "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
},

View file

@ -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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
},
extensions: {
"org.lysand:custom_emojis": {
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
emojis: user.emojis.map((emoji) =>
new Emoji(emoji).toLysand(),
),
},
},
};