feat(federation): Finalize Versia 0.6 port

This commit is contained in:
Jesse Wierzbinski 2026-03-31 04:13:16 +02:00
parent fca30b4dad
commit b7e77097ba
No known key found for this signature in database
20 changed files with 2788 additions and 439 deletions

View file

@ -941,20 +941,19 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
*/
public static async resolve(
reference: VersiaEntities.Reference,
defaultInstance?: Instance,
): Promise<Note | null> {
// Check if note not already in database
if (
!reference.domain ||
!(reference.domain || defaultInstance) ||
reference.domain === config.http.base_url.hostname
) {
return await Note.fromId(reference.id);
}
const instance = await Instance.resolve(reference.domain);
if (!instance) {
return null;
}
const instance = reference.domain
? await Instance.resolve(reference.domain)
: (defaultInstance as Instance);
const foundNote = await Note.fromSql(
and(
@ -963,7 +962,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
Notes.authorId,
sql`(
SELECT "Users".id FROM "Users"
WHERE "Users".instanceId = ${instance.id}
WHERE "Users"."instanceId" = ${instance.id}
LIMIT 1
)`,
),
@ -974,7 +973,14 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return foundNote;
}
return Note.fromVersia(reference);
return Note.fromVersia(
reference.domain
? reference
: new VersiaEntities.Reference(
reference.id,
instance.data.baseUrl,
),
);
}
/**
@ -983,28 +989,45 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
* If the note already exists, it will update it.
* @param versiaNote - Reference or Versia Note representation
*/
public static async fromVersia(
versiaNote: VersiaEntities.Note,
instance: Instance,
): Promise<Note>;
public static async fromVersia(
reference: VersiaEntities.Reference,
): Promise<Note>;
public static async fromVersia(
versiaNote: VersiaEntities.Note | VersiaEntities.Reference,
instance?: Instance,
): Promise<Note> {
if (versiaNote instanceof VersiaEntities.Reference) {
if (!versiaNote.domain) {
throw new Error(
"Cannot fetch Versia note from reference without domain",
);
}
// No bridge support for notes yet
const note = await Instance.federationRequester.fetchEntity(
versiaNote,
VersiaEntities.Note,
);
return Note.fromVersia(note);
const instance = await Instance.resolve(versiaNote.domain);
return Note.fromVersia(note, instance);
}
if (!instance) {
throw new Error("Instance must be provided when fetching note");
}
const { created_at, extensions, group, id, is_sensitive, subject } =
versiaNote.data;
if (!versiaNote.author.domain) {
throw new Error("Entity author domain is missing");
}
const instance = await Instance.resolve(versiaNote.author.domain);
const author = await User.resolve(versiaNote.author);
const author = await User.resolve(versiaNote.author, instance);
if (!author) {
throw new Error("Entity author could not be resolved");
@ -1041,7 +1064,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
const mentions = (
await Promise.all(
versiaNote.mentions.map((m) => User.resolve(m)) ?? [],
versiaNote.mentions.map((m) => User.resolve(m, instance)) ?? [],
)
).filter((m) => m !== null);
@ -1052,10 +1075,10 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
: (group as "public" | "followers" | "unlisted");
const reply = versiaNote.repliesTo
? await Note.resolve(versiaNote.repliesTo)
? await Note.resolve(versiaNote.repliesTo, instance)
: null;
const quote = versiaNote.quotes
? await Note.resolve(versiaNote.quotes)
? await Note.resolve(versiaNote.quotes, instance)
: null;
const spoiler = subject ? await sanitizedHtmlStrip(subject) : undefined;

View file

@ -672,9 +672,18 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
* If the user already exists, it will update it.
* @param versiaUser Reference or Versia User representation
*/
public static async fromVersia(
versiaUser: VersiaEntities.User,
instance: Instance,
): Promise<User>;
public static async fromVersia(
versiaUser: VersiaEntities.Reference,
): Promise<User>;
public static async fromVersia(
versiaUser: VersiaEntities.User | VersiaEntities.Reference,
domain: string,
instance?: Instance,
): Promise<User> {
if (versiaUser instanceof VersiaEntities.Reference) {
if (!versiaUser.domain) {
@ -688,7 +697,13 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
VersiaEntities.User,
);
return User.fromVersia(user, versiaUser.domain);
const instance = await Instance.resolve(versiaUser.domain);
return User.fromVersia(user, instance);
}
if (!instance) {
throw new Error("Instance must be provided when fetching user");
}
const {
@ -702,7 +717,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
extensions,
} = versiaUser.data;
const instance = await Instance.resolve(domain);
const existingUser = await User.fromSql(
and(eq(Users.instanceId, instance.id), eq(Users.remoteId, id)),
);
@ -788,10 +802,11 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
public static async resolve(
reference: VersiaEntities.Reference,
defaultInstance?: Instance,
): Promise<User> {
// Check if user not already in database
if (
!reference.domain ||
!(reference.domain || defaultInstance) ||
reference.domain === config.http.base_url.hostname
) {
const user = await User.fromId(reference.id);
@ -805,7 +820,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return user;
}
const instance = await Instance.resolve(reference.domain);
const instance = reference.domain
? await Instance.resolve(reference.domain)
: (defaultInstance as Instance);
const foundUser = await User.fromSql(
and(
@ -818,7 +835,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return foundUser;
}
return User.fromVersia(reference, reference.domain);
return User.fromVersia(
reference.domain
? reference
: new VersiaEntities.Reference(
reference.id,
instance.data.baseUrl,
),
);
}
/**

View file

@ -182,32 +182,36 @@ export class InboxProcessor {
shouldCheckSignature && federationInboxLogger.debug`Signature is valid`;
try {
// TODO: Rip out bridge code so this is never null
const instance = this.sender?.instance as Instance;
await new EntitySorter(this.body)
.on(VersiaEntities.Note, (n) => InboxProcessor.processNote(n))
.on(VersiaEntities.Note, (n) =>
InboxProcessor.processNote(n, instance),
)
.on(VersiaEntities.Follow, (f) =>
InboxProcessor.processFollowRequest(f),
InboxProcessor.processFollowRequest(f, instance),
)
.on(VersiaEntities.FollowAccept, (f) =>
InboxProcessor.processFollowAccept(f),
InboxProcessor.processFollowAccept(f, instance),
)
.on(VersiaEntities.FollowReject, (f) =>
InboxProcessor.processFollowReject(f),
InboxProcessor.processFollowReject(f, instance),
)
.on(VersiaEntities.Like, (l) =>
InboxProcessor.processLikeRequest(l),
InboxProcessor.processLikeRequest(l, instance),
)
.on(VersiaEntities.Delete, (d) =>
InboxProcessor.processDelete(d),
InboxProcessor.processDelete(d, instance),
)
.on(VersiaEntities.User, (u) =>
InboxProcessor.processUser(
u,
this.sender?.instance.data.baseUrl ?? "",
),
InboxProcessor.processUser(u, instance),
)
.on(VersiaEntities.Share, (s) =>
InboxProcessor.processShare(s, instance),
)
.on(VersiaEntities.Share, (s) => InboxProcessor.processShare(s))
.on(VersiaEntities.Reaction, (r) =>
InboxProcessor.processReaction(r),
InboxProcessor.processReaction(r, instance),
)
.sort(() => {
throw new ApiError(400, "Unknown entity type");
@ -225,9 +229,10 @@ export class InboxProcessor {
*/
private static async processReaction(
reaction: VersiaEntities.Reaction,
sender: Instance,
): Promise<void> {
const author = await User.resolve(reaction.author);
const note = await Note.resolve(reaction.object);
const author = await User.resolve(reaction.author, sender);
const note = await Note.resolve(reaction.object, sender);
if (!author) {
throw new ApiError(404, "Author not found");
@ -246,7 +251,10 @@ export class InboxProcessor {
* @param {VersiaNote} note - The Note entity to process.
* @returns {Promise<void>}
*/
private static async processNote(note: VersiaEntities.Note): Promise<void> {
private static async processNote(
note: VersiaEntities.Note,
sender: Instance,
): Promise<void> {
// If note has a blocked word
if (
Object.values(note.content?.data ?? {})
@ -262,7 +270,7 @@ export class InboxProcessor {
return;
}
await Note.fromVersia(note);
await Note.fromVersia(note, sender);
}
/**
@ -274,7 +282,7 @@ export class InboxProcessor {
*/
private static async processUser(
user: VersiaEntities.User,
domain: string,
sender: Instance,
): Promise<void> {
if (
config.validation.filters.username.some((filter) =>
@ -303,7 +311,7 @@ export class InboxProcessor {
return;
}
await User.fromVersia(user, domain);
await User.fromVersia(user, sender);
}
/**
@ -314,9 +322,10 @@ export class InboxProcessor {
*/
private static async processFollowRequest(
follow: VersiaEntities.Follow,
sender: Instance,
): Promise<void> {
const author = await User.resolve(follow.author);
const followee = await User.resolve(follow.followee);
const author = await User.resolve(follow.author, sender);
const followee = await User.resolve(follow.followee, sender);
if (!author) {
throw new ApiError(404, "Author not found");
@ -362,9 +371,10 @@ export class InboxProcessor {
*/
private static async processFollowAccept(
followAccept: VersiaEntities.FollowAccept,
sender: Instance,
): Promise<void> {
const author = await User.resolve(followAccept.author);
const follower = await User.resolve(followAccept.follower);
const author = await User.resolve(followAccept.author, sender);
const follower = await User.resolve(followAccept.follower, sender);
if (!author) {
throw new ApiError(404, "Author not found");
@ -397,9 +407,10 @@ export class InboxProcessor {
*/
private static async processFollowReject(
followReject: VersiaEntities.FollowReject,
sender: Instance,
): Promise<void> {
const author = await User.resolve(followReject.author);
const follower = await User.resolve(followReject.follower);
const author = await User.resolve(followReject.author, sender);
const follower = await User.resolve(followReject.follower, sender);
if (!author) {
throw new ApiError(404, "Author not found");
@ -432,9 +443,10 @@ export class InboxProcessor {
*/
private static async processShare(
share: VersiaEntities.Share,
sender: Instance,
): Promise<void> {
const author = await User.resolve(share.author);
const sharedNote = await Note.resolve(share.shared);
const author = await User.resolve(share.author, sender);
const sharedNote = await Note.resolve(share.shared, sender);
if (!author) {
throw new ApiError(404, "Author not found");
@ -455,10 +467,11 @@ export class InboxProcessor {
*/ // JS doesn't allow the use of `delete` as a variable name
public static async processDelete(
delete_: VersiaEntities.Delete,
sender: Instance,
): Promise<void> {
const toDelete = delete_.deleted;
const author = await User.resolve(delete_.author);
const author = await User.resolve(delete_.author, sender);
switch (delete_.data.deleted_type) {
case "Note": {
@ -478,7 +491,7 @@ export class InboxProcessor {
return;
}
case "User": {
const userToDelete = await User.resolve(toDelete);
const userToDelete = await User.resolve(toDelete, sender);
if (!userToDelete) {
throw new ApiError(404, "User to delete not found");
@ -573,9 +586,10 @@ export class InboxProcessor {
*/
private static async processLikeRequest(
like: VersiaEntities.Like,
sender: Instance,
): Promise<void> {
const author = await User.resolve(like.author);
const likedNote = await Note.resolve(like.liked);
const author = await User.resolve(like.author, sender);
const likedNote = await Note.resolve(like.liked, sender);
if (!author) {
throw new ApiError(404, "Author not found");

View file

@ -87,7 +87,9 @@ export const parseMentionsFromText = async (text: string): Promise<User[]> => {
VersiaEntities.User,
);
const user = await User.fromVersia(userEntity, url.hostname);
const instance = await Instance.resolve(url.hostname);
const user = await User.fromVersia(userEntity, instance);
if (user) {
finalList.push(user);

View file

@ -0,0 +1,14 @@
ALTER TABLE "Likes" DROP CONSTRAINT "Likes_uri_unique";--> statement-breakpoint
ALTER TABLE "Notes" DROP CONSTRAINT "Notes_uri_unique";--> statement-breakpoint
ALTER TABLE "Users" DROP CONSTRAINT "Users_uri_unique";--> statement-breakpoint
DROP INDEX "Users_uri_index";--> statement-breakpoint
ALTER TABLE "Likes" ADD COLUMN "remote_id" text;--> statement-breakpoint
ALTER TABLE "Notes" ADD COLUMN "remote_id" text;--> statement-breakpoint
ALTER TABLE "Reaction" ADD COLUMN "remote_id" text;--> statement-breakpoint
ALTER TABLE "Users" ADD COLUMN "remote_id" text;--> statement-breakpoint
ALTER TABLE "Likes" DROP COLUMN "uri";--> statement-breakpoint
ALTER TABLE "Notes" DROP COLUMN "uri";--> statement-breakpoint
ALTER TABLE "Users" DROP COLUMN "uri";--> statement-breakpoint
ALTER TABLE "Users" DROP COLUMN "endpoints";--> statement-breakpoint
ALTER TABLE "Users" DROP COLUMN "public_key";--> statement-breakpoint
ALTER TABLE "Users" DROP COLUMN "private_key";

File diff suppressed because it is too large Load diff

View file

@ -379,6 +379,13 @@
"when": 1765422160004,
"tag": "0053_lively_hellfire_club",
"breakpoints": true
},
{
"idx": 54,
"version": "7",
"when": 1771983340896,
"tag": "0054_good_madelyne_pryor",
"breakpoints": true
}
]
}