Unverified Commit b5dd99b7 authored by Hong Minhee's avatar Hong Minhee
Browse files

Shared inbox

parent 83327615
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@
    "fedify",
    "fediverse",
    "preact",
    "unfollowing",
    "uuidv7"
  ]
}
+27 −9
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import {
  Accept,
  Activity,
  Create,
  Endpoints,
  Follow,
  Link,
  Person,
@@ -40,10 +41,20 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => {
    summary: blog.description,
    preferredUsername: handle,
    url: new URL("/", ctx.request.url),
    // A `Context<TContextData>` object has several purposes, and one of
    // them is to provide a way to generate URIs for the dispatchers and
    // the collections:
    outbox: ctx.getOutboxUri(handle),
    inbox: ctx.getInboxUri(handle),
    endpoints: new Endpoints({
      sharedInbox: ctx.getInboxUri(),
    }),
    following: ctx.getFollowingUri(handle),
    followers: ctx.getFollowersUri(handle),
    // The `key` parameter is the public key of the actor, which is used
    // for the HTTP Signatures.  Note that the `key` object is not a
    // `CryptoKey` instance, but a `CryptographicKey` instance which is
    // used for ActivityPub:
    publicKey: key,
  });
})
@@ -105,7 +116,11 @@ federation.setOutboxDispatcher(
    return "";
  });

federation.setInboxListeners("/users/{handle}/inbox")
// Registers the inbox listeners, which are responsible for handling
// incoming activities in the inbox:
federation.setInboxListeners("/users/{handle}/inbox", "/inbox")
  // The `Follow` activity is handled by adding the follower to the
  // follower list:
  .on(Follow, async (ctx, follow) => {
    const blog = await getBlog();
    if (blog == null) return;
@@ -136,20 +151,23 @@ federation.setInboxListeners("/users/{handle}/inbox")
      sharedInbox: recipient.endpoints?.sharedInbox?.href,
      typeName: getActorTypeName(recipient),
    });
    // Note that if a server receives a `Follow` activity, it should reply
    // with either an `Accept` or a `Reject` activity.  In this case, the
    // server automatically accepts the follow request:
    await ctx.sendActivity(
      { handle: blog.handle },
      recipient,
      new Accept({
        actor: actorUri,
        object: follow,
      }),
      new Accept({ actor: actorUri, object: follow }),
    );
  })
  // The `Undo` activity purposes to undo the previous activity.  In this
  // project, we use the `Undo` activity to represent someone unfollowing
  // the blog:
  .on(Undo, async (ctx, undo) => {
    const object = await undo.getObject(ctx);
    if (object instanceof Follow) {
      if (object.id == null) return;
      await removeFollower(object.id.href);
    const activity = await undo.getObject(ctx); // An `Activity` to undo
    if (activity instanceof Follow) {
      if (activity.id == null) return;
      await removeFollower(activity.id.href);
    } else {
      console.debug(undo);
    }
+6 −0
Original line number Diff line number Diff line
@@ -30,6 +30,12 @@ export interface Context<TContextData> {
   */
  getOutboxUri(handle: string): URL;

  /**
   * Builds the URI of the shared inbox.
   * @returns The shared inbox URI.
   */
  getInboxUri(): URL;

  /**
   * Builds the URI of an actor's inbox with the given handle.
   * @param handle The actor's handle.
+2 −2
Original line number Diff line number Diff line
@@ -211,7 +211,7 @@ export async function handleCollection<
}

export interface InboxHandlerParameters<TContextData> {
  handle: string;
  handle: string | null;
  context: RequestContext<TContextData>;
  kv: Deno.Kv;
  kvPrefix: Deno.KvKey;
@@ -242,7 +242,7 @@ export async function handleInbox<TContextData>(
  if (actorDispatcher == null) {
    const response = onNotFound(request);
    return response instanceof Promise ? await response : response;
  } else {
  } else if (handle != null) {
    const key = await context.getActorKey(handle);
    const promise = actorDispatcher(context, handle, key);
    const actor = promise instanceof Promise ? await promise : promise;
+31 −7
Original line number Diff line number Diff line
@@ -201,7 +201,14 @@ export class Federation<TContextData> {
        }
        return new URL(path, url);
      },
      getInboxUri: (handle: string): URL => {
      getInboxUri: (handle?: string): URL => {
        if (handle == null) {
          const path = this.#router.build("sharedInbox", {});
          if (path == null) {
            throw new RouterError("No shared inbox path registered.");
          }
          return new URL(path, url);
        }
        const path = this.#router.build("inbox", { handle });
        if (path == null) {
          throw new RouterError("No inbox path registered.");
@@ -449,22 +456,38 @@ export class Federation<TContextData> {

  /**
   * Assigns the URL patth for the inbox and starts setting inbox listeners.
   * @param path The URI path pattern for the inbox.  The syntax is based on
   *             URI Template ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
   * @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}`.
   * @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)).
   *                        The path must have no variables.
   * @returns An object to register inbox listeners.
   * @throws {RouteError} Thrown if the path pattern is invalid.
   */
  setInboxListeners(path: string): InboxListenerSetter<TContextData> {
  setInboxListeners(
    inboxPath: string,
    sharedInboxPath?: string,
  ): InboxListenerSetter<TContextData> {
    if (this.#router.has("inbox")) {
      throw new RouterError("Inbox already set.");
    }
    const variables = this.#router.add(path, "inbox");
    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) {
        throw new RouterError(
          "Path for shared inbox must have no variables.",
        );
      }
    }
    const listeners = this.#inboxListeners;
    const setter: InboxListenerSetter<TContextData> = {
      on<TActivity extends Activity>(
@@ -571,8 +594,9 @@ export class Federation<TContextData> {
          onNotAcceptable,
        });
      case "inbox":
      case "sharedInbox":
        return await handleInbox(request, {
          handle: route.values.handle,
          handle: route.values.handle ?? null,
          context,
          kv: this.#kv,
          kvPrefix: this.#kvPrefixes.activityIdempotence,