Skip to content

Commit

Permalink
RequestContext.getActor() method
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Apr 22, 2024
1 parent e571d4c commit 12b6044
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions docs/manual/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`
--------------------------

Expand Down
9 changes: 9 additions & 0 deletions federation/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ export interface RequestContext<TContextData> extends Context<TContextData> {
*/
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<Actor | null>;

/**
* Gets the public key of the sender, if any exists and it is verified.
* Otherwise, `null` is returned.
Expand Down
20 changes: 19 additions & 1 deletion federation/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lookupObject } from "@fedify/fedify/vocab";
import { Temporal } from "@js-temporal/polyfill";
import {
assertEquals,
Expand All @@ -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();
Expand Down Expand Up @@ -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/"),
Expand Down Expand Up @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions federation/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,30 @@ export class Federation<TContextData> {
...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);
Expand Down
1 change: 1 addition & 0 deletions testing/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function createRequestContext<TContextData>(
...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)),
};
Expand Down

0 comments on commit 12b6044

Please sign in to comment.