feat(federation): Replace old types and federation validators with @lysand-org/federation

This commit is contained in:
Jesse Wierzbinski 2024-05-14 14:35:13 -10:00
parent 25d087a54b
commit 5fd6a4e43d
No known key found for this signature in database
16 changed files with 80 additions and 65 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -1,7 +1,7 @@
import type { EntityValidator } from "@lysand-org/federation";
import { proxyUrl } from "@response"; import { proxyUrl } from "@response";
import type { Config } from "config-manager"; import type { Config } from "config-manager";
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { Attachments } from "~drizzle/schema"; import { Attachments } from "~drizzle/schema";
@ -65,7 +65,7 @@ export const attachmentToAPI = (
export const attachmentToLysand = ( export const attachmentToLysand = (
attachment: Attachment, attachment: Attachment,
): Lysand.ContentFormat => { ): typeof EntityValidator.$ContentFormat => {
return { return {
[attachment.mimeType]: { [attachment.mimeType]: {
content: attachment.url, content: attachment.url,
@ -86,7 +86,7 @@ export const attachmentToLysand = (
}; };
export const attachmentFromLysand = async ( export const attachmentFromLysand = async (
attachmentToConvert: Lysand.ContentFormat, attachmentToConvert: typeof EntityValidator.$ContentFormat,
): Promise<InferSelectModel<typeof Attachments>> => { ): Promise<InferSelectModel<typeof Attachments>> => {
const key = Object.keys(attachmentToConvert)[0]; const key = Object.keys(attachmentToConvert)[0];
const value = attachmentToConvert[key]; const value = attachmentToConvert[key];

View file

@ -1,7 +1,7 @@
import { emojiValidator, emojiValidatorWithColons } from "@api"; import { emojiValidatorWithColons } from "@api";
import type { EntityValidator } from "@lysand-org/federation";
import { proxyUrl } from "@response"; import { proxyUrl } from "@response";
import { type InferSelectModel, and, eq } from "drizzle-orm"; import { type InferSelectModel, and, eq } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { Emojis, Instances } from "~drizzle/schema"; import { Emojis, Instances } from "~drizzle/schema";
import type { Emoji as APIEmoji } from "~types/mastodon/emoji"; import type { Emoji as APIEmoji } from "~types/mastodon/emoji";
@ -41,7 +41,7 @@ export const parseEmojis = async (text: string) => {
* @returns The emoji * @returns The emoji
*/ */
export const fetchEmoji = async ( export const fetchEmoji = async (
emojiToFetch: Lysand.Emoji, emojiToFetch: (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0],
host?: string, host?: string,
): Promise<EmojiWithInstance> => { ): Promise<EmojiWithInstance> => {
const existingEmoji = await db const existingEmoji = await db
@ -71,7 +71,6 @@ export const fetchEmoji = async (
shortcode: emojiToFetch.name, shortcode: emojiToFetch.name,
url: Object.entries(emojiToFetch.url)[0][1].content, url: Object.entries(emojiToFetch.url)[0][1].content,
alt: alt:
emojiToFetch.alt ||
Object.entries(emojiToFetch.url)[0][1].description || Object.entries(emojiToFetch.url)[0][1].description ||
undefined, undefined,
contentType: Object.keys(emojiToFetch.url)[0], contentType: Object.keys(emojiToFetch.url)[0],
@ -103,7 +102,9 @@ export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => {
}; };
}; };
export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => { export const emojiToLysand = (
emoji: EmojiWithInstance,
): (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0] => {
return { return {
name: emoji.shortcode, name: emoji.shortcode,
url: { url: {
@ -112,6 +113,5 @@ export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => {
description: emoji.alt || undefined, description: emoji.alt || undefined,
}, },
}, },
alt: emoji.alt || undefined,
}; };
}; };

View file

@ -1,11 +1,11 @@
import type { EntityValidator } from "@lysand-org/federation";
import { config } from "config-manager"; import { config } from "config-manager";
import type * as Lysand from "lysand-types";
import type { User } from "~packages/database-interface/user"; import type { User } from "~packages/database-interface/user";
export const localObjectURI = (id: string) => `/objects/${id}`; export const localObjectURI = (id: string) => `/objects/${id}`;
export const objectToInboxRequest = async ( export const objectToInboxRequest = async (
object: Lysand.Entity, object: typeof EntityValidator.$Entity,
author: User, author: User,
userToSendTo: User, userToSendTo: User,
): Promise<Request> => { ): Promise<Request> => {

View file

@ -1,4 +1,4 @@
import type * as Lysand from "lysand-types"; import type { EntityValidator } from "@lysand-org/federation";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { Instances } from "~drizzle/schema"; import { Instances } from "~drizzle/schema";
@ -26,7 +26,7 @@ export const addInstanceIfNotExists = async (url: string) => {
// Fetch the instance configuration // Fetch the instance configuration
const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then( const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then(
(res) => res.json(), (res) => res.json(),
)) as Lysand.ServerMetadata; )) as typeof EntityValidator.$ServerMetadata;
if (metadata.type !== "ServerMetadata") { if (metadata.type !== "ServerMetadata") {
throw new Error("Invalid instance metadata (wrong type)"); throw new Error("Invalid instance metadata (wrong type)");

View file

@ -1,6 +1,6 @@
import type { EntityValidator } from "@lysand-org/federation";
import { config } from "config-manager"; import { config } from "config-manager";
import { type InferSelectModel, and, eq } from "drizzle-orm"; import { type InferSelectModel, and, eq } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { Likes, Notifications } from "~drizzle/schema"; import { Likes, Notifications } from "~drizzle/schema";
import type { Note } from "~packages/database-interface/note"; import type { Note } from "~packages/database-interface/note";
@ -11,7 +11,7 @@ export type Like = InferSelectModel<typeof Likes>;
/** /**
* Represents a Like entity in the database. * Represents a Like entity in the database.
*/ */
export const likeToLysand = (like: Like): Lysand.Like => { export const likeToLysand = (like: Like): typeof EntityValidator.$Like => {
return { return {
id: like.id, id: like.id,
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten // biome-ignore lint/suspicious/noExplicitAny: to be rewritten

View file

@ -1,6 +1,7 @@
import { mentionValidator } from "@api"; import { mentionValidator } from "@api";
import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
import { dualLogger } from "@loggers"; import { dualLogger } from "@loggers";
import type { EntityValidator } from "@lysand-org/federation";
import { sanitizeHtml, sanitizeHtmlInline } from "@sanitization"; import { sanitizeHtml, sanitizeHtmlInline } from "@sanitization";
import { config } from "config-manager"; import { config } from "config-manager";
import { import {
@ -13,7 +14,6 @@ import {
sql, sql,
} from "drizzle-orm"; } from "drizzle-orm";
import linkifyHtml from "linkify-html"; import linkifyHtml from "linkify-html";
import type * as Lysand from "lysand-types";
import { import {
anyOf, anyOf,
charIn, charIn,
@ -253,7 +253,7 @@ export const findManyNotes = async (
export const resolveNote = async ( export const resolveNote = async (
uri?: string, uri?: string,
providedNote?: Lysand.Note, providedNote?: typeof EntityValidator.$Note,
): Promise<Note> => { ): Promise<Note> => {
if (!uri && !providedNote) { if (!uri && !providedNote) {
throw new Error("No URI or note provided"); throw new Error("No URI or note provided");
@ -265,7 +265,7 @@ export const resolveNote = async (
if (foundStatus) return foundStatus; if (foundStatus) return foundStatus;
let note: Lysand.Note | null = providedNote ?? null; let note = providedNote ?? null;
if (uri) { if (uri) {
if (!URL.canParse(uri)) { if (!URL.canParse(uri)) {
@ -279,7 +279,7 @@ export const resolveNote = async (
}, },
}); });
note = (await response.json()) as Lysand.Note; note = (await response.json()) as typeof EntityValidator.$Note;
} }
if (!note) { if (!note) {
@ -484,7 +484,7 @@ export const replaceTextMentions = async (text: string, mentions: User[]) => {
}; };
export const contentToHtml = async ( export const contentToHtml = async (
content: Lysand.ContentFormat, content: typeof EntityValidator.$ContentFormat,
mentions: User[] = [], mentions: User[] = [],
inline = false, inline = false,
): Promise<string> => { ): Promise<string> => {

View file

@ -1,8 +1,7 @@
import { dualLogger } from "@loggers"; import { dualLogger } from "@loggers";
import { addUserToMeilisearch } from "@meilisearch"; import type { EntityValidator } from "@lysand-org/federation";
import { config } from "config-manager"; import { config } from "config-manager";
import { type InferSelectModel, and, eq, inArray, sql } from "drizzle-orm"; import { type InferSelectModel, and, eq, sql } from "drizzle-orm";
import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { import {
Applications, Applications,
@ -462,7 +461,7 @@ export const getRelationshipToOtherUser = async (
export const followRequestToLysand = ( export const followRequestToLysand = (
follower: User, follower: User,
followee: User, followee: User,
): Lysand.Follow => { ): typeof EntityValidator.$Follow => {
if (follower.isRemote()) { if (follower.isRemote()) {
throw new Error("Follower must be a local user"); throw new Error("Follower must be a local user");
} }
@ -490,7 +489,7 @@ export const followRequestToLysand = (
export const followAcceptToLysand = ( export const followAcceptToLysand = (
follower: User, follower: User,
followee: User, followee: User,
): Lysand.FollowAccept => { ): typeof EntityValidator.$FollowAccept => {
if (!follower.isRemote()) { if (!follower.isRemote()) {
throw new Error("Follower must be a remote user"); throw new Error("Follower must be a remote user");
} }
@ -518,7 +517,7 @@ export const followAcceptToLysand = (
export const followRejectToLysand = ( export const followRejectToLysand = (
follower: User, follower: User,
followee: User, followee: User,
): Lysand.FollowReject => { ): typeof EntityValidator.$FollowReject => {
return { return {
...followAcceptToLysand(follower, followee), ...followAcceptToLysand(follower, followee),
type: "FollowReject", type: "FollowReject",

View file

@ -1,3 +1,4 @@
import type { EntityValidator } from "@lysand-org/federation";
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
type AnyPgColumn, type AnyPgColumn,
@ -12,7 +13,6 @@ import {
uniqueIndex, uniqueIndex,
uuid, uuid,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import type * as Lysand from "lysand-types";
import type { Source as APISource } from "~types/mastodon/source"; import type { Source as APISource } from "~types/mastodon/source";
export const Emojis = pgTable("Emojis", { export const Emojis = pgTable("Emojis", {
@ -354,8 +354,8 @@ export const Users = pgTable(
isAdmin: boolean("is_admin").default(false).notNull(), isAdmin: boolean("is_admin").default(false).notNull(),
fields: jsonb("fields").notNull().default("[]").$type< fields: jsonb("fields").notNull().default("[]").$type<
{ {
key: Lysand.ContentFormat; key: typeof EntityValidator.$ContentFormat;
value: Lysand.ContentFormat; value: typeof EntityValidator.$ContentFormat;
}[] }[]
>(), >(),
endpoints: jsonb("endpoints").$type<Partial<{ endpoints: jsonb("endpoints").$type<Partial<{

View file

@ -98,6 +98,7 @@
"@inquirer/confirm": "^3.1.6", "@inquirer/confirm": "^3.1.6",
"@inquirer/input": "^2.1.6", "@inquirer/input": "^2.1.6",
"@json2csv/plainjs": "^7.0.6", "@json2csv/plainjs": "^7.0.6",
"@lysand-org/federation": "^1.1.3",
"@oclif/core": "^3.26.6", "@oclif/core": "^3.26.6",
"@tufjs/canonical-json": "^2.0.0", "@tufjs/canonical-json": "^2.0.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",

View file

@ -1,3 +1,4 @@
import type { EntityValidator } from "@lysand-org/federation";
import { proxyUrl } from "@response"; import { proxyUrl } from "@response";
import { sanitizedHtmlStrip } from "@sanitization"; import { sanitizedHtmlStrip } from "@sanitization";
import { import {
@ -12,7 +13,6 @@ import {
sql, sql,
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import type * as Lysand from "lysand-types";
import { createRegExp, exactly, global } from "magic-regexp"; import { createRegExp, exactly, global } from "magic-regexp";
import { import {
type Application, type Application,
@ -210,7 +210,7 @@ export class Note {
static async fromData( static async fromData(
author: User, author: User,
content: Lysand.ContentFormat, content: typeof EntityValidator.$ContentFormat,
visibility: APIStatus["visibility"], visibility: APIStatus["visibility"],
is_sensitive: boolean, is_sensitive: boolean,
spoiler_text: string, spoiler_text: string,
@ -303,7 +303,7 @@ export class Note {
} }
async updateFromData( async updateFromData(
content?: Lysand.ContentFormat, content?: typeof EntityValidator.$ContentFormat,
visibility?: APIStatus["visibility"], visibility?: APIStatus["visibility"],
is_sensitive?: boolean, is_sensitive?: boolean,
spoiler_text?: string, spoiler_text?: string,
@ -539,7 +539,7 @@ export class Note {
return `/@${this.getAuthor().getUser().username}/${this.id}`; return `/@${this.getAuthor().getUser().username}/${this.id}`;
} }
toLysand(): Lysand.Note { toLysand(): typeof EntityValidator.$Note {
const status = this.getStatus(); const status = this.getStatus();
return { return {
type: "Note", type: "Note",
@ -563,7 +563,11 @@ export class Note {
quotes: Note.getURI(status.quotingId) ?? undefined, quotes: Note.getURI(status.quotingId) ?? undefined,
replies_to: Note.getURI(status.replyId) ?? undefined, replies_to: Note.getURI(status.replyId) ?? undefined,
subject: status.spoilerText, subject: status.spoilerText,
visibility: status.visibility as Lysand.Visibility, visibility: status.visibility as
| "public"
| "unlisted"
| "private"
| "direct",
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)), emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),

View file

@ -1,5 +1,6 @@
import { idValidator } from "@api"; import { idValidator } from "@api";
import { getBestContentType, urlToContentFormat } from "@content_types"; import { getBestContentType, urlToContentFormat } from "@content_types";
import type { EntityValidator } from "@lysand-org/federation";
import { addUserToMeilisearch } from "@meilisearch"; import { addUserToMeilisearch } from "@meilisearch";
import { proxyUrl } from "@response"; import { proxyUrl } from "@response";
import { import {
@ -14,7 +15,6 @@ import {
isNull, isNull,
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import type * as Lysand from "lysand-types";
import { import {
emojiToAPI, emojiToAPI,
emojiToLysand, emojiToLysand,
@ -206,7 +206,9 @@ export class User {
}, },
}); });
const data = (await response.json()) as Partial<Lysand.User>; const data = (await response.json()) as Partial<
typeof EntityValidator.$User
>;
if ( if (
!( !(
@ -255,7 +257,11 @@ export class User {
inbox: data.inbox, inbox: data.inbox,
outbox: data.outbox, outbox: data.outbox,
}, },
fields: data.fields ?? [], fields:
data.fields?.map((f) => ({
key: f.name,
value: f.value,
})) ?? [],
updatedAt: new Date(data.created_at).toISOString(), updatedAt: new Date(data.created_at).toISOString(),
instanceId: instance.id, instanceId: instance.id,
avatar: data.avatar avatar: data.avatar
@ -467,7 +473,7 @@ export class User {
}; };
} }
toLysand(): Lysand.User { toLysand(): typeof EntityValidator.$User {
if (this.isRemote()) { if (this.isRemote()) {
throw new Error("Cannot convert remote user to Lysand format"); throw new Error("Cannot convert remote user to Lysand format");
} }
@ -520,7 +526,10 @@ export class User {
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined, avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined, header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
display_name: user.displayName, display_name: user.displayName,
fields: user.fields, fields: user.fields.map((f) => ({
name: f.key,
value: f.value,
})),
public_key: { public_key: {
actor: new URL( actor: new URL(
`/users/${user.id}`, `/users/${user.id}`,

View file

@ -1,9 +1,9 @@
import { applyConfig, handleZodError } from "@api"; import { applyConfig, handleZodError } from "@api";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { EntityValidator } from "@lysand-org/federation";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type * as Lysand from "lysand-types";
import { z } from "zod"; import { z } from "zod";
import { type Like, likeToLysand } from "~database/entities/Like"; import { type Like, likeToLysand } from "~database/entities/Like";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
@ -37,7 +37,7 @@ export default (app: Hono) =>
const { uuid } = context.req.valid("param"); const { uuid } = context.req.valid("param");
let foundObject: Note | Like | null = null; let foundObject: Note | Like | null = null;
let apiObject: Lysand.Entity | null = null; let apiObject: typeof EntityValidator.$Entity | null = null;
foundObject = await Note.fromSql( foundObject = await Note.fromSql(
and( and(

View file

@ -1,10 +1,10 @@
import { applyConfig, handleZodError } from "@api"; import { applyConfig, handleZodError } from "@api";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { dualLogger } from "@loggers"; import { dualLogger } from "@loggers";
import { EntityValidator, SignatureValidator } from "@lysand-org/federation";
import { errorResponse, jsonResponse, response } from "@response"; import { errorResponse, jsonResponse, response } from "@response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type * as Lysand from "lysand-types";
import { z } from "zod"; import { z } from "zod";
import { isValidationError } from "zod-validation-error"; import { isValidationError } from "zod-validation-error";
import { resolveNote } from "~database/entities/Status"; import { resolveNote } from "~database/entities/Status";
@ -16,7 +16,6 @@ import { db } from "~drizzle/db";
import { Notifications, Relationships } from "~drizzle/schema"; import { Notifications, Relationships } from "~drizzle/schema";
import { User } from "~packages/database-interface/user"; import { User } from "~packages/database-interface/user";
import { LogLevel } from "~packages/log-manager"; import { LogLevel } from "~packages/log-manager";
import { EntityValidator, SignatureValidator } from "~packages/lysand-utils";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -82,29 +81,33 @@ export default (app: Hono) =>
const validator = await SignatureValidator.fromStringKey( const validator = await SignatureValidator.fromStringKey(
sender.getUser().publicKey, sender.getUser().publicKey,
signature,
date,
context.req.method,
new URL(context.req.url),
await context.req.text(),
); );
const isValid = await validator.validate(); const isValid = await validator
.validate(context.req.raw)
.catch((e) => {
dualLogger.logError(
LogLevel.ERROR,
"Inbox.Signature",
e as Error,
);
return false;
});
if (!isValid) { if (!isValid) {
return errorResponse("Invalid signature", 400); return errorResponse("Invalid signature", 400);
} }
} }
const validator = new EntityValidator( const validator = new EntityValidator();
(await context.req.json()) as Lysand.Entity, const body: typeof EntityValidator.$Entity =
); await context.req.json();
try { try {
// Add sent data to database // Add sent data to database
switch (validator.getType()) { switch (body.type) {
case "Note": { case "Note": {
const note = await validator.validate<Lysand.Note>(); const note = await validator.Note(body);
const account = await User.resolve(note.author); const account = await User.resolve(note.author);
@ -131,8 +134,7 @@ export default (app: Hono) =>
return response("Note created", 201); return response("Note created", 201);
} }
case "Follow": { case "Follow": {
const follow = const follow = await validator.Follow(body);
await validator.validate<Lysand.Follow>();
const account = await User.resolve(follow.author); const account = await User.resolve(follow.author);
@ -175,8 +177,7 @@ export default (app: Hono) =>
return response("Follow request sent", 200); return response("Follow request sent", 200);
} }
case "FollowAccept": { case "FollowAccept": {
const followAccept = const followAccept = await validator.FollowAccept(body);
await validator.validate<Lysand.FollowAccept>();
console.log(followAccept); console.log(followAccept);
@ -211,8 +212,7 @@ export default (app: Hono) =>
return response("Follow request accepted", 200); return response("Follow request accepted", 200);
} }
case "FollowReject": { case "FollowReject": {
const followReject = const followReject = await validator.FollowReject(body);
await validator.validate<Lysand.FollowReject>();
const account = await User.resolve(followReject.author); const account = await User.resolve(followReject.author);

View file

@ -1,8 +1,8 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { urlToContentFormat } from "@content_types"; import { urlToContentFormat } from "@content_types";
import type { EntityValidator } from "@lysand-org/federation";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type * as Lysand from "lysand-types";
import pkg from "~package.json"; import pkg from "~package.json";
import { config } from "~packages/config-manager"; import { config } from "~packages/config-manager";
@ -29,5 +29,5 @@ export default (app: Hono) =>
banner: urlToContentFormat(config.instance.banner) ?? undefined, banner: urlToContentFormat(config.instance.banner) ?? undefined,
supported_extensions: ["org.lysand:custom_emojis"], supported_extensions: ["org.lysand:custom_emojis"],
website: "https://lysand.org", website: "https://lysand.org",
} satisfies Lysand.ServerMetadata); } satisfies typeof EntityValidator.$ServerMetadata);
}); });

View file

@ -1,7 +1,9 @@
import type * as Lysand from "lysand-types"; import type { EntityValidator } from "@lysand-org/federation";
import { lookup } from "mime-types"; import { lookup } from "mime-types";
export const getBestContentType = (content?: Lysand.ContentFormat) => { export const getBestContentType = (
content?: typeof EntityValidator.$ContentFormat,
) => {
if (!content) return { content: "", format: "text/plain" }; if (!content) return { content: "", format: "text/plain" };
const bestFormatsRanked = [ const bestFormatsRanked = [
@ -21,7 +23,7 @@ export const getBestContentType = (content?: Lysand.ContentFormat) => {
export const urlToContentFormat = ( export const urlToContentFormat = (
url: string, url: string,
): Lysand.ContentFormat | null => { ): typeof EntityValidator.$ContentFormat | null => {
if (!url) return null; if (!url) return null;
if (url.startsWith("https://api.dicebear.com/")) { if (url.startsWith("https://api.dicebear.com/")) {
return { return {