Unverified Commit 41f72464 authored by Hong Minhee's avatar Hong Minhee
Browse files

Actor aliases

parent 56ee718f
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -12,11 +12,17 @@ To be released.
    `traverseCollection()` function, and `Context.traverseCollection()` method
    now suppresses errors occurred JSON-LD processing.

 -  WebFinger responses are now customizable.  [[#3]]

     -  Added `ActorCallbackSetters.mapAlias()` method.
     -  Added `ActorAliasMapper` type.

 -  Added `-t`/`--traverse` option to the `fedify lookup` subcommand.  [[#195]]

 -  Added `-S`/`--suppress-errors` option to the `fedify lookup` subcommand.
    [[#195]]

[#3]: https://github.com/dahlia/fedify/issues/3
[#195]: https://github.com/dahlia/fedify/issues/195


+93 −0
Original line number Diff line number Diff line
@@ -498,3 +498,96 @@ property set to <http://webfinger.net/rel/profile-page>.
The `icon` property is an `Image` object that represents the actor's
icon (i.e., avatar).  It is used as the `links` property of the WebFinger
response, with the `rel` property set to <http://webfinger.net/rel/avatar>.


Actor aliases
-------------

*This API is available since Fedify 1.4.0.*

Sometimes, you may want to give different URLs to the actor URI and its web
profile URL.  It can be easily configured by setting the `url` property of
the `Actor` object returned by the actor dispatcher.  However, if someone
queries the WebFinger for a profile URL, the WebFinger response will not
contain the corresponding actor URI.

To solve this problem, you can set the aliases of the actor by
the `~ActorCallbackSetters.mapAlias()` method.  It takes a callback function
that takes a `Context` object and a queried URL through WebFinger, and returns
the corresponding actor's internal identifier or username, or `null` if there
is no corresponding actor:

~~~~ typescript{15-25} twoslash
// @noErrors: 2339 2345 2391 7006
import { type Federation } from "@fedify/fedify";
const federation = null as unknown as Federation<void>;
interface User { uuid: string; }
/**
 * It's a hypothetical function that finds a user by the UUID.
 * @param uuid The UUID of the user.
 * @returns The user object.
 */
function findUserByUuid(uuid: string): User;
/**
 * It's a hypothetical function that finds a user by the username.
 * @param username The username of the user.
 * @returns The user object.
 */
function findUserByUsername(username: string): User;
// ---cut-before---
federation
  .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
    // Since we map a WebFinger username to the corresponding user's UUID below,
    // the `identifier` parameter is the user's UUID, not the WebFinger
    // username:
    const user = await findUserByUuid(identifier);
    // Omitted for brevity; see the previous example for details.
  })
  .mapHandle(async (ctx, username) => {
    // Work with the database to find the user's UUID by the WebFinger username.
    const user = await findUserByUsername(username);
    if (user == null) return null;  // Return null if the actor is not found.
    return user.uuid;
  })
  .mapAlias((ctx, resource: URL) => {
    // Parse the URL and return the corresponding actor's username if
    // the URL is the profile URL of the actor:
    if (resource.protocol !== "https:") return null;
    if (resource.hostname !== "example.com") return null;
    const m = /^\/@(\w+)$/.exec(resource.pathname);
    if (m == null) return null;
    // Note that it is okay even if the returned username is non-existent.
    // It's dealt with by the `mapHandle()` above:
    return { username: m[1] };
  });
~~~~

By registering the alias mapper, Fedify can respond to WebFinger requests
for the actor's profile URL with the corresponding actor URI.

> [!TIP]
> You also can return the actor's internal identifier instead of the username
> in the `~ActorCallbackSetters.mapAlias()` method:
>
> ~~~~ typescript twoslash
> // @noErrors: 2339 2345 2391 7006
> import { type Federation } from "@fedify/fedify";
> const federation = null as unknown as Federation<void>;
> federation.setActorDispatcher(
>   "/users/{identifier}", async (ctx, identifier) => {}
> )
> // ---cut-before---
> .mapAlias((ctx, resource: URL) => {
>   // Parse the URL and return the corresponding actor's username if
>   // the URL is the profile URL of the actor:
>   if (resource.protocol !== "https:") return null;
>   if (resource.hostname !== "example.com") return null;
>   const userId = resource.searchParams.get("userId");
>   if (userId == null) return null;
>   return { identifier: userId };  // [!code highlight]
> });
> ~~~~

> [!TIP]
> The callback function of the `~ActorCallbackSetters.mapAlias()` method
> can be an async function.
+19 −0
Original line number Diff line number Diff line
@@ -56,6 +56,25 @@ export type ActorHandleMapper<TContextData> = (
  username: string,
) => string | null | Promise<string | null>;

/**
 * A callback that maps a WebFinger query to the corresponding actor's
 * internal identifier or username, or `null` if the query is not found.
 * @typeParam TContextData The context data to pass to the {@link Context}.
 * @param context The request context.
 * @param resource The URL that was queried through WebFinger.
 * @returns The actor's internal identifier or username, or `null` if the query
 *          is not found.
 * @since 1.4.0
 */
export type ActorAliasMapper<TContextData> = (
  context: RequestContext<TContextData>,
  resource: URL,
) =>
  | { identifier: string }
  | { username: string }
  | null
  | Promise<{ identifier: string } | { username: string } | null>;

/**
 * A callback that dispatches an object.
 *
+18 −0
Original line number Diff line number Diff line
import type { Actor, Recipient } from "../vocab/actor.ts";
import type { Activity, Hashtag, Object } from "../vocab/vocab.ts";
import type {
  ActorAliasMapper,
  ActorDispatcher,
  ActorHandleMapper,
  ActorKeyPairsDispatcher,
@@ -510,12 +511,29 @@ export interface ActorCallbackSetters<TContextData> {
   * is assumed to be the same as the WebFinger username, which makes your
   * actors have the immutable handles.  If you want to let your actors change
   * their fediverse handles, you should set this dispatcher.
   * @param mapper A callback that maps a WebFinger username to
   *               the corresponding actor's identifier.
   * @returns The setters object so that settings can be chained.
   * @since 0.15.0
   */
  mapHandle(
    mapper: ActorHandleMapper<TContextData>,
  ): ActorCallbackSetters<TContextData>;

  /**
   * Sets the callback function that maps a WebFinger query to the corresponding
   * actor's identifier or username.  If it's omitted, the WebFinger handler
   * only supports the actor URIs and `acct:` URIs.  If you want to support
   * other queries, you should set this dispatcher.
   * @param mapper A callback that maps a WebFinger query to the corresponding
   *               actor's identifier or username.
   * @returns The setters object so that settings can be chained.
   * @since 1.4.0
   */
  mapAlias(
    mapper: ActorAliasMapper<TContextData>,
  ): ActorCallbackSetters<TContextData>;

  /**
   * Specifies the conditions under which requests are authorized.
   * @param predicate A callback that returns whether a request is authorized.
+7 −0
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import {
} from "../vocab/vocab.ts";
import { handleWebFinger } from "../webfinger/handler.ts";
import type {
  ActorAliasMapper,
  ActorDispatcher,
  ActorHandleMapper,
  ActorKeyPairsDispatcher,
@@ -1190,6 +1191,10 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
        callbacks.handleMapper = mapper;
        return setters;
      },
      mapAlias(mapper: ActorAliasMapper<TContextData>) {
        callbacks.aliasMapper = mapper;
        return setters;
      },
      authorize(predicate: AuthorizePredicate<TContextData>) {
        callbacks.authorizePredicate = predicate;
        return setters;
@@ -2206,6 +2211,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
          context,
          actorDispatcher: this.actorCallbacks?.dispatcher,
          actorHandleMapper: this.actorCallbacks?.handleMapper,
          actorAliasMapper: this.actorCallbacks?.aliasMapper,
          onNotFound,
          tracer,
        });
@@ -3639,6 +3645,7 @@ interface ActorCallbacks<TContextData> {
  dispatcher?: ActorDispatcher<TContextData>;
  keyPairsDispatcher?: ActorKeyPairsDispatcher<TContextData>;
  handleMapper?: ActorHandleMapper<TContextData>;
  aliasMapper?: ActorAliasMapper<TContextData>;
  authorizePredicate?: AuthorizePredicate<TContextData>;
}

Loading