mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
chore(federation): 👽 Finish initial Versia Working Draft 4 update
This commit is contained in:
parent
c3fa867e74
commit
42e198ca0e
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
15
utils/api.ts
15
utils/api.ts
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue