Unverified Commit 8587a096 authored by Hong Minhee's avatar Hong Minhee
Browse files
parent 77986094
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -8,6 +8,8 @@ Version 0.11.0

To be released.

 -  Added `Federation.setInboxDispatcher()` method.  [[#71]]

 -  Frequently used JSON-LD contexts are now preloaded.  [[74]]

     -  The `fetchDocumentLoader()` function now preloads the following JSON-LD
@@ -25,6 +27,7 @@ To be released.
 -  Added `Offer` class to Activity Vocabulary API.
    [[#65], [#76] by Lee Dogeon]

[#71]: https://github.com/dahlia/fedify/issues/71
[#74]: https://github.com/dahlia/fedify/issues/74
[#76]: https://github.com/dahlia/fedify/pull/76

+35 −1
Original line number Diff line number Diff line
@@ -73,7 +73,7 @@ federation
        }),
      })
    );
    return { items }
    return { items };
  });
~~~~

@@ -314,6 +314,40 @@ federation
~~~~


Inbox
-----

*This API is available since Fedify 0.11.0.*

The inbox collection is similar to the outbox collection, but it's a collection
of activities that an actor has received.

Cursors and counters for the inbox collection are implemented in the same way as
the outbox collection, so we don't repeat the explanation here.

The below example shows how to construct an inbox collection:

~~~~ typescript
import { Activity } from "@fedify/fedify";

federation
  .setInboxDispatcher("/users/{handle}/inbox", async (ctx, handle) => {
    // Work with the database to find the activities that the actor has received
    // (the following `getInboxByUserHandle` is a hypothetical function):
    const items: Activity[] = await getInboxByUserHandle(handle);
    return { items };
  })
  .setCounter(async (ctx, handle) => {
    // The following `countInboxByUserHandle` is a hypothetical function:
    return await countInboxByUserHandle(handle);
  });
~~~~

> [!NOTE]
> The path for the inbox collection dispatcher must match the path for the inbox
> listeners.


Following
---------

+91 −0
Original line number Diff line number Diff line
@@ -432,6 +432,46 @@ test("Federation.setInboxListeners()", async (t) => {
    );
  });

  await t.step("path match", () => {
    const federation = createFederation<void>({
      kv,
      documentLoader: mockDocumentLoader,
    });
    federation.setInboxDispatcher(
      "/users/{handle}/inbox",
      () => ({ items: [] }),
    );
    assertThrows(
      () => federation.setInboxListeners("/users/{handle}/inbox2"),
      RouterError,
    );
  });

  await t.step("wrong variables in path", () => {
    const federation = createFederation<void>({
      kv,
      documentLoader: mockDocumentLoader,
    });
    assertThrows(
      () =>
        federation.setInboxListeners(
          "/users/inbox" as `${string}{handle}${string}`,
        ),
      RouterError,
    );
    assertThrows(
      () => federation.setInboxListeners("/users/{handle}/inbox/{handle2}"),
      RouterError,
    );
    assertThrows(
      () =>
        federation.setInboxListeners(
          "/users/{handle2}/inbox" as `${string}{handle}${string}`,
        ),
      RouterError,
    );
  });

  await t.step("on()", async () => {
    const authenticatedRequests: [string, string][] = [];
    const federation = createFederation<void>({
@@ -640,3 +680,54 @@ test("Federation.setInboxListeners()", async (t) => {

  mf.uninstall();
});

test("Federation.setInboxDispatcher()", async (t) => {
  const kv = new MemoryKvStore();

  await t.step("path match", () => {
    const federation = createFederation<void>({
      kv,
      documentLoader: mockDocumentLoader,
    });
    federation.setInboxListeners("/users/{handle}/inbox");
    assertThrows(
      () =>
        federation.setInboxDispatcher(
          "/users/{handle}/inbox2",
          () => ({ items: [] }),
        ),
      RouterError,
    );
  });

  await t.step("wrong variables in path", () => {
    const federation = createFederation<void>({
      kv,
      documentLoader: mockDocumentLoader,
    });
    assertThrows(
      () =>
        federation.setInboxDispatcher(
          "/users/inbox" as `${string}{handle}${string}`,
          () => ({ items: [] }),
        ),
      RouterError,
    );
    assertThrows(
      () =>
        federation.setInboxDispatcher(
          "/users/{handle}/inbox/{handle2}",
          () => ({ items: [] }),
        ),
      RouterError,
    );
    assertThrows(
      () =>
        federation.setInboxDispatcher(
          "/users/{handle2}/inbox" as `${string}{handle}${string}`,
          () => ({ items: [] }),
        ),
      RouterError,
    );
  });
});
+92 −12
Original line number Diff line number Diff line
@@ -238,10 +238,12 @@ export class Federation<TContextData> {
    // deno-lint-ignore no-explicit-any
    (new (...args: any[]) => Object) & { typeId: URL }
  >;
  #inboxPath?: string;
  #inboxCallbacks?: CollectionCallbacks<Activity, TContextData, void>;
  #outboxCallbacks?: CollectionCallbacks<Activity, TContextData, void>;
  #followingCallbacks?: CollectionCallbacks<Actor | URL, TContextData, void>;
  #followersCallbacks?: CollectionCallbacks<Recipient, TContextData, URL>;
  #inboxListeners: Map<
  #inboxListeners?: Map<
    new (...args: unknown[]) => Activity,
    InboxListener<TContextData, Activity>
  >;
@@ -281,7 +283,6 @@ export class Federation<TContextData> {
    this.#router = new Router();
    this.#router.add("/.well-known/webfinger", "webfinger");
    this.#router.add("/.well-known/nodeinfo", "nodeInfoJrd");
    this.#inboxListeners = new Map();
    this.#objectCallbacks = {};
    this.#objectTypeIds = {};
    this.#documentLoader = options.documentLoader ?? kvCache({
@@ -857,6 +858,65 @@ export class Federation<TContextData> {
    return setters;
  }

  /**
   * Registers an inbox dispatcher.
   *
   * @param path The URI path pattern for the outbox dispatcher.  The syntax is
   *             based on URI Template
   *             ([RFC 6570](https://tools.ietf.org/html/rfc6570)).  The path
   *             must have one variable: `{handle}`, and must match the inbox
   *             listener path.
   * @param dispatcher An inbox dispatcher callback to register.
   * @throws {@link RouterError} Thrown if the path pattern is invalid.
   * @since 0.11.0
   */
  setInboxDispatcher(
    path: `${string}{handle}${string}`,
    dispatcher: CollectionDispatcher<Activity, TContextData, void>,
  ): CollectionCallbackSetters<TContextData, void> {
    if (this.#inboxCallbacks != null) {
      throw new RouterError("Inbox dispatcher already set.");
    }
    if (this.#router.has("inbox")) {
      if (this.#inboxPath !== path) {
        throw new RouterError(
          "Inbox dispatcher path must match inbox listener path.",
        );
      }
    } else {
      const variables = this.#router.add(path, "inbox");
      if (variables.size !== 1 || !variables.has("handle")) {
        throw new RouterError(
          "Path for inbox dispatcher must have one variable: {handle}",
        );
      }
      this.#inboxPath = path;
    }
    const callbacks: CollectionCallbacks<Activity, TContextData, void> = {
      dispatcher,
    };
    this.#inboxCallbacks = callbacks;
    const setters: CollectionCallbackSetters<TContextData, void> = {
      setCounter(counter: CollectionCounter<TContextData, void>) {
        callbacks.counter = counter;
        return setters;
      },
      setFirstCursor(cursor: CollectionCursor<TContextData, void>) {
        callbacks.firstCursor = cursor;
        return setters;
      },
      setLastCursor(cursor: CollectionCursor<TContextData, void>) {
        callbacks.lastCursor = cursor;
        return setters;
      },
      authorize(predicate: AuthorizePredicate<TContextData>) {
        callbacks.authorizePredicate = predicate;
        return setters;
      },
    };
    return setters;
  }

  /**
   * Registers an outbox dispatcher.
   *
@@ -1045,7 +1105,8 @@ export class Federation<TContextData> {
   * @param inboxPath The URI path pattern for the inbox.  The syntax is based
   *                  on URI Template
   *                  ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
   *                  The path must have one variable: `{handle}`.
   *                  The path must have one variable: `{handle}`, and must
   *                  match the inbox dispatcher path.
   * @param sharedInboxPath An optional URI path pattern for the shared inbox.
   *                        The syntax is based on URI Template
   *                        ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
@@ -1057,15 +1118,23 @@ export class Federation<TContextData> {
    inboxPath: `${string}{handle}${string}`,
    sharedInboxPath?: string,
  ): InboxListenerSetter<TContextData> {
    if (this.#inboxListeners != null) {
      throw new RouterError("Inbox listeners already set.");
    }
    if (this.#router.has("inbox")) {
      throw new RouterError("Inbox already set.");
      if (this.#inboxPath !== inboxPath) {
        throw new RouterError(
          "Inbox listener path must match inbox dispatcher path.",
        );
      }
    } else {
      const variables = this.#router.add(inboxPath, "inbox");
      if (variables.size !== 1 || !variables.has("handle")) {
        throw new RouterError(
          "Path for inbox must have one variable: {handle}",
        );
      }
    }
    if (sharedInboxPath != null) {
      const siVars = this.#router.add(sharedInboxPath, "sharedInbox");
      if (siVars.size !== 0) {
@@ -1074,7 +1143,7 @@ export class Federation<TContextData> {
        );
      }
    }
    const listeners = this.#inboxListeners;
    const listeners = this.#inboxListeners = new Map();
    const setter: InboxListenerSetter<TContextData> = {
      on<TActivity extends Activity>(
        // deno-lint-ignore no-explicit-any
@@ -1319,6 +1388,17 @@ export class Federation<TContextData> {
          onNotAcceptable,
        });
      case "inbox":
        if (request.method !== "POST") {
          return await handleCollection(request, {
            name: "inbox",
            handle: route.values.handle,
            context,
            collectionCallbacks: this.#inboxCallbacks,
            onUnauthorized,
            onNotFound,
            onNotAcceptable,
          });
        }
        context = this.#createContext(
          request,
          contextData,
@@ -1336,7 +1416,7 @@ export class Federation<TContextData> {
          kv: this.#kv,
          kvPrefix: this.#kvPrefixes.activityIdempotence,
          actorDispatcher: this.#actorCallbacks?.dispatcher,
          inboxListeners: this.#inboxListeners,
          inboxListeners: this.#inboxListeners ?? new Map(),
          inboxErrorHandler: this.#inboxErrorHandler,
          onNotFound,
          signatureTimeWindow: this.#signatureTimeWindow,