mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(federation): 🔥 Remove confusing User federation methods
This commit is contained in:
parent
9ff9b90f6b
commit
54b2dfb78d
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ describe("/api/v1/statuses", () => {
|
|||
|
||||
expect(ok).toBe(true);
|
||||
expect(data).toMatchObject({
|
||||
content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].getUri()}">@${users[1].data.username}</a>!</p>`,
|
||||
content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].uri.href}">@${users[1].data.username}</a>!</p>`,
|
||||
});
|
||||
expect((data as z.infer<typeof Status>).mentions).toBeArrayOfSize(
|
||||
1,
|
||||
|
|
@ -241,7 +241,7 @@ describe("/api/v1/statuses", () => {
|
|||
|
||||
expect(ok).toBe(true);
|
||||
expect(data).toMatchObject({
|
||||
content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].getUri()}">@${users[1].data.username}</a>!</p>`,
|
||||
content: `<p>Hello, <a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${users[1].uri.href}">@${users[1].data.username}</a>!</p>`,
|
||||
});
|
||||
expect((data as z.infer<typeof Status>).mentions).toBeArrayOfSize(
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -861,7 +861,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
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<typeof Notes, NoteTypeWithRelations> {
|
|||
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": {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
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<void> {
|
||||
public async acceptFollowRequest(follower: User): Promise<void> {
|
||||
if (!follower.isRemote()) {
|
||||
throw new Error("Follower must be a remote user");
|
||||
}
|
||||
|
|
@ -265,9 +265,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
});
|
||||
}
|
||||
|
||||
public async sendFollowReject(follower: User): Promise<void> {
|
||||
public async rejectFollowRequest(follower: User): Promise<void> {
|
||||
if (!follower.isRemote()) {
|
||||
throw new Error("Follower must be a remote user");
|
||||
}
|
||||
|
|
@ -289,9 +289,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
|
||||
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<typeof Users, UserWithRelations> {
|
|||
);
|
||||
}
|
||||
|
||||
public async updateFromRemote(): Promise<User> {
|
||||
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<User | null> {
|
||||
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<User> {
|
||||
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<typeof Users, UserWithRelations> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<User>;
|
||||
|
||||
/**
|
||||
* Takes a Versia User representation, and serializes it to the database.
|
||||
*
|
||||
|
|
@ -687,7 +634,36 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
*/
|
||||
public static async fromVersia(
|
||||
versiaUser: VersiaEntities.User,
|
||||
): Promise<User>;
|
||||
|
||||
public static async fromVersia(
|
||||
versiaUser: VersiaEntities.User | URL,
|
||||
): Promise<User> {
|
||||
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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
["sign"],
|
||||
)
|
||||
.then((k) => {
|
||||
return new FederationRequester(k, this.getUri());
|
||||
return new FederationRequester(k, this.uri);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1037,7 +1013,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
|
||||
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<typeof Users, UserWithRelations> {
|
|||
);
|
||||
} 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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
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<typeof Users, UserWithRelations> {
|
|||
|
||||
public toMention(): z.infer<typeof MentionSchema> {
|
||||
return {
|
||||
url: this.getUri().toString(),
|
||||
url: this.uri.href,
|
||||
username: this.data.username,
|
||||
acct: this.getAcct(),
|
||||
id: this.id,
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ export class InboxProcessor {
|
|||
);
|
||||
|
||||
if (!followee.data.isLocked) {
|
||||
await followee.sendFollowAccept(author);
|
||||
await followee.acceptFollowRequest(author);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue