Unverified Commit 6bbdcb1d authored by Hong Minhee's avatar Hong Minhee
Browse files

`Context.routeActivity()` method

parent 7082badd
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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.
@@ -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
+66 −0
Original line number Diff line number Diff line
@@ -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("@hongminhee@fosstodon.org");
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 −0
Original line number Diff line number Diff line
@@ -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.         |
+56 −0
Original line number Diff line number Diff line
@@ -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>;
}

/**
@@ -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
+1 −1
Original line number Diff line number Diff line
@@ -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",
Loading