Unverified Commit d579f961 authored by Hong Minhee's avatar Hong Minhee
Browse files
parent 00522be8
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -13,7 +13,10 @@ To be released.
 -  Added `suppressError` option to dereferencing accessors of Activity
    Vocabulary classes.

 -  Added more collection dispatchers.  [[#78]]

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

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

@@ -44,6 +47,7 @@ To be released.
[#71]: https://github.com/dahlia/fedify/issues/71
[#74]: https://github.com/dahlia/fedify/issues/74
[#76]: https://github.com/dahlia/fedify/pull/76
[#78]: https://github.com/dahlia/fedify/issues/78
[#79]: https://github.com/dahlia/fedify/issues/79


+32 −0
Original line number Diff line number Diff line
@@ -469,3 +469,35 @@ federation
> In the above example, we filter the actors in memory, but in the real
> world, you should filter the actors in the database query to improve the
> performance.


Liked
-----

*This API is available since Fedify 0.11.0.*

The liked collection is a collection of objects that an actor has liked.
The liked collection is similar to the outbox collection, but it's a collection
of `Like` activities instead of any activities.

Cursors and counters for the liked 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 a liked collection:

~~~~ typescript
federation
  .setLikedDispatcher("/users/{handle}/liked", async (ctx, handle, cursor) => {
    // Work with the database to find the objects that the actor has liked
    // (the below `getLikedPostsByUserHandle` is a hypothetical function):
    const objects = await getLikedByUserHandle(handle);
    // Turn the posts into `Like` activities:
    const items = posts.map(post =>
      new Like({
        id: new URL(`#post-${post.id}`, ctx.url),
        actor: ctx.getActorUri(handle),
        object: new URL(post.uri),
      })
    );
  });
~~~~
+15 −1
Original line number Diff line number Diff line
@@ -97,6 +97,15 @@ export interface Context<TContextData> {
   */
  getFollowersUri(handle: string): URL;

  /**
   * Builds the URI of an actor's liked collection with the given handle.
   * @param handle The actor's handle.
   * @returns The actor's liked collection URI.
   * @throws {RouterError} If no liked collection is available.
   * @since 0.11.0
   */
  getLikedUri(handle: string): URL;

  /**
   * Determines the type of the URI and extracts the associated data.
   * @param uri The URI to parse.
@@ -304,7 +313,12 @@ export type ParseUriResult =
  /**
   * The case of a followers collection URI.
   */
  | { type: "followers"; handle: string };
  | { type: "followers"; handle: string }
  /**
   * The case of a liked collection URI.
   * @since 0.11.0
   */
  | { type: "liked"; handle: string };

/**
 * Options for {@link Context.sendActivity} method and
+15 −0
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ test("Federation.createContext()", async (t) => {
    assertThrows(() => ctx.getOutboxUri("handle"), RouterError);
    assertThrows(() => ctx.getFollowingUri("handle"), RouterError);
    assertThrows(() => ctx.getFollowersUri("handle"), RouterError);
    assertThrows(() => ctx.getLikedUri("handle"), RouterError);
    assertEquals(ctx.parseUri(new URL("https://example.com/")), null);
    assertEquals(
      ctx.getHandleFromActorUri(new URL("https://example.com/")),
@@ -288,6 +289,20 @@ test("Federation.createContext()", async (t) => {
      ctx.parseUri(new URL("https://example.com/users/handle/followers")),
      { type: "followers", handle: "handle" },
    );

    federation.setLikedDispatcher(
      "/users/{handle}/liked",
      () => ({ items: [] }),
    );
    ctx = federation.createContext(new URL("https://example.com/"), 123);
    assertEquals(
      ctx.getLikedUri("handle"),
      new URL("https://example.com/users/handle/liked"),
    );
    assertEquals(
      ctx.parseUri(new URL("https://example.com/users/handle/liked")),
      { type: "liked", handle: "handle" },
    );
  });

  await t.step("RequestContext", async () => {
+73 −1
Original line number Diff line number Diff line
@@ -14,9 +14,10 @@ import type { Actor, Recipient } from "../vocab/actor.ts";
import {
  Activity,
  CryptographicKey,
  type Like,
  Multikey,
  type Object,
} from "../vocab/mod.ts";
} from "../vocab/vocab.ts";
import { handleWebFinger } from "../webfinger/handler.ts";
import type {
  ActorDispatcher,
@@ -244,6 +245,7 @@ export class Federation<TContextData> {
  #outboxCallbacks?: CollectionCallbacks<Activity, TContextData, void>;
  #followingCallbacks?: CollectionCallbacks<Actor | URL, TContextData, void>;
  #followersCallbacks?: CollectionCallbacks<Recipient, TContextData, URL>;
  #likedCallbacks?: CollectionCallbacks<Like, TContextData, void>;
  #inboxListeners?: Map<
    new (...args: unknown[]) => Activity,
    InboxListener<TContextData, Activity>
@@ -1086,6 +1088,56 @@ export class Federation<TContextData> {
    return setters;
  }

  /**
   * Registers a liked collection dispatcher.
   * @param path The URI path pattern for the liked collection.  The syntax
   *             is based on URI Template
   *             ([RFC 6570](https://tools.ietf.org/html/rfc6570)).  The path
   *             must have one variable: `{handle}`.
   * @param dispatcher A liked collection callback to register.
   * @returns An object with methods to set other liked collection
   *          callbacks.
   * @throws {@link RouterError} Thrown if the path pattern is invalid.
   * @since 0.11.0
   */
  setLikedDispatcher(
    path: `${string}{handle}${string}`,
    dispatcher: CollectionDispatcher<Like, TContextData, void>,
  ): CollectionCallbackSetters<TContextData, void> {
    if (this.#router.has("liked")) {
      throw new RouterError("Liked collection dispatcher already set.");
    }
    const variables = this.#router.add(path, "liked");
    if (variables.size !== 1 || !variables.has("handle")) {
      throw new RouterError(
        "Path for liked collection dispatcher must have one variable: {handle}",
      );
    }
    const callbacks: CollectionCallbacks<Like, TContextData, void> = {
      dispatcher,
    };
    this.#likedCallbacks = 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;
  }

  /**
   * Assigns the URL path for the inbox and starts setting inbox listeners.
   *
@@ -1469,6 +1521,16 @@ export class Federation<TContextData> {
          onNotAcceptable,
        });
      }
      case "liked":
        return await handleCollection(request, {
          name: "liked",
          handle: route.values.handle,
          context,
          collectionCallbacks: this.#likedCallbacks,
          onUnauthorized,
          onNotFound,
          onNotAcceptable,
        });
      default: {
        const response = onNotFound(request);
        return response instanceof Promise ? await response : response;
@@ -1619,6 +1681,14 @@ class ContextImpl<TContextData> implements Context<TContextData> {
    return new URL(path, this.#url);
  }

  getLikedUri(handle: string): URL {
    const path = this.#router.build("liked", { handle });
    if (path == null) {
      throw new RouterError("No liked collection path registered.");
    }
    return new URL(path, this.#url);
  }

  parseUri(uri: URL): ParseUriResult | null {
    if (uri.origin !== this.#url.origin) return null;
    const route = this.#router.route(uri.pathname);
@@ -1643,6 +1713,8 @@ class ContextImpl<TContextData> implements Context<TContextData> {
      return { type: "following", handle: route.values.handle };
    } else if (route.name === "followers") {
      return { type: "followers", handle: route.values.handle };
    } else if (route.name === "liked") {
      return { type: "liked", handle: route.values.handle };
    }
    return null;
  }
Loading