diff --git a/cli/commands/federation/user/fetch.ts b/cli/commands/federation/user/fetch.ts index 89037db1..a2356bda 100644 --- a/cli/commands/federation/user/fetch.ts +++ b/cli/commands/federation/user/fetch.ts @@ -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); diff --git a/cli/commands/federation/user/finger.ts b/cli/commands/federation/user/finger.ts index 0595a89a..bddff4b6 100644 --- a/cli/commands/federation/user/finger.ts +++ b/cli/commands/federation/user/finger.ts @@ -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); diff --git a/packages/database-interface/emoji.ts b/packages/database-interface/emoji.ts index 0bec1a75..5324679b 100644 --- a/packages/database-interface/emoji.ts +++ b/packages/database-interface/emoji.ts @@ -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 { 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 { emoji: CustomEmojiExtension["emojis"][0], instanceId: string | null, ): Promise { + // 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], diff --git a/packages/database-interface/instance.ts b/packages/database-interface/instance.ts index 2f9e754a..1f43d66f 100644 --- a/packages/database-interface/instance.ts +++ b/packages/database-interface/instance.ts @@ -139,7 +139,7 @@ export class Instance extends BaseInterface { 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 { // 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 { diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 920bbdc4..1feb7ee8 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -602,8 +602,7 @@ export class Note extends BaseInterface { 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 { 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 { // 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(), ), diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 1d515a38..7210a73a 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -135,56 +135,6 @@ export class User extends BaseInterface { ); } - 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 { uri: string, instance: Instance, ): Promise { - const requester = await User.getServerActor().getFederationRequester(); + const requester = await User.getFederationRequester(); const { data: json } = await requester.get>(uri, { // @ts-expect-error Bun extension proxy: config.http.proxy.address, @@ -749,6 +699,20 @@ export class User extends BaseInterface { return output; } + /** + * Helper to get the appropriate Versia SDK requester with the instance's private key + * + * @returns The requester + */ + static async getFederationRequester(): Promise { + 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 { ).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 { algorithm: "ed25519", }, extensions: { - "org.lysand:custom_emojis": { + "pub.versia:custom_emojis": { emojis: user.emojis.map((emoji) => new Emoji(emoji).toVersia(), ), diff --git a/server/api/api/v1/accounts/lookup/index.ts b/server/api/api/v1/accounts/lookup/index.ts index 464d22e8..c8c68b9e 100644 --- a/server/api/api/v1/accounts/lookup/index.ts +++ b/server/api/api/v1/accounts/lookup/index.ts @@ -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); diff --git a/server/api/api/v1/accounts/search/index.ts b/server/api/api/v1/accounts/search/index.ts index 6bb54fb9..9e23edd0 100644 --- a/server/api/api/v1/accounts/search/index.ts +++ b/server/api/api/v1/accounts/search/index.ts @@ -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); diff --git a/server/api/api/v2/search/index.ts b/server/api/api/v2/search/index.ts index b59138ba..e095b542 100644 --- a/server/api/api/v2/search/index.ts +++ b/server/api/api/v2/search/index.ts @@ -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, diff --git a/server/api/objects/:id/index.ts b/server/api/objects/:id/index.ts index 011c2853..a65306d2 100644 --- a/server/api/objects/:id/index.ts +++ b/server/api/objects/:id/index.ts @@ -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", diff --git a/server/api/users/:uuid/index.ts b/server/api/users/:uuid/index.ts index 467a0943..f6b1c711 100644 --- a/server/api/users/:uuid/index.ts +++ b/server/api/users/:uuid/index.ts @@ -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()); }, ), ); diff --git a/server/api/users/:uuid/outbox/index.ts b/server/api/users/:uuid/outbox/index.ts index 05e675ac..22c40c20 100644 --- a/server/api/users/:uuid/outbox/index.ts +++ b/server/api/users/:uuid/outbox/index.ts @@ -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()); }, ), ); diff --git a/server/api/well-known/webfinger/index.ts b/server/api/well-known/webfinger/index.ts index 013d4d92..28ee31ca 100644 --- a/server/api/well-known/webfinger/index.ts +++ b/server/api/well-known/webfinger/index.ts @@ -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( diff --git a/utils/api.ts b/utils/api.ts index 5db6f027..a8ac6326 100644 --- a/utils/api.ts +++ b/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(