chore(federation): 👽 Finish initial Versia Working Draft 4 update

This commit is contained in:
Jesse Wierzbinski 2024-08-26 19:27:40 +02:00
parent c3fa867e74
commit 42e198ca0e
No known key found for this signature in database
14 changed files with 90 additions and 115 deletions

View file

@ -41,9 +41,7 @@ export default class FederationUserFetch extends BaseCommand<
// Check instance exists, if not, create it
await Instance.resolve(`https://${host}`);
const requester = await User.getServerActor();
const manager = await requester.getFederationRequester();
const manager = await User.getFederationRequester();
const uri = await User.webFinger(manager, username, host);

View file

@ -41,9 +41,7 @@ export default class FederationUserFinger extends BaseCommand<
// Check instance exists, if not, create it
await Instance.resolve(`https://${host}`);
const requester = await User.getServerActor();
const manager = await requester.getFederationRequester();
const manager = await User.getFederationRequester();
const uri = await User.webFinger(manager, username, host);

View file

@ -1,4 +1,4 @@
import { emojiValidatorWithColons } from "@/api";
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
import { proxyUrl } from "@/response";
import type { Emoji as ApiEmoji } from "@lysand-org/client/types";
import type { CustomEmojiExtension } from "@versia/federation/types";
@ -191,7 +191,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
public toVersia(): CustomEmojiExtension["emojis"][0] {
return {
name: this.data.shortcode,
name: `:${this.data.shortcode}:`,
url: {
[this.data.contentType]: {
content: this.data.url,
@ -206,8 +206,17 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
emoji: CustomEmojiExtension["emojis"][0],
instanceId: string | null,
): Promise<Emoji> {
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
const shortcode = [
...emoji.name.matchAll(emojiValidatorWithIdentifiers),
][0].groups.shortcode;
if (!shortcode) {
throw new Error("Could not extract shortcode from emoji name");
}
return Emoji.insert({
shortcode: emoji.name,
shortcode,
url: Object.entries(emoji.url)[0][1].content,
alt: Object.entries(emoji.url)[0][1].description || undefined,
contentType: Object.keys(emoji.url)[0],

View file

@ -139,7 +139,7 @@ export class Instance extends BaseInterface<typeof Instances> {
const wellKnownUrl = new URL("/.well-known/versia", origin);
const logger = getLogger("federation");
const requester = await User.getServerActor().getFederationRequester();
const requester = await User.getFederationRequester();
try {
const { ok, raw, data } = await requester
@ -195,7 +195,7 @@ export class Instance extends BaseInterface<typeof Instances> {
// Go to endpoint, then follow the links to the actual metadata
const logger = getLogger("federation");
const requester = await User.getServerActor().getFederationRequester();
const requester = await User.getFederationRequester();
try {
const {

View file

@ -602,8 +602,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
throw new Error(`Invalid URI to parse ${uri}`);
}
const requester =
await User.getServerActor().getFederationRequester();
const requester = await User.getFederationRequester();
const { data } = await requester.get(uri, {
// @ts-expect-error Bun extension
@ -636,7 +635,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
const emojis: Emoji[] = [];
const logger = getLogger("federation");
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
for (const emoji of note.extensions?.["pub.versia:custom_emojis"]
?.emojis ?? []) {
const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch(
(e) => {
@ -948,7 +947,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
// TODO: Refactor as part of groups
group: status.visibility === "public" ? "public" : "followers",
extensions: {
"org.lysand:custom_emojis": {
"pub.versia:custom_emojis": {
emojis: status.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),

View file

@ -135,56 +135,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
);
}
static getServerActor(): User {
return new User({
id: "00000000-0000-0000-0000-000000000000",
username: "actor",
avatar: "",
createdAt: "2024-01-01T00:00:00.000Z",
displayName: "Server Actor",
note: "This is a system actor used for server-to-server communication. It is not a real user.",
updatedAt: "2024-01-01T00:00:00.000Z",
instanceId: null,
publicKey: config.instance.keys.public,
source: {
fields: [],
language: null,
note: "",
privacy: "public",
sensitive: false,
},
fields: [],
isAdmin: false,
isBot: false,
isLocked: false,
isDiscoverable: false,
endpoints: {
dislikes: "",
featured: "",
likes: "",
followers: "",
following: "",
inbox: "",
outbox: "",
},
disableAutomoderation: false,
email: "",
emailVerificationToken: "",
emojis: [],
followerCount: 0,
followingCount: 0,
header: "",
instance: null,
password: "",
passwordResetToken: "",
privateKey: config.instance.keys.private,
roles: [],
sanctions: [],
statusCount: 0,
uri: "/users/actor",
});
}
static getUri(id: string, uri: string | null, baseUrl: string) {
return uri || new URL(`/users/${id}`, baseUrl).toString();
}
@ -433,7 +383,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
uri: string,
instance: Instance,
): Promise<User> {
const requester = await User.getServerActor().getFederationRequester();
const requester = await User.getFederationRequester();
const { data: json } = await requester.get<Partial<VersiaUser>>(uri, {
// @ts-expect-error Bun extension
proxy: config.http.proxy.address,
@ -749,6 +699,20 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return output;
}
/**
* Helper to get the appropriate Versia SDK requester with the instance's private key
*
* @returns The requester
*/
static async getFederationRequester(): Promise<FederationRequester> {
const signatureConstructor = await SignatureConstructor.fromStringKey(
config.instance.keys.private,
config.http.base_url,
);
return new FederationRequester(signatureConstructor);
}
/**
* Helper to get the appropriate Versia SDK requester with this user's private key
*
@ -947,6 +911,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
).toString(),
indexable: false,
username: user.username,
manually_approves_followers: this.data.isLocked,
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
display_name: user.displayName,
@ -960,7 +925,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
algorithm: "ed25519",
},
extensions: {
"org.lysand:custom_emojis": {
"pub.versia:custom_emojis": {
emojis: user.emojis.map((emoji) =>
new Emoji(emoji).toVersia(),
),

View file

@ -76,9 +76,7 @@ export default apiRoute((app) =>
const [username, domain] = accountMatches[0].split("@");
const requester = user ?? User.getServerActor();
const manager = await requester.getFederationRequester();
const manager = await (user ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, domain);

View file

@ -87,9 +87,7 @@ export default apiRoute((app) =>
const accounts: User[] = [];
if (resolve && username && host) {
const requester = self ?? User.getServerActor();
const manager = await requester.getFederationRequester();
const manager = await (self ?? User).getFederationRequester();
const uri = await User.webFinger(manager, username, host);

View file

@ -128,10 +128,9 @@ export default apiRoute((app) =>
}
if (resolve) {
const requester = self ?? User.getServerActor();
const manager =
await requester.getFederationRequester();
const manager = await (
self ?? User
).getFederationRequester();
const uri = await User.webFinger(
manager,

View file

@ -73,6 +73,10 @@ export default apiRoute((app) =>
return context.json({ error: "Object not found" }, 404);
}
if (!foundAuthor) {
return context.json({ error: "Author not found" }, 404);
}
if (foundAuthor?.isRemote()) {
return context.json(
{ error: "Cannot view objects from remote instances" },
@ -92,9 +96,11 @@ export default apiRoute((app) =>
reqUrl.protocol = "https:";
}
const author = foundAuthor ?? User.getServerActor();
const { headers } = await author.sign(apiObject, reqUrl, "GET");
const { headers } = await foundAuthor.sign(
apiObject,
reqUrl,
"GET",
);
return response(objectString, 200, {
"Content-Type": "application/json",

View file

@ -1,8 +1,7 @@
import { apiRoute, applyConfig, handleZodError } from "@/api";
import { redirect, response } from "@/response";
import { redirect } from "@/response";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { config } from "~/packages/config-manager";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -19,7 +18,7 @@ export const meta = applyConfig({
export const schemas = {
param: z.object({
uuid: z.string().uuid().or(z.literal("actor")),
uuid: z.string().uuid(),
}),
};
@ -31,10 +30,7 @@ export default apiRoute((app) =>
async (context) => {
const { uuid } = context.req.valid("param");
const user =
uuid === "actor"
? User.getServerActor()
: await User.fromId(uuid);
const user = await User.fromId(uuid);
if (!user) {
return context.json({ error: "User not found" }, 404);
@ -55,24 +51,15 @@ export default apiRoute((app) =>
return redirect(user.toApi().url);
}
const userString = JSON.stringify(user.toVersia());
const userJson = user.toVersia();
// If base_url uses https and request uses http, rewrite request to use https
// This fixes reverse proxy errors
const reqUrl = new URL(context.req.url);
if (
new URL(config.http.base_url).protocol === "https:" &&
reqUrl.protocol === "http:"
) {
reqUrl.protocol = "https:";
}
const { headers } = await user.sign(
userJson,
context.req.url,
"GET",
);
const { headers } = await user.sign(user.toVersia(), reqUrl, "GET");
return response(userString, 200, {
"Content-Type": "application/json",
...headers.toJSON(),
});
return context.json(userJson, 200, headers.toJSON());
},
),
);

View file

@ -1,5 +1,6 @@
import { apiRoute, applyConfig, handleZodError } from "@/api";
import { zValidator } from "@hono/zod-validator";
import type { Entity } from "@versia/federation/types";
import { and, count, eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { db } from "~/drizzle/db";
@ -79,7 +80,7 @@ export default apiRoute((app) =>
)
)[0].count;
return context.json({
const json = {
first: new URL(
`/users/${uuid}/outbox?page=1`,
config.http.base_url,
@ -90,8 +91,7 @@ export default apiRoute((app) =>
)}`,
config.http.base_url,
).toString(),
total_items: totalNotes,
// Server actor
total: totalNotes,
author: author.getUri(),
next:
notes.length === NOTES_PER_PAGE
@ -99,16 +99,25 @@ export default apiRoute((app) =>
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
config.http.base_url,
).toString()
: undefined,
prev:
: null,
previous:
pageNumber > 1
? new URL(
`/users/${uuid}/outbox?page=${pageNumber - 1}`,
config.http.base_url,
).toString()
: undefined,
: null,
items: notes.map((note) => note.toVersia()),
});
};
const { headers } = await author.sign(
// @ts-expect-error To fix when I add collections to versia-api
json as Entity,
context.req.url,
"GET",
);
return context.json(json, 200, headers.toJSON());
},
),
);

View file

@ -79,9 +79,7 @@ export default apiRoute((app) =>
let activityPubUrl = "";
if (config.federation.bridge.enabled) {
const requester = await User.getServerActor();
const manager = await requester.getFederationRequester();
const manager = await User.getFederationRequester();
try {
activityPubUrl = await manager.webFinger(

View file

@ -9,12 +9,14 @@ import {
anyOf,
caseInsensitive,
charIn,
charNotIn,
createRegExp,
digit,
exactly,
global,
letter,
maybe,
not,
oneOrMore,
} from "magic-regexp";
import { parse } from "qs";
@ -67,17 +69,26 @@ export const idValidator = createRegExp(
export const emojiValidator = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
oneOrMore(letter.or(digit).or(charIn("_-"))),
[caseInsensitive, global],
);
export const emojiValidatorWithColons = createRegExp(
exactly(":"),
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
oneOrMore(letter.or(digit).or(charIn("_-"))),
exactly(":"),
[caseInsensitive, global],
);
export const emojiValidatorWithIdentifiers = createRegExp(
exactly(
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
oneOrMore(letter.or(digit).or(charIn("_-"))).groupedAs("shortcode"),
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
),
[caseInsensitive, global],
);
export const mentionValidator = createRegExp(
exactly("@"),
oneOrMore(anyOf(letter.lowercase, digit, charIn("-"))).groupedAs(