refactor(federation): Make references always have domains
Some checks failed
CodeQL Scan / Analyze (push) Failing after 0s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
Build Docker Images / lint (push) Has been cancelled
Build Docker Images / check (push) Has been cancelled
Build Docker Images / tests (push) Has been cancelled
Build Docker Images / detect-circular (push) Has been cancelled
Mirror to Codeberg / Mirror (push) Has been cancelled
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been cancelled
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been cancelled

This commit is contained in:
Jesse Wierzbinski 2026-04-03 13:34:19 +02:00
parent df2a5ce260
commit 709e1c6087
No known key found for this signature in database
22 changed files with 688 additions and 477 deletions

View file

@ -4,6 +4,7 @@ import {
EntitySchema, EntitySchema,
URICollectionSchema, URICollectionSchema,
} from "@versia/sdk/schemas"; } from "@versia/sdk/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit"; import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api"; import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db, Instance, Note, User } from "@versia-server/kit/db"; import { db, Instance, Note, User } from "@versia-server/kit/db";
@ -112,13 +113,16 @@ export default apiRoute((app) =>
), ),
); );
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: note.author.id, {
total: replyCount, author: note.author.id,
items: replies.map((reply) => total: replyCount,
reply.reference.toString(), items: replies.map((reply) =>
), reply.reference.toString(),
}); ),
},
config.http.base_url.hostname,
);
break; break;
} }
case "quotes": { case "quotes": {
@ -146,13 +150,16 @@ export default apiRoute((app) =>
), ),
); );
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: note.author.id, {
total: quoteCount, author: note.author.id,
items: quotes.map((quote) => total: quoteCount,
quote.reference.toString(), items: quotes.map((quote) =>
), quote.reference.toString(),
}); ),
},
config.http.base_url.hostname,
);
break; break;
} }
case "pub.versia:share/Shares": { case "pub.versia:share/Shares": {
@ -180,13 +187,16 @@ export default apiRoute((app) =>
), ),
); );
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: note.author.id, {
total: shareCount, author: note.author.id,
items: shares.map((share) => total: shareCount,
share.reference.toString(), items: shares.map((share) =>
), share.reference.toString(),
}); ),
},
config.http.base_url.hostname,
);
break; break;
} }
} }
@ -226,23 +236,29 @@ export default apiRoute((app) =>
offset, offset,
); );
entity = new VersiaEntities.Collection({ entity = new VersiaEntities.Collection(
author: user.id, {
total, author: user.id,
items: outboxItems.map((note) => total,
note.toVersia(), items: outboxItems.map((note) =>
), note.toVersia(),
}); ),
},
config.http.base_url.hostname,
);
break; break;
} }
case "followers": { case "followers": {
if (user.data.isHidingCollections) { if (user.data.isHidingCollections) {
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: user.id, {
items: [], author: user.id,
total: 0, items: [],
}); total: 0,
},
config.http.base_url.hostname,
);
break; break;
} }
@ -258,23 +274,29 @@ export default apiRoute((app) =>
offset, offset,
); );
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: user.id, {
items: followers.map((follower) => author: user.id,
follower.reference.toString(), items: followers.map((follower) =>
), follower.reference.toString(),
total, ),
}); total,
},
config.http.base_url.hostname,
);
break; break;
} }
case "following": { case "following": {
if (user.data.isHidingCollections) { if (user.data.isHidingCollections) {
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: user.id, {
items: [], author: user.id,
total: 0, items: [],
}); total: 0,
},
config.http.base_url.hostname,
);
break; break;
} }
@ -290,13 +312,16 @@ export default apiRoute((app) =>
offset, offset,
); );
entity = new VersiaEntities.URICollection({ entity = new VersiaEntities.URICollection(
author: user.id, {
items: following.map((followed) => author: user.id,
followed.reference.toString(), items: following.map((followed) =>
), followed.reference.toString(),
total, ),
}); total,
},
config.http.base_url.hostname,
);
break; break;
} }

View file

@ -83,15 +83,18 @@ mock(new URL(`/.versia/v0.6/entities/User/${userId}`, instanceUrl).href, {
headers: { headers: {
"Content-Type": "application/vnd.versia+json; charset=utf-8", "Content-Type": "application/vnd.versia+json; charset=utf-8",
}, },
data: new VersiaEntities.User({ data: new VersiaEntities.User(
id: userId, {
created_at: "2025-04-18T10:32:01.427Z", id: userId,
type: "User", created_at: "2025-04-18T10:32:01.427Z",
username: "testuser", type: "User",
fields: [], username: "testuser",
manually_approves_followers: false, fields: [],
indexable: true, manually_approves_followers: false,
}).toJSON(), indexable: true,
},
instanceUrl.hostname,
).toJSON(),
}, },
}); });
@ -111,35 +114,38 @@ afterAll(async () => {
describe("Inbox Tests", () => { describe("Inbox Tests", () => {
test("should correctly process inbox request", async () => { test("should correctly process inbox request", async () => {
const exampleNote = new VersiaEntities.Note({ const exampleNote = new VersiaEntities.Note(
id: noteId, {
created_at: "2025-04-18T10:32:01.427Z", id: noteId,
type: "Note", created_at: "2025-04-18T10:32:01.427Z",
extensions: { type: "Note",
"pub.versia:custom_emojis": { extensions: {
emojis: [], "pub.versia:custom_emojis": {
emojis: [],
},
}, },
previews: [],
attachments: [],
author: userId,
content: {
"text/html": {
content: "<p>Hello!</p>",
remote: false,
},
"text/plain": {
content: "Hello!",
remote: false,
},
},
group: "public",
is_sensitive: false,
mentions: [],
quotes: null,
replies_to: null,
subject: "",
}, },
previews: [], instanceUrl.hostname,
attachments: [], );
author: userId,
content: {
"text/html": {
content: "<p>Hello!</p>",
remote: false,
},
"text/plain": {
content: "Hello!",
remote: false,
},
},
group: "public",
is_sensitive: false,
mentions: [],
quotes: null,
replies_to: null,
subject: "",
});
const signedRequest = await sign( const signedRequest = await sign(
instanceKeys.privateKey, instanceKeys.privateKey,
@ -177,13 +183,16 @@ describe("Inbox Tests", () => {
}); });
test("should correctly process Share", async () => { test("should correctly process Share", async () => {
const exampleRequest = new VersiaEntities.Share({ const exampleRequest = new VersiaEntities.Share(
id: shareId, {
created_at: "2025-04-18T10:32:01.427Z", id: shareId,
type: "pub.versia:share/Share", created_at: "2025-04-18T10:32:01.427Z",
author: userId, type: "pub.versia:share/Share",
shared: noteId, author: userId,
}); shared: noteId,
},
instanceUrl.hostname,
);
const signedRequest = await sign( const signedRequest = await sign(
instanceKeys.privateKey, instanceKeys.privateKey,
@ -229,14 +238,17 @@ describe("Inbox Tests", () => {
}); });
test("should correctly process Reaction", async () => { test("should correctly process Reaction", async () => {
const exampleRequest = new VersiaEntities.Reaction({ const exampleRequest = new VersiaEntities.Reaction(
id: reactionId, {
created_at: "2025-04-18T10:32:01.427Z", id: reactionId,
type: "pub.versia:reactions/Reaction", created_at: "2025-04-18T10:32:01.427Z",
author: userId, type: "pub.versia:reactions/Reaction",
object: noteId, author: userId,
content: "👍", object: noteId,
}); content: "👍",
},
instanceUrl.hostname,
);
const signedRequest = await sign( const signedRequest = await sign(
instanceKeys.privateKey, instanceKeys.privateKey,
@ -304,34 +316,37 @@ describe("Inbox Tests", () => {
}); });
test("should correctly process Reaction with custom emoji", async () => { test("should correctly process Reaction with custom emoji", async () => {
const exampleRequest = new VersiaEntities.Reaction({ const exampleRequest = new VersiaEntities.Reaction(
id: reaction2Id, {
created_at: "2025-04-18T10:32:01.427Z", id: reaction2Id,
type: "pub.versia:reactions/Reaction", created_at: "2025-04-18T10:32:01.427Z",
author: userId, type: "pub.versia:reactions/Reaction",
object: noteId, author: userId,
content: ":neocat:", object: noteId,
extensions: { content: ":neocat:",
"pub.versia:custom_emojis": { extensions: {
emojis: [ "pub.versia:custom_emojis": {
{ emojis: [
name: ":neocat:", {
url: { name: ":neocat:",
"image/webp": { url: {
hash: "e06240155d2cb90e8dc05327d023585ab9d47216ff547ad72aaf75c485fe9649", "image/webp": {
size: 4664, hash: "e06240155d2cb90e8dc05327d023585ab9d47216ff547ad72aaf75c485fe9649",
width: 256, size: 4664,
height: 256, width: 256,
remote: true, height: 256,
content: remote: true,
"https://cdn.cpluspatch.com/versia-cpp/e06240155d2cb90e8dc05327d023585ab9d47216ff547ad72aaf75c485fe9649/neocat.webp", content:
"https://cdn.cpluspatch.com/versia-cpp/e06240155d2cb90e8dc05327d023585ab9d47216ff547ad72aaf75c485fe9649/neocat.webp",
},
}, },
}, },
}, ],
], },
}, },
}, },
}); instanceUrl.hostname,
);
const signedRequest = await sign( const signedRequest = await sign(
instanceKeys.privateKey, instanceKeys.privateKey,
@ -405,13 +420,16 @@ describe("Inbox Tests", () => {
expect(noteToDelete).not.toBeNull(); expect(noteToDelete).not.toBeNull();
// Create a Delete request // Create a Delete request
const exampleRequest = new VersiaEntities.Delete({ const exampleRequest = new VersiaEntities.Delete(
created_at: new Date().toISOString(), {
type: "Delete", created_at: new Date().toISOString(),
author: userId, type: "Delete",
deleted_type: "Note", author: userId,
deleted: noteId, deleted_type: "Note",
}); deleted: noteId,
},
instanceUrl.hostname,
);
const signedRequest = await sign( const signedRequest = await sign(
instanceKeys.privateKey, instanceKeys.privateKey,

View file

@ -1,4 +1,5 @@
import * as VersiaEntities from "@versia/sdk/entities"; import * as VersiaEntities from "@versia/sdk/entities";
import { config } from "@versia-server/config";
import { import {
and, and,
desc, desc,
@ -20,7 +21,9 @@ import { BaseInterface } from "./base.ts";
import type { User } from "./user.ts"; import type { User } from "./user.ts";
type LikeType = InferSelectModel<typeof Likes> & { type LikeType = InferSelectModel<typeof Likes> & {
liker: InferSelectModel<typeof Users>; liker: InferSelectModel<typeof Users> & {
instance: InferSelectModel<typeof Instances> | null;
};
liked: InferSelectModel<typeof Notes> & { liked: InferSelectModel<typeof Notes> & {
author: InferSelectModel<typeof Users> & { author: InferSelectModel<typeof Users> & {
instance: InferSelectModel<typeof Instances> | null; instance: InferSelectModel<typeof Instances> | null;
@ -70,7 +73,11 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
}, },
}, },
}, },
liker: true, liker: {
with: {
instance: true,
},
},
}, },
}); });
@ -102,7 +109,11 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
}, },
}, },
}, },
liker: true, liker: {
with: {
instance: true,
},
},
}, },
}); });
@ -167,22 +178,28 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
likedReference = `${this.data.liked.author.instance.domain}:${this.data.liked.remoteId}`; likedReference = `${this.data.liked.author.instance.domain}:${this.data.liked.remoteId}`;
} }
return new VersiaEntities.Like({ return new VersiaEntities.Like(
id: this.id, {
author: this.data.liker.id, id: this.id,
type: "pub.versia:likes/Like", author: this.data.liker.id,
created_at: this.data.createdAt.toISOString(), type: "pub.versia:likes/Like",
liked: likedReference, created_at: this.data.createdAt.toISOString(),
}); liked: likedReference,
},
this.data.liker.instance?.domain ?? config.http.base_url.hostname,
);
} }
public unlikeToVersia(unliker?: User): VersiaEntities.Delete { public unlikeToVersia(unliker?: User): VersiaEntities.Delete {
return new VersiaEntities.Delete({ return new VersiaEntities.Delete(
type: "Delete", {
created_at: new Date().toISOString(), type: "Delete",
author: unliker ? unliker.id : this.data.liker.id, created_at: new Date().toISOString(),
deleted_type: "pub.versia:likes/Like", author: unliker ? unliker.id : this.data.liker.id,
deleted: this.id, deleted_type: "pub.versia:likes/Like",
}); deleted: this.id,
},
this.data.liker.instance?.domain ?? config.http.base_url.hostname,
);
} }
} }

View file

@ -426,13 +426,13 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
public get reference(): VersiaEntities.Reference { public get reference(): VersiaEntities.Reference {
if (this.remote) { if (this.remote) {
const instanceUrl = new URL( return new VersiaEntities.Reference(
this.id,
this.author.data.instance?.domain || "", this.author.data.instance?.domain || "",
); );
return new VersiaEntities.Reference(this.id, instanceUrl.hostname);
} }
return new VersiaEntities.Reference(this.id); return new VersiaEntities.Reference(this.id, config.http.base_url.host);
} }
public async federateToUsers(): Promise<void> { public async federateToUsers(): Promise<void> {
@ -941,19 +941,13 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
*/ */
public static async resolve( public static async resolve(
reference: VersiaEntities.Reference, reference: VersiaEntities.Reference,
defaultInstance?: Instance,
): Promise<Note | null> { ): Promise<Note | null> {
// Check if note not already in database // Check if note not already in database
if ( if (reference.domain === config.http.base_url.hostname) {
!(reference.domain || defaultInstance) ||
reference.domain === config.http.base_url.hostname
) {
return await Note.fromId(reference.id); return await Note.fromId(reference.id);
} }
const instance = reference.domain const instance = await Instance.resolve(reference.domain);
? await Instance.resolve(reference.domain)
: (defaultInstance as Instance);
const foundNote = await Note.fromSql( const foundNote = await Note.fromSql(
and( and(
@ -973,14 +967,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return foundNote; return foundNote;
} }
return Note.fromVersia( return Note.fromVersia(reference);
reference.domain
? reference
: new VersiaEntities.Reference(
reference.id,
instance.data.domain,
),
);
} }
/** /**
@ -1003,12 +990,6 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
instance?: Instance, instance?: Instance,
): Promise<Note> { ): Promise<Note> {
if (versiaNote instanceof VersiaEntities.Reference) { 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 // No bridge support for notes yet
const note = await Instance.federationRequester.fetchEntity( const note = await Instance.federationRequester.fetchEntity(
versiaNote, versiaNote,
@ -1027,7 +1008,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
const { created_at, extensions, group, id, is_sensitive, subject } = const { created_at, extensions, group, id, is_sensitive, subject } =
versiaNote.data; versiaNote.data;
const author = await User.resolve(versiaNote.author, instance); const author = await User.resolve(versiaNote.author);
if (!author) { if (!author) {
throw new Error("Entity author could not be resolved"); throw new Error("Entity author could not be resolved");
@ -1064,7 +1045,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
const mentions = ( const mentions = (
await Promise.all( await Promise.all(
versiaNote.mentions.map((m) => User.resolve(m, instance)) ?? [], versiaNote.mentions.map((m) => User.resolve(m)) ?? [],
) )
).filter((m) => m !== null); ).filter((m) => m !== null);
@ -1075,10 +1056,10 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
: (group as "public" | "followers" | "unlisted"); : (group as "public" | "followers" | "unlisted");
const reply = versiaNote.repliesTo const reply = versiaNote.repliesTo
? await Note.resolve(versiaNote.repliesTo, instance) ? await Note.resolve(versiaNote.repliesTo)
: null; : null;
const quote = versiaNote.quotes const quote = versiaNote.quotes
? await Note.resolve(versiaNote.quotes, instance) ? await Note.resolve(versiaNote.quotes)
: null; : null;
const spoiler = subject ? await sanitizedHtmlStrip(subject) : undefined; const spoiler = subject ? await sanitizedHtmlStrip(subject) : undefined;
@ -1296,13 +1277,16 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
} }
public deleteToVersia(): VersiaEntities.Delete { public deleteToVersia(): VersiaEntities.Delete {
return new VersiaEntities.Delete({ return new VersiaEntities.Delete(
type: "Delete", {
author: this.author.id, type: "Delete",
deleted_type: "Note", author: this.author.id,
deleted: this.id, deleted_type: "Note",
created_at: new Date().toISOString(), deleted: this.id,
}); created_at: new Date().toISOString(),
},
this.data.author.instance?.domain ?? config.http.base_url.hostname,
);
} }
/** /**
@ -1324,48 +1308,51 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
replyReference = `${status.reply.author.instance.domain}:${status.reply.remoteId}`; replyReference = `${status.reply.author.instance.domain}:${status.reply.remoteId}`;
} }
return new VersiaEntities.Note({ return new VersiaEntities.Note(
type: "Note", {
created_at: status.createdAt.toISOString(), type: "Note",
id: status.id, created_at: status.createdAt.toISOString(),
author: this.author.id, id: status.id,
content: { author: this.author.id,
"text/html": { content: {
content: status.content, "text/html": {
remote: false, content: status.content,
remote: false,
},
"text/plain": {
content: htmlToText(status.content),
remote: false,
},
}, },
"text/plain": { previews: [],
content: htmlToText(status.content), attachments: status.attachments.map(
remote: false, (attachment) =>
new Media(attachment).toVersia().data as z.infer<
typeof NonTextContentFormatSchema
>,
),
is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) =>
mention.instance
? `${mention.instance.domain}:${mention.id}`
: mention.id,
),
quotes: quoteReference,
replies_to: replyReference,
subject: status.spoilerText,
// TODO: Refactor as part of groups
group: status.visibility === "public" ? "public" : "followers",
extensions: {
"pub.versia:custom_emojis": {
emojis: status.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),
},
// TODO: Add polls and reactions
}, },
}, },
previews: [], status.author.instance?.domain ?? config.http.base_url.hostname,
attachments: status.attachments.map( );
(attachment) =>
new Media(attachment).toVersia().data as z.infer<
typeof NonTextContentFormatSchema
>,
),
is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) =>
mention.instance
? `${mention.instance.domain}:${mention.id}`
: mention.id,
),
quotes: quoteReference,
replies_to: replyReference,
subject: status.spoilerText,
// TODO: Refactor as part of groups
group: status.visibility === "public" ? "public" : "followers",
extensions: {
"pub.versia:custom_emojis": {
emojis: status.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),
},
// TODO: Add polls and reactions
},
});
} }
public toVersiaShare(): VersiaEntities.Share { public toVersiaShare(): VersiaEntities.Share {
@ -1373,25 +1360,31 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
throw new Error("Cannot share a non-reblogged note"); throw new Error("Cannot share a non-reblogged note");
} }
return new VersiaEntities.Share({ return new VersiaEntities.Share(
type: "pub.versia:share/Share", {
author: this.author.id, type: "pub.versia:share/Share",
id: this.id, author: this.author.id,
created_at: new Date().toISOString(), id: this.id,
shared: this.data.reblog.author.instance created_at: new Date().toISOString(),
? `${this.data.reblog.author.instance.domain}:${this.data.reblog.id}` shared: this.data.reblog.author.instance
: this.data.reblog.id, ? `${this.data.reblog.author.instance.domain}:${this.data.reblog.id}`
}); : this.data.reblog.id,
},
this.data.author.instance?.domain ?? config.http.base_url.hostname,
);
} }
public toVersiaUnshare(): VersiaEntities.Delete { public toVersiaUnshare(): VersiaEntities.Delete {
return new VersiaEntities.Delete({ return new VersiaEntities.Delete(
type: "Delete", {
created_at: new Date().toISOString(), type: "Delete",
author: this.author.id, created_at: new Date().toISOString(),
deleted_type: "pub.versia:share/Share", author: this.author.id,
deleted: this.id, deleted_type: "pub.versia:share/Share",
}); deleted: this.id,
},
this.data.author.instance?.domain ?? config.http.base_url.hostname,
);
} }
/** /**

View file

@ -26,7 +26,9 @@ import type { User } from "./user.ts";
type ReactionType = InferSelectModel<typeof Reactions> & { type ReactionType = InferSelectModel<typeof Reactions> & {
emoji: typeof Emoji.$type | null; emoji: typeof Emoji.$type | null;
author: InferSelectModel<typeof Users>; author: InferSelectModel<typeof Users> & {
instance: InferSelectModel<typeof Instances> | null;
};
note: InferSelectModel<typeof Notes> & { note: InferSelectModel<typeof Notes> & {
author: InferSelectModel<typeof Users> & { author: InferSelectModel<typeof Users> & {
instance: InferSelectModel<typeof Instances> | null; instance: InferSelectModel<typeof Instances> | null;
@ -72,7 +74,11 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
media: true, media: true,
}, },
}, },
author: true, author: {
with: {
instance: true,
},
},
note: { note: {
with: { with: {
author: { author: {
@ -112,7 +118,11 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
media: true, media: true,
}, },
}, },
author: true, author: {
with: {
instance: true,
},
},
note: { note: {
with: { with: {
author: { author: {
@ -245,37 +255,43 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
noteReference = `${this.data.note.author.instance.domain}:${this.data.note.remoteId}`; noteReference = `${this.data.note.author.instance.domain}:${this.data.note.remoteId}`;
} }
return new VersiaEntities.Reaction({ return new VersiaEntities.Reaction(
type: "pub.versia:reactions/Reaction", {
author: this.data.author.id, type: "pub.versia:reactions/Reaction",
created_at: this.data.createdAt.toISOString(), author: this.data.author.id,
id: this.id, created_at: this.data.createdAt.toISOString(),
object: noteReference, id: this.id,
content: this.hasCustomEmoji() object: noteReference,
? `:${this.data.emoji?.shortcode}:` content: this.hasCustomEmoji()
: this.data.emojiText || "", ? `:${this.data.emoji?.shortcode}:`
extensions: this.hasCustomEmoji() : this.data.emojiText || "",
? { extensions: this.hasCustomEmoji()
"pub.versia:custom_emojis": { ? {
emojis: [ "pub.versia:custom_emojis": {
new Emoji( emojis: [
this.data.emoji as typeof Emoji.$type, new Emoji(
).toVersia(), this.data.emoji as typeof Emoji.$type,
], ).toVersia(),
}, ],
} },
: undefined, }
}); : undefined,
},
this.data.author.instance?.domain ?? config.http.base_url.hostname,
);
} }
public toVersiaUnreact(): VersiaEntities.Delete { public toVersiaUnreact(): VersiaEntities.Delete {
return new VersiaEntities.Delete({ return new VersiaEntities.Delete(
type: "Delete", {
created_at: new Date().toISOString(), type: "Delete",
author: this.data.authorId, created_at: new Date().toISOString(),
deleted_type: "pub.versia:reactions/Reaction", author: this.data.authorId,
deleted: this.id, deleted_type: "pub.versia:reactions/Reaction",
}); deleted: this.id,
},
this.data.author.instance?.domain ?? config.http.base_url.hostname,
);
} }
public static async fromVersia( public static async fromVersia(

View file

@ -224,7 +224,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
public get reference(): VersiaEntities.Reference { public get reference(): VersiaEntities.Reference {
if (this.local) { if (this.local) {
return new VersiaEntities.Reference(this.id); return new VersiaEntities.Reference(
this.id,
config.http.base_url.hostname,
);
} }
return new VersiaEntities.Reference( return new VersiaEntities.Reference(
@ -330,14 +333,17 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
} }
private unfollowToVersia(followee: User): VersiaEntities.Unfollow { private unfollowToVersia(followee: User): VersiaEntities.Unfollow {
return new VersiaEntities.Unfollow({ return new VersiaEntities.Unfollow(
type: "Unfollow", {
author: this.id, type: "Unfollow",
created_at: new Date().toISOString(), author: this.id,
followee: followee.data.instance created_at: new Date().toISOString(),
? `${followee.data.instance.domain}:${followee.id}` followee: followee.data.instance
: followee.id, ? `${followee.data.instance.domain}:${followee.id}`
}); : followee.id,
},
this.data.instance?.domain ?? config.http.base_url.hostname,
);
} }
public async acceptFollowRequest(follower: User): Promise<void> { public async acceptFollowRequest(follower: User): Promise<void> {
@ -352,14 +358,17 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await follower.recalculateFollowerCount(); await follower.recalculateFollowerCount();
await this.recalculateFollowingCount(); await this.recalculateFollowingCount();
const entity = new VersiaEntities.FollowAccept({ const entity = new VersiaEntities.FollowAccept(
type: "FollowAccept", {
author: this.id, type: "FollowAccept",
created_at: new Date().toISOString(), author: this.id,
follower: follower.data.instance created_at: new Date().toISOString(),
? `${follower.data.instance.domain}:${follower.id}` follower: follower.data.instance
: follower.id, ? `${follower.data.instance.domain}:${follower.id}`
}); : follower.id,
},
this.data.instance?.domain ?? config.http.base_url.hostname,
);
await deliveryQueue.add(DeliveryJobType.FederateEntity, { await deliveryQueue.add(DeliveryJobType.FederateEntity, {
entity: entity.toJSON(), entity: entity.toJSON(),
@ -377,14 +386,17 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
throw new Error("Followee must be a local user"); throw new Error("Followee must be a local user");
} }
const entity = new VersiaEntities.FollowReject({ const entity = new VersiaEntities.FollowReject(
type: "FollowReject", {
author: this.id, type: "FollowReject",
created_at: new Date().toISOString(), author: this.id,
follower: follower.data.instance created_at: new Date().toISOString(),
? `${follower.data.instance.domain}:${follower.id}` follower: follower.data.instance
: follower.id, ? `${follower.data.instance.domain}:${follower.id}`
}); : follower.id,
},
this.data.instance?.domain ?? config.http.base_url.hostname,
);
await deliveryQueue.add(DeliveryJobType.FederateEntity, { await deliveryQueue.add(DeliveryJobType.FederateEntity, {
entity: entity.toJSON(), entity: entity.toJSON(),
@ -686,12 +698,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
instance?: Instance, instance?: Instance,
): Promise<User> { ): Promise<User> {
if (versiaUser instanceof VersiaEntities.Reference) { if (versiaUser instanceof VersiaEntities.Reference) {
if (!versiaUser.domain) {
throw new Error(
"Cannot fetch Versia user from reference without domain",
);
}
const user = await Instance.federationRequester.fetchEntity( const user = await Instance.federationRequester.fetchEntity(
versiaUser, versiaUser,
VersiaEntities.User, VersiaEntities.User,
@ -802,13 +808,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
public static async resolve( public static async resolve(
reference: VersiaEntities.Reference, reference: VersiaEntities.Reference,
defaultInstance?: Instance,
): Promise<User> { ): Promise<User> {
// Check if user not already in database // Check if user not already in database
if ( if (reference.domain === config.http.base_url.hostname) {
!(reference.domain || defaultInstance) ||
reference.domain === config.http.base_url.hostname
) {
const user = await User.fromId(reference.id); const user = await User.fromId(reference.id);
if (!user) { if (!user) {
@ -820,9 +822,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return user; return user;
} }
const instance = reference.domain const instance = await Instance.resolve(reference.domain);
? await Instance.resolve(reference.domain)
: (defaultInstance as Instance);
const foundUser = await User.fromSql( const foundUser = await User.fromSql(
and( and(
@ -836,12 +836,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
} }
return User.fromVersia( return User.fromVersia(
reference.domain new VersiaEntities.Reference(reference.id, instance.data.domain),
? reference
: new VersiaEntities.Reference(
reference.id,
instance.data.domain,
),
); );
} }
@ -1088,39 +1083,42 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const user = this.data; const user = this.data;
return new VersiaEntities.User({ return new VersiaEntities.User(
id: user.id, {
type: "User", id: user.id,
bio: { type: "User",
"text/html": { bio: {
content: user.note, "text/html": {
remote: false, content: user.note,
remote: false,
},
"text/plain": {
content: htmlToText(user.note),
remote: false,
},
}, },
"text/plain": { created_at: user.createdAt.toISOString(),
content: htmlToText(user.note), indexable: this.data.isIndexable,
remote: false, username: user.username,
manually_approves_followers: this.data.isLocked,
avatar: this.avatar?.toVersia().data as z.infer<
typeof ImageContentFormatSchema
>,
header: this.header?.toVersia().data as z.infer<
typeof ImageContentFormatSchema
>,
display_name: user.displayName,
fields: user.fields,
extensions: {
"pub.versia:custom_emojis": {
emojis: user.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),
},
}, },
}, },
created_at: user.createdAt.toISOString(), this.data.instance?.domain ?? config.http.base_url.hostname,
indexable: this.data.isIndexable, );
username: user.username,
manually_approves_followers: this.data.isLocked,
avatar: this.avatar?.toVersia().data as z.infer<
typeof ImageContentFormatSchema
>,
header: this.header?.toVersia().data as z.infer<
typeof ImageContentFormatSchema
>,
display_name: user.displayName,
fields: user.fields,
extensions: {
"pub.versia:custom_emojis": {
emojis: user.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),
},
},
});
} }
public toMention(): z.infer<typeof MentionSchema> { public toMention(): z.infer<typeof MentionSchema> {

View file

@ -185,33 +185,31 @@ export class InboxProcessor {
// TODO: Rip out bridge code so this is never null // TODO: Rip out bridge code so this is never null
const instance = this.sender?.instance as Instance; const instance = this.sender?.instance as Instance;
await new EntitySorter(this.body) await new EntitySorter(this.body, instance.data.domain)
.on(VersiaEntities.Note, (n) => .on(VersiaEntities.Note, (n) =>
InboxProcessor.processNote(n, instance), InboxProcessor.processNote(n, instance),
) )
.on(VersiaEntities.Follow, (f) => .on(VersiaEntities.Follow, (f) =>
InboxProcessor.processFollowRequest(f, instance), InboxProcessor.processFollowRequest(f),
) )
.on(VersiaEntities.FollowAccept, (f) => .on(VersiaEntities.FollowAccept, (f) =>
InboxProcessor.processFollowAccept(f, instance), InboxProcessor.processFollowAccept(f),
) )
.on(VersiaEntities.FollowReject, (f) => .on(VersiaEntities.FollowReject, (f) =>
InboxProcessor.processFollowReject(f, instance), InboxProcessor.processFollowReject(f),
) )
.on(VersiaEntities.Like, (l) => .on(VersiaEntities.Like, (l) =>
InboxProcessor.processLikeRequest(l, instance), InboxProcessor.processLikeRequest(l),
) )
.on(VersiaEntities.Delete, (d) => .on(VersiaEntities.Delete, (d) =>
InboxProcessor.processDelete(d, instance), InboxProcessor.processDelete(d),
) )
.on(VersiaEntities.User, (u) => .on(VersiaEntities.User, (u) =>
InboxProcessor.processUser(u, instance), InboxProcessor.processUser(u, instance),
) )
.on(VersiaEntities.Share, (s) => .on(VersiaEntities.Share, (s) => InboxProcessor.processShare(s))
InboxProcessor.processShare(s, instance),
)
.on(VersiaEntities.Reaction, (r) => .on(VersiaEntities.Reaction, (r) =>
InboxProcessor.processReaction(r, instance), InboxProcessor.processReaction(r),
) )
.sort(() => { .sort(() => {
throw new ApiError(400, "Unknown entity type"); throw new ApiError(400, "Unknown entity type");
@ -229,10 +227,9 @@ export class InboxProcessor {
*/ */
private static async processReaction( private static async processReaction(
reaction: VersiaEntities.Reaction, reaction: VersiaEntities.Reaction,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(reaction.author, sender); const author = await User.resolve(reaction.author);
const note = await Note.resolve(reaction.object, sender); const note = await Note.resolve(reaction.object);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");
@ -322,10 +319,9 @@ export class InboxProcessor {
*/ */
private static async processFollowRequest( private static async processFollowRequest(
follow: VersiaEntities.Follow, follow: VersiaEntities.Follow,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(follow.author, sender); const author = await User.resolve(follow.author);
const followee = await User.resolve(follow.followee, sender); const followee = await User.resolve(follow.followee);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");
@ -371,10 +367,9 @@ export class InboxProcessor {
*/ */
private static async processFollowAccept( private static async processFollowAccept(
followAccept: VersiaEntities.FollowAccept, followAccept: VersiaEntities.FollowAccept,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(followAccept.author, sender); const author = await User.resolve(followAccept.author);
const follower = await User.resolve(followAccept.follower, sender); const follower = await User.resolve(followAccept.follower);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");
@ -407,10 +402,9 @@ export class InboxProcessor {
*/ */
private static async processFollowReject( private static async processFollowReject(
followReject: VersiaEntities.FollowReject, followReject: VersiaEntities.FollowReject,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(followReject.author, sender); const author = await User.resolve(followReject.author);
const follower = await User.resolve(followReject.follower, sender); const follower = await User.resolve(followReject.follower);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");
@ -443,10 +437,9 @@ export class InboxProcessor {
*/ */
private static async processShare( private static async processShare(
share: VersiaEntities.Share, share: VersiaEntities.Share,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(share.author, sender); const author = await User.resolve(share.author);
const sharedNote = await Note.resolve(share.shared, sender); const sharedNote = await Note.resolve(share.shared);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");
@ -467,11 +460,10 @@ export class InboxProcessor {
*/ // JS doesn't allow the use of `delete` as a variable name */ // JS doesn't allow the use of `delete` as a variable name
public static async processDelete( public static async processDelete(
delete_: VersiaEntities.Delete, delete_: VersiaEntities.Delete,
sender: Instance,
): Promise<void> { ): Promise<void> {
const toDelete = delete_.deleted; const toDelete = delete_.deleted;
const author = await User.resolve(delete_.author, sender); const author = await User.resolve(delete_.author);
switch (delete_.data.deleted_type) { switch (delete_.data.deleted_type) {
case "Note": { case "Note": {
@ -491,7 +483,7 @@ export class InboxProcessor {
return; return;
} }
case "User": { case "User": {
const userToDelete = await User.resolve(toDelete, sender); const userToDelete = await User.resolve(toDelete);
if (!userToDelete) { if (!userToDelete) {
throw new ApiError(404, "User to delete not found"); throw new ApiError(404, "User to delete not found");
@ -586,10 +578,9 @@ export class InboxProcessor {
*/ */
private static async processLikeRequest( private static async processLikeRequest(
like: VersiaEntities.Like, like: VersiaEntities.Like,
sender: Instance,
): Promise<void> { ): Promise<void> {
const author = await User.resolve(like.author, sender); const author = await User.resolve(like.author);
const likedNote = await Note.resolve(like.liked, sender); const likedNote = await Note.resolve(like.liked);
if (!author) { if (!author) {
throw new ApiError(404, "Author not found"); throw new ApiError(404, "Author not found");

View file

@ -62,7 +62,10 @@ export const getDeliveryWorker = (): Worker<
} }
await sender.federateToUser( await sender.federateToUser(
await entityCtor.fromJSON(entity), await entityCtor.fromJSON(
entity,
config.http.base_url.hostname,
),
recipient, recipient,
); );

View file

@ -7,25 +7,37 @@ import type { JSONObject } from "../types.ts";
import { Entity } from "./entity.ts"; import { Entity } from "./entity.ts";
export class Collection extends Entity { export class Collection extends Entity {
public constructor(public override data: z.infer<typeof CollectionSchema>) { public constructor(
super(data); public override data: z.infer<typeof CollectionSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Collection> { public static override fromJSON(
return CollectionSchema.parseAsync(json).then((u) => new Collection(u)); json: JSONObject,
instanceDomain: string,
): Promise<Collection> {
return CollectionSchema.parseAsync(json).then(
(u) => new Collection(u, instanceDomain),
);
} }
} }
export class URICollection extends Entity { export class URICollection extends Entity {
public constructor( public constructor(
public override data: z.infer<typeof URICollectionSchema>, public override data: z.infer<typeof URICollectionSchema>,
instanceDomain: string,
) { ) {
super(data); super(data, instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<URICollection> { public static override fromJSON(
json: JSONObject,
instanceDomain: string,
): Promise<URICollection> {
return URICollectionSchema.parseAsync(json).then( return URICollectionSchema.parseAsync(json).then(
(u) => new URICollection(u), (u) => new URICollection(u, instanceDomain),
); );
} }
} }

View file

@ -6,19 +6,27 @@ import { Entity, Reference } from "./entity.ts";
export class Delete extends Entity { export class Delete extends Entity {
public static override name = "Delete"; public static override name = "Delete";
public constructor(public override data: z.infer<typeof DeleteSchema>) { public constructor(
super(data); public override data: z.infer<typeof DeleteSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get deleted(): Reference { public get deleted(): Reference {
return Reference.fromString(this.data.deleted); return Reference.fromString(this.data.deleted, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Delete> { public static override fromJSON(
return DeleteSchema.parseAsync(json).then((u) => new Delete(u)); json: JSONObject,
instanceDomain: string,
): Promise<Delete> {
return DeleteSchema.parseAsync(json).then(
(u) => new Delete(u, instanceDomain),
);
} }
} }

View file

@ -4,11 +4,19 @@ import type { JSONObject } from "../types.ts";
export class Entity { export class Entity {
public static name = "Entity"; public static name = "Entity";
// biome-ignore lint/suspicious/noExplicitAny: This is a base class that is never instanciated directly public constructor(
public constructor(public data: any) {} // biome-ignore lint/suspicious/noExplicitAny: This is a base class that is never instanciated directly
public data: any,
public instanceDomain: string,
) {}
public static fromJSON(json: JSONObject): Promise<Entity> { public static fromJSON(
return EntitySchema.parseAsync(json).then((u) => new Entity(u)); json: JSONObject,
instanceDomain: string,
): Promise<Entity> {
return EntitySchema.parseAsync(json).then(
(u) => new Entity(u, instanceDomain),
);
} }
public toJSON(): JSONObject { public toJSON(): JSONObject {
@ -19,10 +27,19 @@ export class Entity {
export class Reference { export class Reference {
public constructor( public constructor(
public id: string, public id: string,
public domain?: string, public domain: string,
) {} ) {}
public static fromString(str: string): Reference { /**
* Parses a reference from a string. The string can be in the format "domain:id" or just "id" if the domain is the local instance (in which case a default domain must be provided).
* @param str
* @param defaultDomain
* @returns
*/
public static fromString(
str: string,
defaultDomain: URL | string,
): Reference {
// Expect format: domain:id or id (if domain is the local instance) // Expect format: domain:id or id (if domain is the local instance)
// Handle IPv6 addresses in brackets // Handle IPv6 addresses in brackets
const chunks = str.split(":"); const chunks = str.split(":");
@ -36,10 +53,21 @@ export class Reference {
return new Reference(id, domain); return new Reference(id, domain);
} }
return new Reference(str); if (!defaultDomain) {
throw new Error(
`Invalid reference string: ${str}. Expected format "domain:id" or "id" with a default domain provided.`,
);
}
return new Reference(
str,
defaultDomain instanceof URL
? defaultDomain.hostname
: defaultDomain,
);
} }
public toString(): string { public toString(): string {
return this.domain ? `${this.domain}:${this.id}` : this.id; return `${this.domain}:${this.id}`;
} }
} }

View file

@ -6,39 +6,55 @@ import { Entity, Reference } from "../entity.ts";
export class Like extends Entity { export class Like extends Entity {
public static override name = "pub.versia:likes/Like"; public static override name = "pub.versia:likes/Like";
public constructor(public override data: z.infer<typeof LikeSchema>) { public constructor(
super(data); public override data: z.infer<typeof LikeSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get liked(): Reference { public get liked(): Reference {
return Reference.fromString(this.data.liked); return Reference.fromString(this.data.liked, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Like> { public static override fromJSON(
return LikeSchema.parseAsync(json).then((u) => new Like(u)); json: JSONObject,
instanceDomain: string,
): Promise<Like> {
return LikeSchema.parseAsync(json).then(
(u) => new Like(u, instanceDomain),
);
} }
} }
export class Dislike extends Entity { export class Dislike extends Entity {
public static override name = "pub.versia:likes/Dislike"; public static override name = "pub.versia:likes/Dislike";
public constructor(public override data: z.infer<typeof DislikeSchema>) { public constructor(
super(data); public override data: z.infer<typeof DislikeSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get disliked(): Reference { public get disliked(): Reference {
return Reference.fromString(this.data.disliked); return Reference.fromString(this.data.disliked, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Dislike> { public static override fromJSON(
return DislikeSchema.parseAsync(json).then((u) => new Dislike(u)); json: JSONObject,
instanceDomain: string,
): Promise<Dislike> {
return DislikeSchema.parseAsync(json).then(
(u) => new Dislike(u, instanceDomain),
);
} }
} }

View file

@ -6,19 +6,27 @@ import { Entity, Reference } from "../entity.ts";
export class Vote extends Entity { export class Vote extends Entity {
public static override name = "pub.versia:polls/Vote"; public static override name = "pub.versia:polls/Vote";
public constructor(public override data: z.infer<typeof VoteSchema>) { public constructor(
super(data); public override data: z.infer<typeof VoteSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get poll(): Reference { public get poll(): Reference {
return Reference.fromString(this.data.poll); return Reference.fromString(this.data.poll, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Vote> { public static override fromJSON(
return VoteSchema.parseAsync(json).then((u) => new Vote(u)); json: JSONObject,
instanceDomain: string,
): Promise<Vote> {
return VoteSchema.parseAsync(json).then(
(u) => new Vote(u, instanceDomain),
);
} }
} }

View file

@ -6,19 +6,27 @@ import { Entity, Reference } from "../entity.ts";
export class Reaction extends Entity { export class Reaction extends Entity {
public static override name = "pub.versia:reactions/Reaction"; public static override name = "pub.versia:reactions/Reaction";
public constructor(public override data: z.infer<typeof ReactionSchema>) { public constructor(
super(data); public override data: z.infer<typeof ReactionSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get object(): Reference { public get object(): Reference {
return Reference.fromString(this.data.object); return Reference.fromString(this.data.object, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Reaction> { public static override fromJSON(
return ReactionSchema.parseAsync(json).then((u) => new Reaction(u)); json: JSONObject,
instanceDomain: string,
): Promise<Reaction> {
return ReactionSchema.parseAsync(json).then(
(u) => new Reaction(u, instanceDomain),
);
} }
} }

View file

@ -6,19 +6,31 @@ import { Entity, Reference } from "../entity.ts";
export class Report extends Entity { export class Report extends Entity {
public static override name = "pub.versia:reports/Report"; public static override name = "pub.versia:reports/Report";
public constructor(public override data: z.infer<typeof ReportSchema>) { public constructor(
super(data); public override data: z.infer<typeof ReportSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference | null { public get author(): Reference | null {
return this.data.author ? Reference.fromString(this.data.author) : null; return this.data.author
? Reference.fromString(this.data.author, this.instanceDomain)
: null;
} }
public get reported(): Reference[] { public get reported(): Reference[] {
return this.data.reported.map((r) => Reference.fromString(r)); return this.data.reported.map((r) =>
Reference.fromString(r, this.instanceDomain),
);
} }
public static override fromJSON(json: JSONObject): Promise<Report> { public static override fromJSON(
return ReportSchema.parseAsync(json).then((u) => new Report(u)); json: JSONObject,
instanceDomain: string,
): Promise<Report> {
return ReportSchema.parseAsync(json).then(
(u) => new Report(u, instanceDomain),
);
} }
} }

View file

@ -6,19 +6,27 @@ import { Entity, Reference } from "../entity.ts";
export class Share extends Entity { export class Share extends Entity {
public static override name = "pub.versia:share/Share"; public static override name = "pub.versia:share/Share";
public constructor(public override data: z.infer<typeof ShareSchema>) { public constructor(
super(data); public override data: z.infer<typeof ShareSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get shared(): Reference { public get shared(): Reference {
return Reference.fromString(this.data.shared); return Reference.fromString(this.data.shared, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Share> { public static override fromJSON(
return ShareSchema.parseAsync(json).then((u) => new Share(u)); json: JSONObject,
instanceDomain: string,
): Promise<Share> {
return ShareSchema.parseAsync(json).then(
(u) => new Share(u, instanceDomain),
);
} }
} }

View file

@ -11,20 +11,28 @@ import { Entity, Reference } from "./entity.ts";
export class Follow extends Entity { export class Follow extends Entity {
public static override name = "Follow"; public static override name = "Follow";
public constructor(public override data: z.infer<typeof FollowSchema>) { public constructor(
super(data); public override data: z.infer<typeof FollowSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get followee(): Reference { public get followee(): Reference {
return Reference.fromString(this.data.followee); return Reference.fromString(this.data.followee, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Follow> { public static override fromJSON(
return FollowSchema.parseAsync(json).then((u) => new Follow(u)); json: JSONObject,
instanceDomain: string,
): Promise<Follow> {
return FollowSchema.parseAsync(json).then(
(u) => new Follow(u, instanceDomain),
);
} }
} }
@ -33,21 +41,25 @@ export class FollowAccept extends Entity {
public constructor( public constructor(
public override data: z.infer<typeof FollowAcceptSchema>, public override data: z.infer<typeof FollowAcceptSchema>,
instanceDomain: string,
) { ) {
super(data); super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get follower(): Reference { public get follower(): Reference {
return Reference.fromString(this.data.follower); return Reference.fromString(this.data.follower, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<FollowAccept> { public static override fromJSON(
json: JSONObject,
instanceDomain: string,
): Promise<FollowAccept> {
return FollowAcceptSchema.parseAsync(json).then( return FollowAcceptSchema.parseAsync(json).then(
(u) => new FollowAccept(u), (u) => new FollowAccept(u, instanceDomain),
); );
} }
} }
@ -57,21 +69,25 @@ export class FollowReject extends Entity {
public constructor( public constructor(
public override data: z.infer<typeof FollowRejectSchema>, public override data: z.infer<typeof FollowRejectSchema>,
instanceDomain: string,
) { ) {
super(data); super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get follower(): Reference { public get follower(): Reference {
return Reference.fromString(this.data.follower); return Reference.fromString(this.data.follower, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<FollowReject> { public static override fromJSON(
json: JSONObject,
instanceDomain: string,
): Promise<FollowReject> {
return FollowRejectSchema.parseAsync(json).then( return FollowRejectSchema.parseAsync(json).then(
(u) => new FollowReject(u), (u) => new FollowReject(u, instanceDomain),
); );
} }
} }
@ -79,19 +95,27 @@ export class FollowReject extends Entity {
export class Unfollow extends Entity { export class Unfollow extends Entity {
public static override name = "Unfollow"; public static override name = "Unfollow";
public constructor(public override data: z.infer<typeof UnfollowSchema>) { public constructor(
super(data); public override data: z.infer<typeof UnfollowSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get followee(): Reference { public get followee(): Reference {
return Reference.fromString(this.data.followee); return Reference.fromString(this.data.followee, this.instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Unfollow> { public static override fromJSON(
return UnfollowSchema.parseAsync(json).then((u) => new Unfollow(u)); json: JSONObject,
instanceDomain: string,
): Promise<Unfollow> {
return UnfollowSchema.parseAsync(json).then(
(u) => new Unfollow(u, instanceDomain),
);
} }
} }

View file

@ -10,7 +10,7 @@ export class InstanceMetadata extends Entity {
public constructor( public constructor(
public override data: z.infer<typeof InstanceMetadataSchema>, public override data: z.infer<typeof InstanceMetadataSchema>,
) { ) {
super(data); super(data, data.domain);
} }
public get logo(): ImageContentFormat | undefined { public get logo(): ImageContentFormat | undefined {

View file

@ -7,16 +7,24 @@ import { Entity, Reference } from "./entity.ts";
export class Note extends Entity { export class Note extends Entity {
public static override name = "Note"; public static override name = "Note";
public constructor(public override data: z.infer<typeof NoteSchema>) { public constructor(
super(data); public override data: z.infer<typeof NoteSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<Note> { public static override fromJSON(
return NoteSchema.parseAsync(json).then((n) => new Note(n)); json: JSONObject,
instanceDomain: string,
): Promise<Note> {
return NoteSchema.parseAsync(json).then(
(n) => new Note(n, instanceDomain),
);
} }
public get author(): Reference { public get author(): Reference {
return Reference.fromString(this.data.author); return Reference.fromString(this.data.author, this.instanceDomain);
} }
public get group(): Reference | null { public get group(): Reference | null {
@ -27,20 +35,24 @@ export class Note extends Entity {
return null; return null;
} }
return Reference.fromString(this.data.group); return Reference.fromString(this.data.group, this.instanceDomain);
} }
public get mentions(): Reference[] { public get mentions(): Reference[] {
return this.data.mentions.map((m) => Reference.fromString(m)); return this.data.mentions.map((m) =>
Reference.fromString(m, this.instanceDomain),
);
} }
public get quotes(): Reference | null { public get quotes(): Reference | null {
return this.data.quotes ? Reference.fromString(this.data.quotes) : null; return this.data.quotes
? Reference.fromString(this.data.quotes, this.instanceDomain)
: null;
} }
public get repliesTo(): Reference | null { public get repliesTo(): Reference | null {
return this.data.replies_to return this.data.replies_to
? Reference.fromString(this.data.replies_to) ? Reference.fromString(this.data.replies_to, this.instanceDomain)
: null; : null;
} }

View file

@ -7,12 +7,20 @@ import { Entity } from "./entity.ts";
export class User extends Entity { export class User extends Entity {
public static override name = "User"; public static override name = "User";
public constructor(public override data: z.infer<typeof UserSchema>) { public constructor(
super(data); public override data: z.infer<typeof UserSchema>,
instanceDomain: string,
) {
super(data, instanceDomain);
} }
public static override fromJSON(json: JSONObject): Promise<User> { public static override fromJSON(
return UserSchema.parseAsync(json).then((u) => new User(u)); json: JSONObject,
instanceDomain: string,
): Promise<User> {
return UserSchema.parseAsync(json).then(
(u) => new User(u, instanceDomain),
);
} }
public get avatar(): ImageContentFormat | undefined { public get avatar(): ImageContentFormat | undefined {

View file

@ -75,7 +75,7 @@ export class FederationRequester {
); );
} }
const entity = await entityType.fromJSON(jsonData); const entity = await entityType.fromJSON(jsonData, url.hostname);
return entity as InstanceType<T>; return entity as InstanceType<T>;
} }
@ -150,7 +150,10 @@ export class FederationRequester {
entities.push( entities.push(
...collection.data.items.map( ...collection.data.items.map(
(item) => (item) =>
collectionItemType.fromJSON(item) as InstanceType<T>, collectionItemType.fromJSON(
item,
reference.domain,
) as InstanceType<T>,
), ),
); );
limit -= collection.data.items.length; limit -= collection.data.items.length;

View file

@ -19,7 +19,10 @@ type MaybePromise<T> = T | Promise<T>;
export class EntitySorter { export class EntitySorter {
private readonly handlers: EntitySorterHandlers = new Map(); private readonly handlers: EntitySorterHandlers = new Map();
public constructor(private readonly jsonData: JSONObject) {} public constructor(
private readonly jsonData: JSONObject,
public instanceDomain: string,
) {}
public on<T extends typeof Entity>( public on<T extends typeof Entity>(
entity: T, entity: T,
@ -45,7 +48,7 @@ export class EntitySorter {
if (entity) { if (entity) {
await this.handlers.get(entity)?.( await this.handlers.get(entity)?.(
await entity.fromJSON(this.jsonData), await entity.fromJSON(this.jsonData, this.instanceDomain),
); );
} else { } else {
await defaultHandler?.(); await defaultHandler?.();