mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(federation): ➕ Replace old types and federation validators with @lysand-org/federation
This commit is contained in:
parent
25d087a54b
commit
5fd6a4e43d
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue