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)}`,