Skip to content

Commit

Permalink
Object dispatcher
Browse files Browse the repository at this point in the history
Close #33
  • Loading branch information
dahlia committed Apr 22, 2024
1 parent 1d26571 commit ab5891c
Show file tree
Hide file tree
Showing 14 changed files with 744 additions and 25 deletions.
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ To be released.
a path as <code>`${string}{handle}${string}`</code> instead of `string`
so that it is more type-safe.

- Added generalized object dispatchers. [[#33]]

- Added `Federation.setObjectDispatcher()` method.
- Added `ObjectDispatcher` type.
- Added `ObjectAuthorizePredicate` type.
- Added `Context.getObjectUri()` method.
- Added `RequestContext.getObject()` method.

[public addressing]: https://www.w3.org/TR/activitypub/#public-addressing
[authorized fetch]: https://swicg.github.io/activitypub-http-signature/#authorized-fetch
[LogTape]: https://github.com/dahlia/logtape
[#33]: https://github.com/dahlia/fedify/issues/33


Version 0.6.1
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default defineConfig({
{ text: "Inbox listeners", link: "/manual/inbox.md" },
{ text: "Sending activities", link: "/manual/send.md" },
{ text: "Collections", link: "/manual/collections.md" },
{ text: "Object dispatcher", link: "/manual/object.md" },
{ text: "Access control", link: "/manual/access-control.md" },
{ text: "NodeInfo", link: "/manual/nodeinfo.md" },
{ text: "Pragmatics", link: "/manual/pragmatics.md" },
Expand Down
1 change: 1 addition & 0 deletions docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ framework.
- [Inbox listeners](./manual/inbox.md)
- [Send activities](./manual/send.md)
- [Collections](./manual/collections.md)
- [Object](./manual/object.md)
- [Access control](./manual/access-control.md)
- [NodeInfo](./manual/nodeinfo.md)
- [Pragmatics](./manual/pragmatics.md)
Expand Down
7 changes: 4 additions & 3 deletions docs/manual/access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ description: >-
who can access your resources. This section explains how to use the access
control system.
prev:
text: Collections
link: ./collections.md
text: Object dispatcher
link: ./object.md
next:
text: NodeInfo
link: ./nodeinfo.md
Expand Down Expand Up @@ -35,7 +35,8 @@ Enabling authorized fetch

To enable authorized fetch, you need to register an `AuthorizePredicate`
callback with `ActorCallbackSetters.authorize()` or
`CollectionCallbackSetters.authorize()`. The below example shows how to enable
`CollectionCallbackSetters.authorize()`, or `ObjectAuthorizePredicate` callback
with `ObjectCallbackSetters.authorize()`. The below example shows how to enable
authorized fetch for the actor dispatcher:

~~~~ typescript{8-10}
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ prev:
text: Sending activities
link: ./send.md
next:
text: Access control
link: ./access-control.md
text: Object dispatcher
link: ./object.md
---

Collections
Expand Down
20 changes: 15 additions & 5 deletions docs/manual/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The key features of the `Context` object are as follows:

- Carrying [`TContextData`](./federation.md#tcontextdata)
- Building the object URIs (e.g., actor URIs, shared inbox URI)
- Building Activity Vocabulary objects
- Dispatching Activity Vocabulary objects
- Getting the current HTTP request
- Enqueuing an outgoing activity
- Getting a `DocumentLoader`
Expand Down Expand Up @@ -139,11 +139,13 @@ section](./send.md).
[key pair dispatcher]: ./actor.md#public-key-of-an-actor


Building `Actor` objects
------------------------
Dispatching objects
-------------------

*This API is available since Fedify 0.7.0.*

The `RequestContext` object has a method to build an `Actor` object from
the handle. The following shows an example of using
The `RequestContext` object has a method to dispatch an Activity Vocabulary
object from the URL arguments. The following shows an example of using
the `RequestContext.getActor()` method:

~~~~ typescript
Expand All @@ -161,6 +163,14 @@ await ctx.sendActivity(
> dispatcher is registered to the `Federation` object. If the actor dispatcher
> is not registered, the `RequestContext.getActor()` method throws an error.
In the same way, you can use the `RequestContext.getObject()` method to dispatch
an object from the URL arguments. The following shows an example:

~~~~ typescript
const ctx = federation.createContext(request, undefined);
const note = await ctx.getObject(Note, { handle, id }); // [!code highlight]
~~~~


Getting a `DocumentLoader`
--------------------------
Expand Down
59 changes: 59 additions & 0 deletions docs/manual/object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
description: >-
You can register an object dispatcher so that Fedify can dispatch an
appropriate object by its class and URL arguments. This section explains
how to register an object dispatcher.
prev:
text: Collections
link: ./collections.md
next:
text: Access control
link: ./access-control.md
---

Object dispatcher
=================

*This API is available since Fedify 0.7.0.*

In ActivityPub, [objects] are entities that can be attached to activities or
other objects. Objects sometimes need to be resolved by their dereferenceable
URIs. To let objects be resolved, you can register object dispatchers so that
Fedify can dispatch an appropriate object by its class and URL arguments.

An object dispatcher is a callback function that takes a `Context` object and
URL arguments, and returns an object. Every object dispatcher has one or more
URL parameters that are used to dispatch the object. The URL parameters are
specified in the path pattern of the object dispatcher, e.g., `/notes/{id}`,
`/users/{handle}/articles/{id}`.

The below example shows how to register an object dispatcher:

~~~~ typescript {7-19}
import { Federation, Note } from "@fedify/fedify";

const federation = new Federation({
// Omitted for brevity; see the related section for details.
});

federation.setObjectDispatcher(
Note,
"/users/{handle}/notes/{id}",
async (ctx, { handle, id }) => {
// Work with the database to find the note by the author's handle and the note ID.
if (note == null) return null; // Return null if the note is not found.
return new Note({
id: ctx.getObjectUri(Note, { handle, id }),
content: note.content,
// Many more properties...
});
}
);
~~~~

In the above example, the `~Federation.setObjectDispatcher()` method registers
an object dispatcher for the `Note` class and the `/users/{handle}/notes/{id}`
path. This pattern syntax follows the [URI Template] specification.

[objects]: https://www.w3.org/TR/activitystreams-core/#object
[URI Template]: https://datatracker.ietf.org/doc/html/rfc6570
41 changes: 41 additions & 0 deletions federation/callback.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NodeInfo } from "../nodeinfo/types.ts";
import type { Actor } from "../vocab/actor.ts";
import type { Activity, CryptographicKey } from "../vocab/mod.ts";
import type { Object } from "../vocab/vocab.ts";
import type { PageItems } from "./collection.ts";
import type { RequestContext } from "./context.ts";

Expand Down Expand Up @@ -34,6 +35,23 @@ export type ActorKeyPairDispatcher<TContextData> = (
handle: string,
) => CryptoKeyPair | null | Promise<CryptoKeyPair | null>;

/**
* A callback that dispatches an object.
*
* @typeParam TContextData The context data to pass to the {@link Context}.
* @typeParam TObject The type of object to dispatch.
* @typeParam TParam The parameter names of the requested URL.
* @since 0.7.0
*/
export type ObjectDispatcher<
TContextData,
TObject extends Object,
TParam extends string,
> = (
context: RequestContext<TContextData>,
values: Record<TParam, string>,
) => TObject | null | Promise<TObject | null>;

/**
* A callback that dispatches a collection.
*
Expand Down Expand Up @@ -120,3 +138,26 @@ export type AuthorizePredicate<TContextData> = (
signedKey: CryptographicKey | null,
signedKeyOwner: Actor | null,
) => boolean | Promise<boolean>;

/**
* A callback that determines if a request is authorized or not.
*
* @typeParam TContextData The context data to pass to the {@link Context}.
* @typeParam TParam The parameter names of the requested URL.
* @param context The request context.
* @param values The parameters of the requested URL.
* @param signedKey The key that was used to sign the request, or `null` if
* the request was not signed or the signature was invalid.
* @param signedKeyOwner The actor that owns the key that was used to sign the
* request, or `null` if the request was not signed or the
* signature was invalid, or if the key is not associated
* with an actor.
* @returns `true` if the request is authorized, `false` otherwise.
* @since 0.7.0
*/
export type ObjectAuthorizePredicate<TContextData, TParam extends string> = (
context: RequestContext<TContextData>,
values: Record<TParam, string>,
signedKey: CryptographicKey | null,
signedKeyOwner: Actor | null,
) => boolean | Promise<boolean>;
33 changes: 32 additions & 1 deletion federation/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DocumentLoader } from "../runtime/docloader.ts";
import type { Actor } from "../vocab/actor.ts";
import type { Activity, CryptographicKey } from "../vocab/mod.ts";
import type { Activity, CryptographicKey, Object } from "../vocab/mod.ts";

/**
* A context.
Expand Down Expand Up @@ -32,6 +32,21 @@ export interface Context<TContextData> {
*/
getActorUri(handle: string): URL;

/**
* Builds the URI of an object with the given class and values.
* @param cls The class of the object.
* @param values The values to pass to the object dispatcher.
* @returns The object's URI.
* @throws {RouteError} If no object dispatcher is available for the class.
* @throws {TypeError} If values are invalid.
* @since 0.7.0
*/
getObjectUri<TObject extends Object>(
// deno-lint-ignore no-explicit-any
cls: (new (...args: any[]) => TObject) & { typeId: URL },
values: Record<string, string>,
): URL;

/**
* Builds the URI of an actor's outbox with the given handle.
* @param handle The actor's handle.
Expand Down Expand Up @@ -151,6 +166,22 @@ export interface RequestContext<TContextData> extends Context<TContextData> {
*/
getActor(handle: string): Promise<Actor | null>;

/**
* Gets an object of the given class with the given values.
* @param cls The class to instantiate.
* @param values The values to pass to the object dispatcher.
* @returns The object of the given class with the given values, or `null`
* if the object is not found.
* @throws {Error} If no object dispatcher is available for the class.
* @throws {TypeError} If values are invalid.
* @since 0.7.0
*/
getObject<TObject extends Object>(
// deno-lint-ignore no-explicit-any
cls: (new (...args: any[]) => TObject) & { typeId: URL },
values: Record<string, string>,
): Promise<TObject | null>;

/**
* Gets the public key of the sender, if any exists and it is verified.
* Otherwise, `null` is returned.
Expand Down
Loading

0 comments on commit ab5891c

Please sign in to comment.