Loading CHANGES.md +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading docs/manual/actor.md +93 −0 Original line number Diff line number Diff line Loading @@ -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. src/federation/callback.ts +19 −0 Original line number Diff line number Diff line Loading @@ -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. * Loading src/federation/federation.ts +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, Loading Loading @@ -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. Loading src/federation/middleware.ts +7 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import { } from "../vocab/vocab.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import type { ActorAliasMapper, ActorDispatcher, ActorHandleMapper, ActorKeyPairsDispatcher, Loading Loading @@ -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; Loading Loading @@ -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, }); Loading Loading @@ -3639,6 +3645,7 @@ interface ActorCallbacks<TContextData> { dispatcher?: ActorDispatcher<TContextData>; keyPairsDispatcher?: ActorKeyPairsDispatcher<TContextData>; handleMapper?: ActorHandleMapper<TContextData>; aliasMapper?: ActorAliasMapper<TContextData>; authorizePredicate?: AuthorizePredicate<TContextData>; } Loading Loading
CHANGES.md +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
docs/manual/actor.md +93 −0 Original line number Diff line number Diff line Loading @@ -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.
src/federation/callback.ts +19 −0 Original line number Diff line number Diff line Loading @@ -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. * Loading
src/federation/federation.ts +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, Loading Loading @@ -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. Loading
src/federation/middleware.ts +7 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import { } from "../vocab/vocab.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import type { ActorAliasMapper, ActorDispatcher, ActorHandleMapper, ActorKeyPairsDispatcher, Loading Loading @@ -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; Loading Loading @@ -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, }); Loading Loading @@ -3639,6 +3645,7 @@ interface ActorCallbacks<TContextData> { dispatcher?: ActorDispatcher<TContextData>; keyPairsDispatcher?: ActorKeyPairsDispatcher<TContextData>; handleMapper?: ActorHandleMapper<TContextData>; aliasMapper?: ActorAliasMapper<TContextData>; authorizePredicate?: AuthorizePredicate<TContextData>; } Loading