Skip to content

Commit

Permalink
Context.routeActivity() method
Browse files Browse the repository at this point in the history
Close #193
  • Loading branch information
dahlia committed Nov 29, 2024
1 parent 7082bad commit 6bbdcb1
Show file tree
Hide file tree
Showing 13 changed files with 729 additions and 160 deletions.
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ To be released.
- `Context.sendActivity()` and `InboxContext.forwardActivity()` methods now
reject when they fail to enqueue the task. [[#192]]

- Fedify now allows you to manually route an `Activity` to the corresponding
inbox listener. [[#193]]

- Added `Context.routeActivity()` method.
- Added `RouteActivityOptions` interface.

- `Object.toJsonLd()` without any `format` option now returns its original
JSON-LD object even if it not created from `Object.fromJsonLd()` but it is
returned from another `Object`'s `get*()` method.
Expand Down Expand Up @@ -111,6 +117,7 @@ To be released.
[#183]: https://github.com/dahlia/fedify/pull/183
[#186]: https://github.com/dahlia/fedify/pull/186
[#192]: https://github.com/dahlia/fedify/issues/192
[#193]: https://github.com/dahlia/fedify/issues/193


Version 1.2.8
Expand Down
66 changes: 66 additions & 0 deletions docs/manual/inbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,69 @@ const ctx = null as unknown as Context<void>;
// ---cut-before---
ctx.getInboxUri()
~~~~


Manual routing
--------------

*This API is available since Fedify 1.3.0.*

If you want to manually route an activity to the appropriate inbox listener
with no actual HTTP request, you can use the `Context.routeActivity()` method.
The method takes an identifier of the recipient (or `null` for the shared inbox)
and an `Activity` object to route. The point of this method is that it verifies
if the `Activity` object is made by the its actor, and unless it is, the method
silently ignores the activity.

The following code shows how to route an `Activity` object enclosed in
top-level `Announce` object to the corresponding inbox listener:

~~~~ typescript twoslash
import { Activity, Announce, type Federation } from "@fedify/fedify";

const federation = null as unknown as Federation<void>;

federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
// ---cut-before---
.on(Announce, async (ctx, announce) => {
// Get an object enclosed in the `Announce` object:
const object = await announce.getObject();
if (object instanceof Activity) {
// Route the activity to the appropriate inbox listener (shared inbox):
await ctx.routeActivity(ctx.recipient, object);
}
})
~~~~

As another example, the following code shows how to invoke the corresponding
inbox listeners for a remote actor's activities:

~~~~ typescript twoslash
import { Activity, type Context, isActor } from "@fedify/fedify";

async function main(context: Context<void>) {
// ---cut-before---
const actor = await context.lookupObject("@[email protected]");
if (!isActor(actor)) return;
const collection = await actor.getOutbox();
if (collection == null) return;
for await (const item of context.traverseCollection(collection)) {
if (item instanceof Activity) {
await context.routeActivity(null, item);
}
}
// ---cut-after---
}
~~~~

> [!TIP]
> The `Context.routeActivity()` method trusts the `Activity` object only if
> one of the following conditions is met:
>
> - The `Activity` has its Object Integrity Proofs and the proofs are signed
> by its actor.
>
> - The `Activity` is dereferenceable by its `~Object.id` and
> the dereferenced object has an actor that belongs to the same origin
> as the `Activity` object.
1 change: 1 addition & 0 deletions docs/manual/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ spans:
| `activitypub.fetch_key` | Client | Fetches the public keys for the actor. |
| `activitypub.get_actor_handle` | Client | Resolves the actor handle. |
| `activitypub.inbox` | Consumer | Dequeues the ActivityPub activity to receive. |
| `activitypub.inbox` | Internal | Manually routes the ActivityPub activity. |
| `activitypub.inbox` | Producer | Enqueues the ActivityPub activity to receive. |
| `activitypub.inbox` | Server | Receives the ActivityPub activity. |
| `activitypub.lookup_object` | Client | Looks up the Activity Streams object. |
Expand Down
56 changes: 56 additions & 0 deletions src/federation/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,32 @@ export interface Context<TContextData> {
activity: Activity,
options?: SendActivityOptions,
): Promise<void>;

/**
* Manually routes an activity to the appropriate inbox listener.
*
* It is useful for routing an activity that is not received from the network,
* or for routing an activity that is enclosed in another activity.
*
* Note that the activity will be verified if it has Object Integrity Proofs
* or is equivalent to the actual remote object. If the activity is not
* verified, it will be rejected.
* @param recipient The recipient of the activity. If it is `null`,
* the activity will be routed to the shared inbox.
* Otherwise, the activity will be routed to the personal
* inbox of the recipient with the given identifier.
* @param activity The activity to route. It must have a proof or
* a dereferenceable `id` to verify the activity.
* @param options Options for routing the activity.
* @returns `true` if the activity is successfully verified and routed.
* Otherwise, `false`.
* @since 1.3.0
*/
routeActivity(
recipient: string | null,
activity: Activity,
options?: RouteActivityOptions,
): Promise<boolean>;
}

/**
Expand Down Expand Up @@ -579,6 +605,36 @@ export interface ForwardActivityOptions extends SendActivityOptions {
skipIfUnsigned: boolean;
}

/**
* Options for {@link Context.routeActivity} method.
* @since 1.3.0
*/
export interface RouteActivityOptions {
/**
* Whether to skip enqueuing the activity and invoke the listener immediately.
* If no inbox queue is available, this option is ignored and the activity
* will be always invoked immediately.
* @default false
*/
immediate?: boolean;

/**
* The document loader for loading remote JSON-LD documents.
*/
documentLoader?: DocumentLoader;

/**
* The context loader for loading remote JSON-LD contexts.
*/
contextLoader?: DocumentLoader;

/**
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
* is used.
*/
tracerProvider?: TracerProvider;
}

/**
* A pair of a public key and a private key in various formats.
* @since 0.10.0
Expand Down
2 changes: 1 addition & 1 deletion src/federation/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ test("handleInbox()", async () => {
...inboxOptions,
});
assertEquals(onNotFoundCalled, null);
assertEquals(response.status, 202);
assertEquals([response.status, await response.text()], [202, ""]);

response = await handleInbox(signedRequest, {
recipient: "someone",
Expand Down
Loading

0 comments on commit 6bbdcb1

Please sign in to comment.