From 12b6044cfeebaedc5f6113f267bfe2e024728fa5 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 22 Apr 2024 10:36:52 +0900 Subject: [PATCH] RequestContext.getActor() method --- CHANGES.md | 2 ++ docs/manual/context.md | 24 ++++++++++++++++++++++++ federation/context.ts | 9 +++++++++ federation/middleware.test.ts | 20 +++++++++++++++++++- federation/middleware.ts | 24 ++++++++++++++++++++++++ testing/context.ts | 1 + 6 files changed, 79 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4b7a5ee3..779e3bbd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ To be released. - `["fedify", "federation", "inbox"]` - `["fedify", "federation", "outbox"]` + - Added `RequestContext.getActor()` method. + [public addressing]: https://www.w3.org/TR/activitypub/#public-addressing [authorized fetch]: https://swicg.github.io/activitypub-http-signature/#authorized-fetch [LogTape]: https://github.com/dahlia/logtape diff --git a/docs/manual/context.md b/docs/manual/context.md index 03f4075b..e21ee5d1 100644 --- a/docs/manual/context.md +++ b/docs/manual/context.md @@ -22,6 +22,7 @@ The key features of the `Context` object are as follows: - Carrying [`TContextData`](./federation.md#tcontextdata) - Building the object URIs (e.g., actor URIs, shared inbox URI) + - Building Activity Vocabulary objects - Getting the current HTTP request - Enqueuing an outgoing activity - Getting a `DocumentLoader` @@ -138,6 +139,29 @@ section](./send.md). [key pair dispatcher]: ./actor.md#public-key-of-an-actor +Building `Actor` objects +------------------------ + +The `RequestContext` object has a method to build an `Actor` object from +the handle. The following shows an example of using +the `RequestContext.getActor()` method: + +~~~~ typescript +const ctx = federation.createContext(request, undefined); +const actor = await ctx.getActor(handle); // [!code highlight] +await ctx.sendActivity( + { handle }, + followers, + new Update({ actor: actor.id, object: actor }), +); +~~~~ + +> [!NOTE] +> The `RequestContext.getActor()` method is only available when the actor +> dispatcher is registered to the `Federation` object. If the actor dispatcher +> is not registered, the `RequestContext.getActor()` method throws an error. + + Getting a `DocumentLoader` -------------------------- diff --git a/federation/context.ts b/federation/context.ts index 37cd3460..161475be 100644 --- a/federation/context.ts +++ b/federation/context.ts @@ -142,6 +142,15 @@ export interface RequestContext extends Context { */ readonly url: URL; + /** + * Gets an {@link Actor} object for the given handle. + * @param handle The actor's handle. + * @returns The actor object, or `null` if the actor is not found. + * @throws {Error} If no actor dispatcher is available. + * @since 0.7.0 + */ + getActor(handle: string): Promise; + /** * Gets the public key of the sender, if any exists and it is verified. * Otherwise, `null` is returned. diff --git a/federation/middleware.test.ts b/federation/middleware.test.ts index 48461393..d392b350 100644 --- a/federation/middleware.test.ts +++ b/federation/middleware.test.ts @@ -1,3 +1,4 @@ +import { lookupObject } from "@fedify/fedify/vocab"; import { Temporal } from "@js-temporal/polyfill"; import { assertEquals, @@ -24,7 +25,6 @@ import type { Context } from "./context.ts"; import { MemoryKvStore } from "./kv.ts"; import { Federation } from "./middleware.ts"; import { RouterError } from "./router.ts"; -import { lookupObject } from "@fedify/fedify/vocab"; Deno.test("Federation.createContext()", async (t) => { const kv = new MemoryKvStore(); @@ -189,6 +189,11 @@ Deno.test("Federation.createContext()", async (t) => { // Multiple calls should return the same result: assertEquals(await ctx.getSignedKey(), null); assertEquals(await ctx.getSignedKeyOwner(), null); + assertRejects( + () => ctx.getActor("someone"), + Error, + "No actor dispatcher registered", + ); const signedReq = await sign( new Request("https://example.com/"), @@ -223,6 +228,19 @@ Deno.test("Federation.createContext()", async (t) => { // Multiple calls should return the same result: assertEquals(await signedCtx2.getSignedKey(), publicKey3); assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner); + + federation.setActorDispatcher( + "/users/{handle}", + (_ctx, handle) => new Person({ preferredUsername: handle }), + ); + const ctx2 = federation.createContext(req, 789); + assertEquals(ctx2.request, req); + assertEquals(ctx2.url, new URL("https://example.com/")); + assertEquals(ctx2.data, 789); + assertEquals( + await ctx2.getActor("john"), + new Person({ preferredUsername: "john" }), + ); }); mf.uninstall(); diff --git a/federation/middleware.ts b/federation/middleware.ts index 3717b6c2..a179f1fd 100644 --- a/federation/middleware.ts +++ b/federation/middleware.ts @@ -433,6 +433,30 @@ export class Federation { ...context, request, url, + getActor: async (handle: string) => { + if ( + this.#actorCallbacks == null || + this.#actorCallbacks.dispatcher == null + ) { + throw new Error("No actor dispatcher registered."); + } + return this.#actorCallbacks.dispatcher( + { + ...reqCtx, + getActor(handle2: string) { + getLogger(["fedify", "federation"]).warn( + "RequestContext.getActor({getActorHandle}) is invoked from " + + "the actor dispatcher ({actorDispatcherHandle}); " + + "this may cause an infinite loop.", + { getActorHandle: handle2, actorDispatcherHandle: handle }, + ); + return reqCtx.getActor(handle2); + }, + }, + handle, + await context.getActorKey(handle), + ); + }, async getSignedKey() { if (signedKey !== undefined) return signedKey; return signedKey = await verify(request, context.documentLoader); diff --git a/testing/context.ts b/testing/context.ts index f24436c0..7ab92e0e 100644 --- a/testing/context.ts +++ b/testing/context.ts @@ -55,6 +55,7 @@ export function createRequestContext( ...createContext(args), request: args.request ?? new Request(args.url), url: args.url, + getActor: args.getActor ?? (() => Promise.resolve(null)), getSignedKey: args.getSignedKey ?? (() => Promise.resolve(null)), getSignedKeyOwner: args.getSignedKeyOwner ?? (() => Promise.resolve(null)), };