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 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<InferSelectModel<typeof Attachments>> => {
const key = Object.keys(attachmentToConvert)[0];
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 { 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<EmojiWithInstance> => {
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,
};
};

View file

@ -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<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 { 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)");

View file

@ -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<typeof Likes>;
/**
* 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

View file

@ -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<Note> => {
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<string> => {

View file

@ -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",

View file

@ -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<Partial<{

View file

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

View file

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

View file

@ -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<Lysand.User>;
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}`,

View file

@ -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(

View file

@ -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<Lysand.Note>();
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<Lysand.Follow>();
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<Lysand.FollowAccept>();
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<Lysand.FollowReject>();
const followReject = await validator.FollowReject(body);
const account = await User.resolve(followReject.author);

View file

@ -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);
});

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";
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 {