refactor(federation): ♻️ Rewrite federation SDK

This commit is contained in:
Jesse Wierzbinski 2025-04-08 16:01:10 +02:00
parent ad1dc13a51
commit d638610361
No known key found for this signature in database
72 changed files with 2137 additions and 738 deletions

View file

@ -49,7 +49,6 @@ export default apiRoute((app) =>
),
async (context) => {
const { acct } = context.req.valid("query");
const { user } = context.get("auth");
// Check if acct is matching format username@domain.com or @username@domain.com
const { username, domain } = parseUserAddress(acct);
@ -93,9 +92,7 @@ export default apiRoute((app) =>
}
// Fetch from remote instance
const manager = await (user ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, domain);
const uri = await User.webFinger(username, domain);
if (!uri) {
throw ApiError.accountNotFound();

View file

@ -91,9 +91,7 @@ export default apiRoute((app) =>
const accounts: User[] = [];
if (resolve && domain) {
const manager = await (user ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, domain);
const uri = await User.webFinger(username, domain);
if (uri) {
const resolvedUser = await User.resolve(uri);

View file

@ -5,6 +5,7 @@ import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
import { RolePermission } from "@versia/client/schemas";
import { Emoji, Media, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import * as VersiaEntities from "@versia/sdk/entities";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
@ -186,12 +187,14 @@ export default apiRoute((app) =>
if (note && self.source) {
self.source.note = note;
self.note = await contentToHtml({
"text/markdown": {
content: note,
remote: false,
},
});
self.note = await contentToHtml(
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: note,
remote: false,
},
}),
);
}
if (source?.privacy) {
@ -275,23 +278,23 @@ export default apiRoute((app) =>
for (const field of fields_attributes) {
// Can be Markdown or plaintext, also has emojis
const parsedName = await contentToHtml(
{
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: field.name,
remote: false,
},
},
}),
undefined,
true,
);
const parsedValue = await contentToHtml(
{
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: field.value,
remote: false,
},
},
}),
undefined,
true,
);

View file

@ -14,6 +14,7 @@ import {
} from "@versia/client/schemas";
import { RolePermission } from "@versia/client/schemas";
import { Media } from "@versia/kit/db";
import * as VersiaEntities from "@versia/sdk/entities";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
@ -228,12 +229,12 @@ export default apiRoute((app) => {
const newNote = await note.updateFromData({
author: user,
content: statusText
? {
? new VersiaEntities.TextContentFormat({
[content_type]: {
content: statusText,
remote: false,
},
}
})
: undefined,
isSensitive: sensitive,
spoilerText: spoiler_text,

View file

@ -8,6 +8,7 @@ import {
} from "@versia/client/schemas";
import { RolePermission } from "@versia/client/schemas";
import { Media, Note } from "@versia/kit/db";
import * as VersiaEntities from "@versia/sdk/entities";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
@ -176,12 +177,12 @@ export default apiRoute((app) =>
const newNote = await Note.fromData({
author: user,
content: {
content: new VersiaEntities.TextContentFormat({
[content_type]: {
content: status ?? "",
remote: false,
},
},
}),
visibility,
isSensitive: sensitive ?? false,
spoilerText: spoiler_text ?? "",

View file

@ -198,15 +198,7 @@ export default apiRoute((app) =>
}
if (resolve && domain) {
const manager = await (
user ?? User
).getFederationRequester();
const uri = await User.webFinger(
manager,
username,
domain,
);
const uri = await User.webFinger(username, domain);
if (uri) {
const newUser = await User.resolve(uri);

View file

@ -1,5 +1,4 @@
import { apiRoute, handleZodError } from "@/api";
import type { Entity } from "@versia/federation/types";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { z } from "zod";
@ -33,7 +32,7 @@ export default apiRoute((app) =>
handleZodError,
),
async (context) => {
const body: Entity = await context.req.valid("json");
const body = await context.req.valid("json");
await inboxQueue.add(InboxJobType.ProcessEntity, {
data: body,

View file

@ -1,8 +1,8 @@
import { apiRoute, handleZodError } from "@/api";
import { Status as StatusSchema } from "@versia/client/schemas";
import { LikeExtension as LikeSchema } from "@versia/federation/schemas";
import { Like, User } from "@versia/kit/db";
import { Likes } from "@versia/kit/tables";
import { LikeSchema } from "@versia/sdk/schemas";
import { and, eq, sql } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";

View file

@ -1,8 +1,8 @@
import { apiRoute, handleZodError } from "@/api";
import { Status as StatusSchema } from "@versia/client/schemas";
import { Note as NoteSchema } from "@versia/federation/schemas";
import { Note } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import { NoteSchema } from "@versia/sdk/schemas";
import { and, eq, inArray } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";

View file

@ -1,9 +1,9 @@
import { apiRoute, handleZodError } from "@/api";
import { Status as StatusSchema } from "@versia/client/schemas";
import { URICollection as URICollectionSchema } from "@versia/federation/schemas";
import type { URICollection } from "@versia/federation/types";
import { Note, db } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import * as VersiaEntities from "@versia/sdk/entities";
import { URICollectionSchema } from "@versia/sdk/schemas";
import { and, eq, inArray } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
@ -88,39 +88,39 @@ export default apiRoute((app) =>
),
);
const uriCollection = {
author: note.author.getUri().href,
const uriCollection = new VersiaEntities.URICollection({
author: note.author.getUri(),
first: new URL(
`/notes/${note.id}/quotes?offset=0`,
config.http.base_url,
).href,
),
last:
replyCount > limit
? new URL(
`/notes/${note.id}/quotes?offset=${replyCount - limit}`,
config.http.base_url,
).href
)
: new URL(
`/notes/${note.id}/quotes`,
config.http.base_url,
).href,
),
next:
offset + limit < replyCount
? new URL(
`/notes/${note.id}/quotes?offset=${offset + limit}`,
config.http.base_url,
).href
)
: null,
previous:
offset - limit >= 0
? new URL(
`/notes/${note.id}/quotes?offset=${offset - limit}`,
config.http.base_url,
).href
)
: null,
total: replyCount,
items: replies.map((reply) => reply.getUri().href),
} satisfies URICollection;
items: replies.map((reply) => reply.getUri()),
});
// If base_url uses https and request uses http, rewrite request to use https
// This fixes reverse proxy errors

View file

@ -1,9 +1,9 @@
import { apiRoute, handleZodError } from "@/api";
import { Status as StatusSchema } from "@versia/client/schemas";
import { URICollection as URICollectionSchema } from "@versia/federation/schemas";
import type { URICollection } from "@versia/federation/types";
import { Note, db } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import * as VersiaEntities from "@versia/sdk/entities";
import { URICollectionSchema } from "@versia/sdk/schemas";
import { and, eq, inArray } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
@ -86,39 +86,39 @@ export default apiRoute((app) =>
),
);
const uriCollection = {
author: note.author.getUri().href,
const uriCollection = new VersiaEntities.URICollection({
author: note.author.getUri(),
first: new URL(
`/notes/${note.id}/replies?offset=0`,
config.http.base_url,
).href,
),
last:
replyCount > limit
? new URL(
`/notes/${note.id}/replies?offset=${replyCount - limit}`,
config.http.base_url,
).href
)
: new URL(
`/notes/${note.id}/replies`,
config.http.base_url,
).href,
),
next:
offset + limit < replyCount
? new URL(
`/notes/${note.id}/replies?offset=${offset + limit}`,
config.http.base_url,
).href
)
: null,
previous:
offset - limit >= 0
? new URL(
`/notes/${note.id}/replies?offset=${offset - limit}`,
config.http.base_url,
).href
)
: null,
total: replyCount,
items: replies.map((reply) => reply.getUri().href),
} satisfies URICollection;
items: replies.map((reply) => reply.getUri()),
});
// If base_url uses https and request uses http, rewrite request to use https
// This fixes reverse proxy errors

View file

@ -1,10 +1,10 @@
import { apiRoute, handleZodError } from "@/api";
import type { Entity } from "@versia/federation/types";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error";
import { InboxJobType, inboxQueue } from "~/classes/queues/inbox";
import type { JSONObject } from "~/packages/federation/types";
export default apiRoute((app) =>
app.post(
@ -89,7 +89,7 @@ export default apiRoute((app) =>
),
validator("json", z.any(), handleZodError),
async (context) => {
const body: Entity = await context.req.valid("json");
const body: JSONObject = await context.req.valid("json");
await inboxQueue.add(InboxJobType.ProcessEntity, {
data: body,

View file

@ -1,6 +1,6 @@
import { apiRoute, handleZodError } from "@/api";
import { User as UserSchema } from "@versia/federation/schemas";
import { User } from "@versia/kit/db";
import { UserSchema } from "@versia/sdk/schemas";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
@ -43,6 +43,7 @@ export default apiRoute((app) =>
}),
handleZodError,
),
// @ts-expect-error
async (context) => {
const { uuid } = context.req.valid("param");

View file

@ -1,10 +1,8 @@
import { apiRoute, handleZodError } from "@/api";
import {
Collection as CollectionSchema,
Note as NoteSchema,
} from "@versia/federation/schemas";
import { Note, User, db } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import * as VersiaEntities from "@versia/sdk/entities";
import { CollectionSchema, NoteSchema } from "@versia/sdk/schemas";
import { and, eq, inArray } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
@ -96,35 +94,35 @@ export default apiRoute((app) =>
),
);
const json = {
const json = new VersiaEntities.Collection({
first: new URL(
`/users/${uuid}/outbox?page=1`,
config.http.base_url,
).toString(),
),
last: new URL(
`/users/${uuid}/outbox?page=${Math.ceil(
totalNotes / NOTES_PER_PAGE,
)}`,
config.http.base_url,
).toString(),
),
total: totalNotes,
author: author.getUri().toString(),
author: author.getUri(),
next:
notes.length === NOTES_PER_PAGE
? new URL(
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
config.http.base_url,
).toString()
)
: null,
previous:
pageNumber > 1
? new URL(
`/users/${uuid}/outbox?page=${pageNumber - 1}`,
config.http.base_url,
).toString()
)
: null,
items: notes.map((note) => note.toVersia()),
};
});
const { headers } = await author.sign(
json,

View file

@ -1,8 +1,8 @@
import { apiRoute } from "@/api";
import { urlToContentFormat } from "@/content_types";
import { InstanceMetadata as InstanceMetadataSchema } from "@versia/federation/schemas";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { InstanceMetadataSchema } from "@versia/sdk/schemas";
import { asc } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";

View file

@ -6,10 +6,9 @@ import {
webfingerMention,
} from "@/api";
import { getLogger } from "@logtape/logtape";
import type { ResponseError } from "@versia/federation";
import { WebFinger } from "@versia/federation/schemas";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { WebFingerSchema } from "@versia/sdk/schemas";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
@ -28,7 +27,7 @@ export default apiRoute((app) =>
description: "User information",
content: {
"application/json": {
schema: resolver(WebFinger),
schema: resolver(WebFingerSchema),
},
},
},
@ -81,23 +80,22 @@ export default apiRoute((app) =>
throw ApiError.accountNotFound();
}
let activityPubUrl = "";
let activityPubUrl: URL | null = null;
if (config.federation.bridge) {
const manager = await User.getFederationRequester();
try {
activityPubUrl = await manager.webFinger(
user.data.username,
config.http.base_url.host,
"application/activity+json",
config.federation.bridge.url.origin,
);
activityPubUrl =
await User.federationRequester.resolveWebFinger(
user.data.username,
config.http.base_url.host,
"application/activity+json",
config.federation.bridge.url.origin,
);
} catch (e) {
const error = e as ResponseError;
const error = e as ApiError;
getLogger(["federation", "bridge"])
.error`Error from bridge: ${await error.response.data}`;
.error`Error from bridge: ${error.message}`;
}
}
@ -112,7 +110,7 @@ export default apiRoute((app) =>
? {
rel: "self",
type: "application/activity+json",
href: activityPubUrl,
href: activityPubUrl.href,
}
: undefined,
{