diff --git a/bun.lockb b/bun.lockb index bf8a3eb6..7635f41b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/entities/Attachment.ts b/database/entities/Attachment.ts index 6ce6cdaa..c1d766eb 100644 --- a/database/entities/Attachment.ts +++ b/database/entities/Attachment.ts @@ -1,7 +1,7 @@ +import type { EntityValidator } from "@lysand-org/federation"; import { proxyUrl } from "@response"; import type { Config } from "config-manager"; import type { InferSelectModel } from "drizzle-orm"; -import type * as Lysand from "lysand-types"; import { MediaBackendType } from "media-manager"; import { db } from "~drizzle/db"; import { Attachments } from "~drizzle/schema"; @@ -65,7 +65,7 @@ export const attachmentToAPI = ( export const attachmentToLysand = ( attachment: Attachment, -): Lysand.ContentFormat => { +): typeof EntityValidator.$ContentFormat => { return { [attachment.mimeType]: { content: attachment.url, @@ -86,7 +86,7 @@ export const attachmentToLysand = ( }; export const attachmentFromLysand = async ( - attachmentToConvert: Lysand.ContentFormat, + attachmentToConvert: typeof EntityValidator.$ContentFormat, ): Promise> => { const key = Object.keys(attachmentToConvert)[0]; const value = attachmentToConvert[key]; diff --git a/database/entities/Emoji.ts b/database/entities/Emoji.ts index 323a26ea..bf4d44f9 100644 --- a/database/entities/Emoji.ts +++ b/database/entities/Emoji.ts @@ -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 { type InferSelectModel, and, eq } from "drizzle-orm"; -import type * as Lysand from "lysand-types"; import { db } from "~drizzle/db"; import { Emojis, Instances } from "~drizzle/schema"; import type { Emoji as APIEmoji } from "~types/mastodon/emoji"; @@ -41,7 +41,7 @@ export const parseEmojis = async (text: string) => { * @returns The emoji */ export const fetchEmoji = async ( - emojiToFetch: Lysand.Emoji, + emojiToFetch: (typeof EntityValidator.$CustomEmojiExtension)["emojis"][0], host?: string, ): Promise => { const existingEmoji = await db @@ -71,7 +71,6 @@ export const fetchEmoji = async ( shortcode: emojiToFetch.name, url: Object.entries(emojiToFetch.url)[0][1].content, alt: - emojiToFetch.alt || Object.entries(emojiToFetch.url)[0][1].description || undefined, 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 { name: emoji.shortcode, url: { @@ -112,6 +113,5 @@ export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => { description: emoji.alt || undefined, }, }, - alt: emoji.alt || undefined, }; }; diff --git a/database/entities/Federation.ts b/database/entities/Federation.ts index bb127d3f..5b0ab77f 100644 --- a/database/entities/Federation.ts +++ b/database/entities/Federation.ts @@ -1,11 +1,11 @@ +import type { EntityValidator } from "@lysand-org/federation"; import { config } from "config-manager"; -import type * as Lysand from "lysand-types"; import type { User } from "~packages/database-interface/user"; export const localObjectURI = (id: string) => `/objects/${id}`; export const objectToInboxRequest = async ( - object: Lysand.Entity, + object: typeof EntityValidator.$Entity, author: User, userToSendTo: User, ): Promise => { diff --git a/database/entities/Instance.ts b/database/entities/Instance.ts index 0d1c0902..7532a22e 100644 --- a/database/entities/Instance.ts +++ b/database/entities/Instance.ts @@ -1,4 +1,4 @@ -import type * as Lysand from "lysand-types"; +import type { EntityValidator } from "@lysand-org/federation"; import { db } from "~drizzle/db"; import { Instances } from "~drizzle/schema"; @@ -26,7 +26,7 @@ export const addInstanceIfNotExists = async (url: string) => { // Fetch the instance configuration const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then( (res) => res.json(), - )) as Lysand.ServerMetadata; + )) as typeof EntityValidator.$ServerMetadata; if (metadata.type !== "ServerMetadata") { throw new Error("Invalid instance metadata (wrong type)"); diff --git a/database/entities/Like.ts b/database/entities/Like.ts index 31a300dd..d810bd55 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -1,6 +1,6 @@ +import type { EntityValidator } from "@lysand-org/federation"; import { config } from "config-manager"; import { type InferSelectModel, and, eq } from "drizzle-orm"; -import type * as Lysand from "lysand-types"; import { db } from "~drizzle/db"; import { Likes, Notifications } from "~drizzle/schema"; import type { Note } from "~packages/database-interface/note"; @@ -11,7 +11,7 @@ export type Like = InferSelectModel; /** * Represents a Like entity in the database. */ -export const likeToLysand = (like: Like): Lysand.Like => { +export const likeToLysand = (like: Like): typeof EntityValidator.$Like => { return { id: like.id, // biome-ignore lint/suspicious/noExplicitAny: to be rewritten diff --git a/database/entities/Status.ts b/database/entities/Status.ts index a7727eee..43d142a5 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -1,6 +1,7 @@ import { mentionValidator } from "@api"; import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; import { dualLogger } from "@loggers"; +import type { EntityValidator } from "@lysand-org/federation"; import { sanitizeHtml, sanitizeHtmlInline } from "@sanitization"; import { config } from "config-manager"; import { @@ -13,7 +14,6 @@ import { sql, } from "drizzle-orm"; import linkifyHtml from "linkify-html"; -import type * as Lysand from "lysand-types"; import { anyOf, charIn, @@ -253,7 +253,7 @@ export const findManyNotes = async ( export const resolveNote = async ( uri?: string, - providedNote?: Lysand.Note, + providedNote?: typeof EntityValidator.$Note, ): Promise => { if (!uri && !providedNote) { throw new Error("No URI or note provided"); @@ -265,7 +265,7 @@ export const resolveNote = async ( if (foundStatus) return foundStatus; - let note: Lysand.Note | null = providedNote ?? null; + let note = providedNote ?? null; if (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) { @@ -484,7 +484,7 @@ export const replaceTextMentions = async (text: string, mentions: User[]) => { }; export const contentToHtml = async ( - content: Lysand.ContentFormat, + content: typeof EntityValidator.$ContentFormat, mentions: User[] = [], inline = false, ): Promise => { diff --git a/database/entities/User.ts b/database/entities/User.ts index 467a1d18..e19de6fd 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -1,8 +1,7 @@ import { dualLogger } from "@loggers"; -import { addUserToMeilisearch } from "@meilisearch"; +import type { EntityValidator } from "@lysand-org/federation"; import { config } from "config-manager"; -import { type InferSelectModel, and, eq, inArray, sql } from "drizzle-orm"; -import type * as Lysand from "lysand-types"; +import { type InferSelectModel, and, eq, sql } from "drizzle-orm"; import { db } from "~drizzle/db"; import { Applications, @@ -462,7 +461,7 @@ export const getRelationshipToOtherUser = async ( export const followRequestToLysand = ( follower: User, followee: User, -): Lysand.Follow => { +): typeof EntityValidator.$Follow => { if (follower.isRemote()) { throw new Error("Follower must be a local user"); } @@ -490,7 +489,7 @@ export const followRequestToLysand = ( export const followAcceptToLysand = ( follower: User, followee: User, -): Lysand.FollowAccept => { +): typeof EntityValidator.$FollowAccept => { if (!follower.isRemote()) { throw new Error("Follower must be a remote user"); } @@ -518,7 +517,7 @@ export const followAcceptToLysand = ( export const followRejectToLysand = ( follower: User, followee: User, -): Lysand.FollowReject => { +): typeof EntityValidator.$FollowReject => { return { ...followAcceptToLysand(follower, followee), type: "FollowReject", diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 1f7345f8..62133d07 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,3 +1,4 @@ +import type { EntityValidator } from "@lysand-org/federation"; import { relations, sql } from "drizzle-orm"; import { type AnyPgColumn, @@ -12,7 +13,6 @@ import { uniqueIndex, uuid, } from "drizzle-orm/pg-core"; -import type * as Lysand from "lysand-types"; import type { Source as APISource } from "~types/mastodon/source"; export const Emojis = pgTable("Emojis", { @@ -354,8 +354,8 @@ export const Users = pgTable( isAdmin: boolean("is_admin").default(false).notNull(), fields: jsonb("fields").notNull().default("[]").$type< { - key: Lysand.ContentFormat; - value: Lysand.ContentFormat; + key: typeof EntityValidator.$ContentFormat; + value: typeof EntityValidator.$ContentFormat; }[] >(), endpoints: jsonb("endpoints").$type emojiToLysand(emoji)), diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 71ac7e8a..122a4358 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -1,5 +1,6 @@ import { idValidator } from "@api"; import { getBestContentType, urlToContentFormat } from "@content_types"; +import type { EntityValidator } from "@lysand-org/federation"; import { addUserToMeilisearch } from "@meilisearch"; import { proxyUrl } from "@response"; import { @@ -14,7 +15,6 @@ import { isNull, } from "drizzle-orm"; import { htmlToText } from "html-to-text"; -import type * as Lysand from "lysand-types"; import { emojiToAPI, emojiToLysand, @@ -206,7 +206,9 @@ export class User { }, }); - const data = (await response.json()) as Partial; + const data = (await response.json()) as Partial< + typeof EntityValidator.$User + >; if ( !( @@ -255,7 +257,11 @@ export class User { inbox: data.inbox, outbox: data.outbox, }, - fields: data.fields ?? [], + fields: + data.fields?.map((f) => ({ + key: f.name, + value: f.value, + })) ?? [], updatedAt: new Date(data.created_at).toISOString(), instanceId: instance.id, avatar: data.avatar @@ -467,7 +473,7 @@ export class User { }; } - toLysand(): Lysand.User { + toLysand(): typeof EntityValidator.$User { if (this.isRemote()) { throw new Error("Cannot convert remote user to Lysand format"); } @@ -520,7 +526,10 @@ export class User { avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined, header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined, display_name: user.displayName, - fields: user.fields, + fields: user.fields.map((f) => ({ + name: f.key, + value: f.value, + })), public_key: { actor: new URL( `/users/${user.id}`, diff --git a/server/api/objects/[uuid]/index.ts b/server/api/objects/[uuid]/index.ts index 9d7525b5..5ded42b1 100644 --- a/server/api/objects/[uuid]/index.ts +++ b/server/api/objects/[uuid]/index.ts @@ -1,9 +1,9 @@ import { applyConfig, handleZodError } from "@api"; import { zValidator } from "@hono/zod-validator"; +import type { EntityValidator } from "@lysand-org/federation"; import { errorResponse, jsonResponse } from "@response"; import { and, eq, inArray, sql } from "drizzle-orm"; import type { Hono } from "hono"; -import type * as Lysand from "lysand-types"; import { z } from "zod"; import { type Like, likeToLysand } from "~database/entities/Like"; import { db } from "~drizzle/db"; @@ -37,7 +37,7 @@ export default (app: Hono) => const { uuid } = context.req.valid("param"); let foundObject: Note | Like | null = null; - let apiObject: Lysand.Entity | null = null; + let apiObject: typeof EntityValidator.$Entity | null = null; foundObject = await Note.fromSql( and( diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index ad6800f4..373ecb50 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -1,10 +1,10 @@ import { applyConfig, handleZodError } from "@api"; import { zValidator } from "@hono/zod-validator"; import { dualLogger } from "@loggers"; +import { EntityValidator, SignatureValidator } from "@lysand-org/federation"; import { errorResponse, jsonResponse, response } from "@response"; import { eq } from "drizzle-orm"; import type { Hono } from "hono"; -import type * as Lysand from "lysand-types"; import { z } from "zod"; import { isValidationError } from "zod-validation-error"; import { resolveNote } from "~database/entities/Status"; @@ -16,7 +16,6 @@ import { db } from "~drizzle/db"; import { Notifications, Relationships } from "~drizzle/schema"; import { User } from "~packages/database-interface/user"; import { LogLevel } from "~packages/log-manager"; -import { EntityValidator, SignatureValidator } from "~packages/lysand-utils"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -82,29 +81,33 @@ export default (app: Hono) => const validator = await SignatureValidator.fromStringKey( 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) { return errorResponse("Invalid signature", 400); } } - const validator = new EntityValidator( - (await context.req.json()) as Lysand.Entity, - ); + const validator = new EntityValidator(); + const body: typeof EntityValidator.$Entity = + await context.req.json(); try { // Add sent data to database - switch (validator.getType()) { + switch (body.type) { case "Note": { - const note = await validator.validate(); + const note = await validator.Note(body); const account = await User.resolve(note.author); @@ -131,8 +134,7 @@ export default (app: Hono) => return response("Note created", 201); } case "Follow": { - const follow = - await validator.validate(); + const follow = await validator.Follow(body); const account = await User.resolve(follow.author); @@ -175,8 +177,7 @@ export default (app: Hono) => return response("Follow request sent", 200); } case "FollowAccept": { - const followAccept = - await validator.validate(); + const followAccept = await validator.FollowAccept(body); console.log(followAccept); @@ -211,8 +212,7 @@ export default (app: Hono) => return response("Follow request accepted", 200); } case "FollowReject": { - const followReject = - await validator.validate(); + const followReject = await validator.FollowReject(body); const account = await User.resolve(followReject.author); diff --git a/server/api/well-known/lysand.ts b/server/api/well-known/lysand.ts index 230f6b03..f726eeef 100644 --- a/server/api/well-known/lysand.ts +++ b/server/api/well-known/lysand.ts @@ -1,8 +1,8 @@ import { applyConfig } from "@api"; import { urlToContentFormat } from "@content_types"; +import type { EntityValidator } from "@lysand-org/federation"; import { jsonResponse } from "@response"; import type { Hono } from "hono"; -import type * as Lysand from "lysand-types"; import pkg from "~package.json"; import { config } from "~packages/config-manager"; @@ -29,5 +29,5 @@ export default (app: Hono) => banner: urlToContentFormat(config.instance.banner) ?? undefined, supported_extensions: ["org.lysand:custom_emojis"], website: "https://lysand.org", - } satisfies Lysand.ServerMetadata); + } satisfies typeof EntityValidator.$ServerMetadata); }); diff --git a/utils/content_types.ts b/utils/content_types.ts index 95a39ac6..41bbf0c2 100644 --- a/utils/content_types.ts +++ b/utils/content_types.ts @@ -1,7 +1,9 @@ -import type * as Lysand from "lysand-types"; +import type { EntityValidator } from "@lysand-org/federation"; 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" }; const bestFormatsRanked = [ @@ -21,7 +23,7 @@ export const getBestContentType = (content?: Lysand.ContentFormat) => { export const urlToContentFormat = ( url: string, -): Lysand.ContentFormat | null => { +): typeof EntityValidator.$ContentFormat | null => { if (!url) return null; if (url.startsWith("https://api.dicebear.com/")) { return {