mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(api): 🎨 Refactor emojis into their own class
This commit is contained in:
parent
c61f519a34
commit
d8cb1d475b
11 changed files with 327 additions and 278 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
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,
|
||||
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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue