From 54b2dfb78dfaca9f728bb3efe20d080c7a45ad12 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 8 Apr 2025 17:27:08 +0200 Subject: [PATCH] refactor(federation): :fire: Remove confusing User federation methods --- api/api/v1/accounts/[id]/refetch.ts | 3 +- .../follow_requests/[account_id]/authorize.ts | 2 +- .../v1/follow_requests/[account_id]/reject.ts | 2 +- api/api/v1/statuses/index.test.ts | 4 +- api/notes/[uuid]/quotes.ts | 2 +- api/notes/[uuid]/replies.ts | 2 +- api/users/[uuid]/outbox/index.ts | 2 +- classes/database/note.ts | 4 +- classes/database/user.ts | 138 ++++++++---------- classes/functions/status.ts | 2 +- classes/inbox/processor.ts | 2 +- cli/user/refetch.ts | 3 +- 12 files changed, 72 insertions(+), 94 deletions(-) diff --git a/api/api/v1/accounts/[id]/refetch.ts b/api/api/v1/accounts/[id]/refetch.ts index 954e4577..c089c8a4 100644 --- a/api/api/v1/accounts/[id]/refetch.ts +++ b/api/api/v1/accounts/[id]/refetch.ts @@ -3,6 +3,7 @@ import { Account as AccountSchema } from "@versia/client/schemas"; import { RolePermission } from "@versia/client/schemas"; import { describeRoute } from "hono-openapi"; import { resolver } from "hono-openapi/zod"; +import { User } from "~/classes/database/user"; import { ApiError } from "~/classes/errors/api-error"; export default apiRoute((app) => @@ -48,7 +49,7 @@ export default apiRoute((app) => throw new ApiError(400, "Cannot refetch a local user"); } - const newUser = await otherUser.updateFromRemote(); + const newUser = await User.fromVersia(otherUser.uri); return context.json(newUser.toApi(false), 200); }, diff --git a/api/api/v1/follow_requests/[account_id]/authorize.ts b/api/api/v1/follow_requests/[account_id]/authorize.ts index 901e261c..70acad50 100644 --- a/api/api/v1/follow_requests/[account_id]/authorize.ts +++ b/api/api/v1/follow_requests/[account_id]/authorize.ts @@ -73,7 +73,7 @@ export default apiRoute((app) => // Check if accepting remote follow if (account.isRemote()) { // Federate follow accept - await user.sendFollowAccept(account); + await user.acceptFollowRequest(account); } return context.json(foundRelationship.toApi(), 200); diff --git a/api/api/v1/follow_requests/[account_id]/reject.ts b/api/api/v1/follow_requests/[account_id]/reject.ts index 1ee1b858..a1d8354e 100644 --- a/api/api/v1/follow_requests/[account_id]/reject.ts +++ b/api/api/v1/follow_requests/[account_id]/reject.ts @@ -74,7 +74,7 @@ export default apiRoute((app) => // Check if rejecting remote follow if (account.isRemote()) { // Federate follow reject - await user.sendFollowReject(account); + await user.rejectFollowRequest(account); } return context.json(foundRelationship.toApi(), 200); diff --git a/api/api/v1/statuses/index.test.ts b/api/api/v1/statuses/index.test.ts index 6f6cc6e2..d2571b9e 100644 --- a/api/api/v1/statuses/index.test.ts +++ b/api/api/v1/statuses/index.test.ts @@ -218,7 +218,7 @@ describe("/api/v1/statuses", () => { expect(ok).toBe(true); expect(data).toMatchObject({ - content: `

Hello, @${users[1].data.username}!

`, + content: `

Hello, @${users[1].data.username}!

`, }); expect((data as z.infer).mentions).toBeArrayOfSize( 1, @@ -241,7 +241,7 @@ describe("/api/v1/statuses", () => { expect(ok).toBe(true); expect(data).toMatchObject({ - content: `

Hello, @${users[1].data.username}!

`, + content: `

Hello, @${users[1].data.username}!

`, }); expect((data as z.infer).mentions).toBeArrayOfSize( 1, diff --git a/api/notes/[uuid]/quotes.ts b/api/notes/[uuid]/quotes.ts index f62e0bfb..1ebfaa36 100644 --- a/api/notes/[uuid]/quotes.ts +++ b/api/notes/[uuid]/quotes.ts @@ -89,7 +89,7 @@ export default apiRoute((app) => ); const uriCollection = new VersiaEntities.URICollection({ - author: note.author.getUri(), + author: note.author.uri, first: new URL( `/notes/${note.id}/quotes?offset=0`, config.http.base_url, diff --git a/api/notes/[uuid]/replies.ts b/api/notes/[uuid]/replies.ts index 4499d61b..e95b49e7 100644 --- a/api/notes/[uuid]/replies.ts +++ b/api/notes/[uuid]/replies.ts @@ -87,7 +87,7 @@ export default apiRoute((app) => ); const uriCollection = new VersiaEntities.URICollection({ - author: note.author.getUri(), + author: note.author.uri, first: new URL( `/notes/${note.id}/replies?offset=0`, config.http.base_url, diff --git a/api/users/[uuid]/outbox/index.ts b/api/users/[uuid]/outbox/index.ts index 9f2dee9b..50ae6925 100644 --- a/api/users/[uuid]/outbox/index.ts +++ b/api/users/[uuid]/outbox/index.ts @@ -106,7 +106,7 @@ export default apiRoute((app) => config.http.base_url, ), total: totalNotes, - author: author.getUri(), + author: author.uri, next: notes.length === NOTES_PER_PAGE ? new URL( diff --git a/classes/database/note.ts b/classes/database/note.ts index 6b2b418a..f83cbacf 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -861,7 +861,7 @@ export class Note extends BaseInterface { return new VersiaEntities.Delete({ type: "Delete", id, - author: this.author.getUri(), + author: this.author.uri, deleted_type: "Note", deleted: this.getUri(), created_at: new Date().toISOString(), @@ -878,7 +878,7 @@ export class Note extends BaseInterface { type: "Note", created_at: new Date(status.createdAt).toISOString(), id: status.id, - author: this.author.getUri(), + author: this.author.uri, uri: this.getUri(), content: { "text/html": { diff --git a/classes/database/user.ts b/classes/database/user.ts index aacb65d0..8b75b612 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -156,7 +156,7 @@ export class User extends BaseInterface { return !this.isLocal(); } - public getUri(): URL { + public get uri(): URL { return this.data.uri ? new URL(this.data.uri) : new URL(`/users/${this.data.id}`, config.http.base_url); @@ -208,8 +208,8 @@ export class User extends BaseInterface { entity: { type: "Follow", id: crypto.randomUUID(), - author: this.getUri().toString(), - followee: otherUser.getUri().toString(), + author: this.uri.href, + followee: otherUser.uri.href, created_at: new Date().toISOString(), }, recipientId: otherUser.id, @@ -247,13 +247,13 @@ export class User extends BaseInterface { return new VersiaEntities.Unfollow({ type: "Unfollow", id, - author: this.getUri(), + author: this.uri, created_at: new Date().toISOString(), - followee: followee.getUri(), + followee: followee.uri, }); } - public async sendFollowAccept(follower: User): Promise { + public async acceptFollowRequest(follower: User): Promise { if (!follower.isRemote()) { throw new Error("Follower must be a remote user"); } @@ -265,9 +265,9 @@ export class User extends BaseInterface { const entity = new VersiaEntities.FollowAccept({ type: "FollowAccept", id: crypto.randomUUID(), - author: this.getUri(), + author: this.uri, created_at: new Date().toISOString(), - follower: follower.getUri(), + follower: follower.uri, }); await deliveryQueue.add(DeliveryJobType.FederateEntity, { @@ -277,7 +277,7 @@ export class User extends BaseInterface { }); } - public async sendFollowReject(follower: User): Promise { + public async rejectFollowRequest(follower: User): Promise { if (!follower.isRemote()) { throw new Error("Follower must be a remote user"); } @@ -289,9 +289,9 @@ export class User extends BaseInterface { const entity = new VersiaEntities.FollowReject({ type: "FollowReject", id: crypto.randomUUID(), - author: this.getUri(), + author: this.uri, created_at: new Date().toISOString(), - follower: follower.getUri(), + follower: follower.uri, }); await deliveryQueue.add(DeliveryJobType.FederateEntity, { @@ -326,7 +326,7 @@ export class User extends BaseInterface { const { headers } = await sign( privateKey, - this.getUri(), + this.uri, new Request(signatureUrl, { method: signatureMethod, body: JSON.stringify(entity), @@ -600,66 +600,6 @@ export class User extends BaseInterface { ); } - public async updateFromRemote(): Promise { - if (!this.isRemote()) { - throw new Error( - "Cannot refetch a local user (they are not remote)", - ); - } - - const updated = await User.fetchFromRemote(this.getUri()); - - if (!updated) { - throw new Error("Failed to update user from remote"); - } - - this.data = updated.data; - - return this; - } - - public static async fetchFromRemote(uri: URL): Promise { - const instance = await Instance.resolve(uri); - - if (!instance) { - return null; - } - - if (instance.data.protocol === "versia") { - return await User.saveFromVersia(uri); - } - - if (instance.data.protocol === "activitypub") { - if (!config.federation.bridge) { - throw new Error("ActivityPub bridge is not enabled"); - } - - const bridgeUri = new URL( - `/apbridge/versia/query?${new URLSearchParams({ - user_url: uri.toString(), - })}`, - config.federation.bridge.url, - ); - - return await User.saveFromVersia(bridgeUri); - } - - throw new Error(`Unsupported protocol: ${instance.data.protocol}`); - } - - private static async saveFromVersia(uri: URL): Promise { - const userData = await User.federationRequester.fetchEntity( - uri, - VersiaEntities.User, - ); - - const user = await User.fromVersia(userData); - - await searchManager.addUser(user); - - return user; - } - /** * Change the emojis linked to this user in database * @param emojis @@ -679,6 +619,13 @@ export class User extends BaseInterface { ); } + /** + * Tries to fetch a Versia user from the given URL. + * + * @param url The URL to fetch the user from + */ + public static async fromVersia(url: URL): Promise; + /** * Takes a Versia User representation, and serializes it to the database. * @@ -687,7 +634,36 @@ export class User extends BaseInterface { */ public static async fromVersia( versiaUser: VersiaEntities.User, + ): Promise; + + public static async fromVersia( + versiaUser: VersiaEntities.User | URL, ): Promise { + if (versiaUser instanceof URL) { + let uri = versiaUser; + const instance = await Instance.resolve(uri); + + if (instance.data.protocol === "activitypub") { + if (!config.federation.bridge) { + throw new Error("ActivityPub bridge is not enabled"); + } + + uri = new URL( + `/apbridge/versia/query?${new URLSearchParams({ + user_url: uri.href, + })}`, + config.federation.bridge.url, + ); + } + + const user = await User.federationRequester.fetchEntity( + uri, + VersiaEntities.User, + ); + + return User.fromVersia(user); + } + const { username, inbox, @@ -799,7 +775,7 @@ export class User extends BaseInterface { getLogger(["federation", "resolvers"]) .debug`Resolving user ${chalk.gray(uri)}`; // Check if user not already in database - const foundUser = await User.fromSql(eq(Users.uri, uri.toString())); + const foundUser = await User.fromSql(eq(Users.uri, uri.href)); if (foundUser) { return foundUser; @@ -821,7 +797,7 @@ export class User extends BaseInterface { getLogger(["federation", "resolvers"]) .debug`User not found in database, fetching from remote`; - return await User.fetchFromRemote(uri); + return User.fromVersia(uri); } /** @@ -983,7 +959,7 @@ export class User extends BaseInterface { ["sign"], ) .then((k) => { - return new FederationRequester(k, this.getUri()); + return new FederationRequester(k, this.uri); }); } @@ -1037,7 +1013,7 @@ export class User extends BaseInterface { if (!inbox) { throw new Error( - `User ${chalk.gray(user.getUri())} does not have an inbox endpoint`, + `User ${chalk.gray(user.uri)} does not have an inbox endpoint`, ); } @@ -1048,7 +1024,7 @@ export class User extends BaseInterface { ); } catch (e) { getLogger(["federation", "delivery"]) - .error`Federating ${chalk.gray(entity.data.type)} to ${user.getUri()} ${chalk.bold.red("failed")}`; + .error`Federating ${chalk.gray(entity.data.type)} to ${user.uri} ${chalk.bold.red("failed")}`; getLogger(["federation", "delivery"]).error`${e}`; sentry?.captureException(e); @@ -1066,10 +1042,10 @@ export class User extends BaseInterface { username: user.username, display_name: user.displayName || user.username, note: user.note, - uri: this.getUri().toString(), + uri: this.uri.href, url: user.uri || - new URL(`/@${user.username}`, config.http.base_url).toString(), + new URL(`/@${user.username}`, config.http.base_url).href, avatar: this.getAvatarUrl().proxied, header: this.getHeaderUrl()?.proxied ?? "", locked: user.isLocked, @@ -1123,7 +1099,7 @@ export class User extends BaseInterface { return new VersiaEntities.User({ id: user.id, type: "User", - uri: this.getUri(), + uri: this.uri, bio: { "text/html": { content: user.note, @@ -1190,7 +1166,7 @@ export class User extends BaseInterface { public toMention(): z.infer { return { - url: this.getUri().toString(), + url: this.uri.href, username: this.data.username, acct: this.getAcct(), id: this.id, diff --git a/classes/functions/status.ts b/classes/functions/status.ts index 87d5dd21..42efcf0f 100644 --- a/classes/functions/status.ts +++ b/classes/functions/status.ts @@ -296,7 +296,7 @@ export const parseTextMentions = async ( export const replaceTextMentions = (text: string, mentions: User[]): string => { return mentions.reduce((finalText, mention) => { const { username, instance } = mention.data; - const uri = mention.getUri(); + const { uri } = mention; const baseHost = config.http.base_url.host; const linkTemplate = (displayText: string): string => `${displayText}`; diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index 79e826b0..7f74da3c 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -254,7 +254,7 @@ export class InboxProcessor { ); if (!followee.data.isLocked) { - await followee.sendFollowAccept(author); + await followee.acceptFollowRequest(author); } } diff --git a/cli/user/refetch.ts b/cli/user/refetch.ts index c6ff0f9d..43d782fc 100644 --- a/cli/user/refetch.ts +++ b/cli/user/refetch.ts @@ -3,6 +3,7 @@ import chalk from "chalk"; // biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work import { type Root, defineCommand } from "clerc"; import ora from "ora"; +import { User } from "~/classes/database/user.ts"; import { retrieveUser } from "../utils.ts"; export const refetchUserCommand = defineCommand( @@ -29,7 +30,7 @@ export const refetchUserCommand = defineCommand( const spinner = ora("Refetching user").start(); try { - await user.updateFromRemote(); + await User.fromVersia(user.uri); } catch (error) { spinner.fail( `Failed to refetch user ${chalk.gray(user.data.username)}`,