--- url: /cli.md description: >- The fedify command is a CLI toolchain for Fedify and debugging ActivityPub-enabled federated server apps. This section explains the key features of the fedify command. --- # `fedify`: CLI toolchain The `fedify` command is a CLI toolchain for Fedify and debugging ActivityPub-enabled federated server apps. Although it is primarily designed for developers who use Fedify, it can be used with any ActivityPub-enabled server. ## Installation ### Using npm If you have [Node.js] or [Bun] installed, you can install `fedify` by running the following command: ::: code-group ```sh [Node.js] npm install -g @fedify/cli ``` ```sh [Bun] bun install -g @fedify/cli ``` ::: [Node.js]: https://nodejs.org/ [Bun]: https://bun.sh/ ### Using Homebrew on macOS and Linux If you are using macOS or Linux and have [Homebrew] installed, you can install `fedify` by running the following command: ```sh brew install fedify ``` [Homebrew]: https://brew.sh/ ### Using Scoop on Windows If you are using Windows and have [Scoop] installed, you can install `fedify` by running the following command: ```powershell scoop install fedify ``` [Scoop]: https://scoop.sh/ ### Using Deno If you have [Deno] installed, you can install `fedify` by running the following command: ::: code-group ```sh [Linux/macOS] deno install \ -g \ -A \ --unstable-fs --unstable-kv --unstable-temporal \ -n fedify \ jsr:@fedify/cli ``` ```powershell [Windows] deno install ` -g ` -A ` --unstable-fs --unstable-kv --unstable-temporal ` -n fedify ` jsr:@fedify/cli ``` ::: [Deno]: https://deno.com/ ### Downloading the executable You can download the pre-built executables from the [releases] page. Download the appropriate executable for your platform and put it in your `PATH`. [releases]: https://github.com/fedify-dev/fedify/releases ## `fedify init`: Initializing a Fedify project *This command is available since Fedify 0.12.0.* [![The “fedify init” command demo](https://asciinema.org/a/671658.svg)](https://asciinema.org/a/671658) The `fedify init` command is used to initialize a new Fedify project. It creates a new directory with the necessary files and directories for a Fedify project. To create a new Fedify project, run the below command: ```sh fedify init my-fedify-project ``` The above command will start the interactive prompt to initialize a new Fedify project. It will ask you a few questions to set up the project: * JavaScript runtime: [Deno], [Bun], or [Node.js] * Package manager (if Node.js): [npm], [pnpm], or [Yarn] * Web framework: Bare-bones, [Fresh] (if Deno), [Hono], [Express] (unless Deno), or [Nitro] (unless Deno) * Key-value store: In-memory, [Redis], [PostgreSQL], or [Deno KV] (if Deno) * Message queue: In-memory, [Redis], [PostgreSQL], [AMQP] (e.g., [RabbitMQ]), or [Deno KV] (if Deno) Alternatively, you can specify the options in the command line to skip some of interactive prompts: [npm]: https://www.npmjs.com/ [pnpm]: https://pnpm.io/ [Yarn]: https://yarnpkg.com/ [Fresh]: https://fresh.deno.dev/ [Hono]: https://hono.dev/ [Express]: https://expressjs.com/ [Nitro]: https://nitro.unjs.io/ [Redis]: https://redis.io/ [PostgreSQL]: https://www.postgresql.org/ [AMQP]: https://www.amqp.org/ [RabbitMQ]: https://www.rabbitmq.com/ [Deno KV]: https://deno.com/kv ### `-r`/`--runtime`: JavaScript runtime You can specify the JavaScript runtime by using the `-r`/`--runtime` option. The available options are: * `deno`: [Deno] * `bun`: [Bun] * `node`: [Node.js] ### `-p`/`--package-manager`: Node.js package manager If you choose Node.js as the JavaScript runtime, you can specify the package manager by using the `-p`/`--package-manager` option. The available options are: * `npm`: [npm] * `pnpm`: [pnpm] * `yarn`: [Yarn] It's ignored if you choose Deno or Bun as the JavaScript runtime. ### `-w`/`--web-framework`: Web framework You can specify the web framework to integrate with Fedify by using the `-w`/`--web-framework` option. The available options are: * `fresh`: [Fresh] (if Deno) * `hono`: [Hono] * `express`: [Express] (unless Deno) * `nitro`: [Nitro] (unless Deno) If it's omitted, no web framework will be integrated. ### `-k`/`--kv-store`: Key-value store You can specify the key-value store to use by using the `-k`/`--kv-store` option. The available options are: * `redis`: [Redis] * `postgres`: [PostgreSQL] * `denokv`: [Deno KV] (if Deno) If it's omitted, the in-memory key-value store (which is for development purpose) will be used. ### `-q`/`--message-queue`: Message queue You can specify the message queue to use by using the `-q`/`--message-queue` option. The available options are: * `redis`: [Redis] * `postgres`: [PostgreSQL] * `amqp`: [AMQP] (e.g., [RabbitMQ]) * `denokv`: [Deno KV] (if Deno) If it's omitted, the in-process message queue (which is for development purpose) will be used. ## `fedify lookup`: Looking up an ActivityPub object The `fedify lookup` command is used to look up an ActivityPub object by its URL or an actor by its handle. For example, the below command looks up a `Note` object with the given URL: ```sh fedify lookup https://todon.eu/@hongminhee/112341925069749583 ``` The output will be like the below: ``` Note { id: URL "https://todon.eu/users/hongminhee/statuses/112341925069749583", attachments: [ Document { name: "The demo video on my terminal", url: URL "https://todon.eu/system/media_attachments/files/112/341/916/300/016/369/original/f83659866f94054f.mp"... 1 more character, mediaType: "video/mp4" } ], attribution: URL "https://todon.eu/users/hongminhee", contents: [ '

I'm working on adding a CLI toolchain to \[!NOTE] > The `fedify lookup` command cannot take multiple argument if > [`-t`/`--traverse`](#t-traverse-traverse-the-collection) option is turned > on. ### `-t`/`--traverse`: Traverse the collection *This option is available since Fedify 0.14.0.* The `-t`/`--traverse` option is used to traverse the collection when looking up a collection object. For example, the below command looks up a collection object: ```sh fedify lookup --traverse https://fosstodon.org/users/hongminhee/outbox ``` The difference between with and without the `-t`/`--traverse` option is that the former will output the objects in the collection, while the latter will output the collection object itself. This option only works with a single argument, and it has to be a collection. ### `-S`/`--suppress-errors`: Suppress partial errors during traversal *This option is available since Fedify 0.14.0.* The `-S`/`--suppress-errors` option is used to suppress partial errors during traversal. For example, the below command looks up a collection object with the `-t`/`--traverse` option: ```sh fedify lookup --traverse --suppress-errors https://fosstodon.org/users/hongminhee/outbox ``` The difference between with and without the `-S`/`--suppress-errors` option is that the former will suppress the partial errors during traversal, while the latter will stop the traversal when an error occurs. This option depends on the `-t`/`--traverse` option. ### `-c`/`--compact`: Compact JSON-LD > \[!NOTE] > This option is mutually exclusive with `-e`/`--expanded` and `-r`/`--raw`. You can also output the object in the [compacted JSON-LD] format by using the `-c`/`--compact` option: ```sh fedify lookup --compact https://todon.eu/@hongminhee/112341925069749583 ``` The output will be like the below: ```json { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://todon.eu/users/hongminhee/statuses/112341925069749583", "type": "Note", "attachment": { "type": "Document", "mediaType": "video/mp4", "name": "The demo video on my terminal", "url": "https://todon.eu/system/media_attachments/files/112/341/916/300/016/369/original/f83659866f94054f.mp4" }, "attributedTo": "https://todon.eu/users/hongminhee", "cc": "https://todon.eu/users/hongminhee/followers", "content": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

", "contentMap": { "en": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

" }, "published": "2024-04-27T07:08:57Z", "replies": { "id": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies", "type": "Collection", "first": { "type": "CollectionPage", "items": "https://todon.eu/users/hongminhee/statuses/112343493232608516", "next": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies?min_id=112343493232608516&page=true", "partOf": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies" } }, "as:sensitive": false, "to": "as:Public", "url": "https://todon.eu/@hongminhee/112341925069749583" } ``` [compacted JSON-LD]: https://www.w3.org/TR/json-ld/#compacted-document-form ### `-e`/`--expanded`: Expanded JSON-LD > \[!NOTE] > This option is mutually exclusive with `-c`/`--compact` and `-r`/`--raw`. You can also output the object in the [expanded JSON-LD] format by using the `-e`/`--expanded` option: ```sh fedify lookup --expand https://todon.eu/@hongminhee/112341925069749583 ``` The output will be like the below: ```json [ { "@id": "https://todon.eu/users/hongminhee/statuses/112341925069749583", "@type": [ "https://www.w3.org/ns/activitystreams#Note" ], "https://www.w3.org/ns/activitystreams#attachment": [ { "@type": [ "https://www.w3.org/ns/activitystreams#Document" ], "https://www.w3.org/ns/activitystreams#mediaType": [ { "@value": "video/mp4" } ], "https://www.w3.org/ns/activitystreams#name": [ { "@value": "The demo video on my terminal" } ], "https://www.w3.org/ns/activitystreams#url": [ { "@id": "https://todon.eu/system/media_attachments/files/112/341/916/300/016/369/original/f83659866f94054f.mp4" } ] } ], "https://www.w3.org/ns/activitystreams#attributedTo": [ { "@id": "https://todon.eu/users/hongminhee" } ], "https://www.w3.org/ns/activitystreams#cc": [ { "@id": "https://todon.eu/users/hongminhee/followers" } ], "https://www.w3.org/ns/activitystreams#content": [ { "@value": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

" }, { "@language": "en", "@value": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

" } ], "https://www.w3.org/ns/activitystreams#published": [ { "@type": "http://www.w3.org/2001/XMLSchema#dateTime", "@value": "2024-04-27T07:08:57Z" } ], "https://www.w3.org/ns/activitystreams#replies": [ { "@id": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies", "@type": [ "https://www.w3.org/ns/activitystreams#Collection" ], "https://www.w3.org/ns/activitystreams#first": [ { "@type": [ "https://www.w3.org/ns/activitystreams#CollectionPage" ], "https://www.w3.org/ns/activitystreams#items": [ { "@id": "https://todon.eu/users/hongminhee/statuses/112343493232608516" } ], "https://www.w3.org/ns/activitystreams#next": [ { "@id": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies?min_id=112343493232608516&page=true" } ], "https://www.w3.org/ns/activitystreams#partOf": [ { "@id": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies" } ] } ] } ], "https://www.w3.org/ns/activitystreams#sensitive": [ { "@value": false } ], "https://www.w3.org/ns/activitystreams#to": [ { "@id": "https://www.w3.org/ns/activitystreams#Public" } ], "https://www.w3.org/ns/activitystreams#url": [ { "@id": "https://todon.eu/@hongminhee/112341925069749583" } ] } ] ``` [expanded JSON-LD]: https://www.w3.org/TR/json-ld/#expanded-document-form ### `-r`/`--raw`: Raw JSON *This option is available since Fedify 0.15.0.* > \[!NOTE] > This option is mutually exclusive with `-c`/`--compact` and `-e`/`--expanded`. You can also output the fetched object in the raw JSON format by using the `-r`/`--raw` option: ```sh fedify lookup --raw https://todon.eu/@hongminhee/112341925069749583 ``` The output will be like the below: ```json { "@context": [ "https://www.w3.org/ns/activitystreams", { "ostatus": "http://ostatus.org#", "atomUri": "ostatus:atomUri", "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "sensitive": "as:sensitive", "toot": "http://joinmastodon.org/ns#", "votersCount": "toot:votersCount", "blurhash": "toot:blurhash", "focalPoint": { "@container": "@list", "@id": "toot:focalPoint" }, "Hashtag": "as:Hashtag" } ], "id": "https://todon.eu/users/hongminhee/statuses/112341925069749583", "type": "Note", "summary": null, "inReplyTo": null, "published": "2024-04-27T07:08:57Z", "url": "https://todon.eu/@hongminhee/112341925069749583", "attributedTo": "https://todon.eu/users/hongminhee", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "cc": [ "https://todon.eu/users/hongminhee/followers" ], "sensitive": false, "atomUri": "https://todon.eu/users/hongminhee/statuses/112341925069749583", "inReplyToAtomUri": null, "conversation": "tag:todon.eu,2024-04-27:objectId=90184788:objectType=Conversation", "content": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

", "contentMap": { "en": "

I'm working on adding a CLI toolchain to #Fedify to help with debugging. The first feature I implemented is the ActivityPub object lookup.

Here's a demo.

#fedidev #ActivityPub

" }, "attachment": [ { "type": "Document", "mediaType": "video/mp4", "url": "https://todon.eu/system/media_attachments/files/112/341/916/300/016/369/original/f83659866f94054f.mp4", "name": "The demo video on my terminal", "blurhash": "U87_4lWB_3WBt7bHazWV~qbHaybFozj[ayfj", "width": 1092, "height": 954 } ], "tag": [ { "type": "Hashtag", "href": "https://todon.eu/tags/fedify", "name": "#fedify" }, { "type": "Hashtag", "href": "https://todon.eu/tags/fedidev", "name": "#fedidev" }, { "type": "Hashtag", "href": "https://todon.eu/tags/activitypub", "name": "#activitypub" } ], "replies": { "id": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies", "type": "Collection", "first": { "type": "CollectionPage", "next": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies?min_id=112343493232608516&page=true", "partOf": "https://todon.eu/users/hongminhee/statuses/112341925069749583/replies", "items": [ "https://todon.eu/users/hongminhee/statuses/112343493232608516" ] } } } ``` ### `-a`/`--authorized-fetch`: Authorized fetch You can also use the `-a`/`--authorized-fetch` option to fetch the object with authentication. Under the hood, this option generates an one-time key pair, spins up a temporary ActivityPub server to serve the public key, and signs the request with the private key. Here's an example where the `fedify lookup` fails due to the object being protected: ```sh fedify lookup @tchambers@indieweb.social ``` The above command will output the below error: ``` Failed to fetch the object. It may be a private object. Try with -a/--authorized-fetch. ``` However, you can fetch the object with the `-a`/`--authorized-fetch` option: ```sh fedify lookup --authorized-fetch @tchambers@indieweb.social ``` This time, the above command will output the object successfully: ``` Person { id: URL "https://indieweb.social/users/tchambers", attachments: [ PropertyValue { name: "Indieweb Site", value: 'Technologist, writer, admin of indieweb.social. Fascinated by how new politics impacts technology"... 346 more characters, url: URL "https://indieweb.social/@tchambers", preferredUsername: "tchambers", publicKey: CryptographicKey { id: URL "https://indieweb.social/users/tchambers#main-key", owner: URL "https://indieweb.social/users/tchambers", publicKey: CryptoKey { type: "public", extractable: true, algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: Uint8Array(3) [ 1, 0, 1 ], hash: { name: "SHA-256" } }, usages: [ "verify" ] } }, manuallyApprovesFollowers: false, inbox: URL "https://indieweb.social/users/tchambers/inbox", outbox: URL "https://indieweb.social/users/tchambers/outbox", following: URL "https://indieweb.social/users/tchambers/following", followers: URL "https://indieweb.social/users/tchambers/followers", endpoints: Endpoints { sharedInbox: URL "https://indieweb.social/inbox" }, discoverable: true, memorial: false, indexable: true } ``` ### `--first-knock`: First-knock spec for `-a`/`--authorized-fetch` *This option is available since Fedify 1.6.0.* The `--first-knock` option is used to specify which HTTP Signatures spec to try first when using the `-a`/`--authorized-fetch` option. The ActivityPub ecosystem currently uses different versions of HTTP Signatures specifications, and the [double-knocking] technique (trying one version, then falling back to another if rejected) allows for better compatibility across servers. Available options are: `draft-cavage-http-signatures-12` : [HTTP Signatures], which is obsolete but still widely adopted in the fediverse as of May 2025. `rfc9421` (default) : [RFC 9421]: HTTP Message Signatures, which is the final revision of the specification and is recommended, but not yet widely adopted in the fediverse as of May 2025. If the first signature attempt fails, Fedify will automatically try the other specification format, implementing the [double-knocking] technique described in the [ActivityPub HTTP Signatures] specification. [double-knocking]: https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions [HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12 [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421 [ActivityPub HTTP Signatures]: https://swicg.github.io/activitypub-http-signature/ ### `-u`/`--user-agent`: Custom `User-Agent` header *This option is available since Fedify 1.3.0.* By default, the `fedify lookup` command sends the `User-Agent` header with the value `Fedify/1.3.0 (Deno/2.0.4)` (version numbers may vary). You can specify a custom `User-Agent` header by using the `-u`/`--user-agent` option. For example, to send the `User-Agent` header with the value `MyApp/1.0`, run the below command: ```sh fedify lookup --user-agent MyApp/1.0 @fedify@hollo.social ``` ### `-s`/`--separator`: Output separator *This option is available since Fedify 1.3.0.* You can specify the separator between the outputs when looking up multiple objects at once by using the `-s`/`--separator` option. For example, to use the separator `====` between the outputs, run the below command: ```sh fedify lookup -s ==== @fedify@hollo.social @hongminhee@fosstodon.org ``` It does not affect the output when looking up a single object. > \[!TIP] > The separator is also used when looking up a collection object with the > [`-t`/`--traverse`](#t-traverse-traverse-the-collection) option. ## `fedify inbox`: Ephemeral inbox server The `fedify inbox` command is used to spin up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. This is useful when you want to test and debug the outgoing activities of your server. To start an ephemeral inbox server, run the below command: ```sh fedify inbox ``` If it goes well, you will see the output like the below (without termination; press ^C to stop the server): ``` ✔ The ephemeral ActivityPub server is up and running: https://f9285cf4974c86.lhr.life/ ✔ Sent follow request to @faguscus_dashirniul@activitypub.academy. ╭───────────────┬─────────────────────────────────────────╮ │ Actor handle: │ i@f9285cf4974c86.lhr.life │ ├───────────────┼─────────────────────────────────────────┤ │ Actor URI: │ https://f9285cf4974c86.lhr.life/i │ ├───────────────┼─────────────────────────────────────────┤ │ Actor inbox: │ https://f9285cf4974c86.lhr.life/i/inbox │ ├───────────────┼─────────────────────────────────────────┤ │ Shared inbox: │ https://f9285cf4974c86.lhr.life/inbox │ ╰───────────────┴─────────────────────────────────────────╯ ``` Although the given URIs and handle are short-lived, they are anyway publicly dereferenceable until the server is terminated. You can use these URIs and handle to test and debug the outgoing activities of your server. If any incoming activities are received, the server will log them to the console: ``` ╭────────────────┬─────────────────────────────────────╮ │ Request #: │ 2 │ ├────────────────┼─────────────────────────────────────┤ │ Activity type: │ Follow │ ├────────────────┼─────────────────────────────────────┤ │ HTTP request: │ POST /i/inbox │ ├────────────────┼─────────────────────────────────────┤ │ HTTP response: │ 202 │ ├────────────────┼─────────────────────────────────────┤ │ Details │ https://f9285cf4974c86.lhr.life/r/2 │ ╰────────────────┴─────────────────────────────────────╯ ``` You can also see the details of the incoming activities by visiting the `/r/:id` endpoint of the server in your browser: ![The details of the incoming activities](cli/fedify-inbox-web.png) ### `-f`/`--follow`: Follow an actor The `-f`/`--follow` option is used to follow an actor. You can specify the actor handle or URI to follow. For example, to follow the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ```sh fedify inbox -f @john@doe.com -f @jane@doe.com ``` > \[!NOTE] > Although `-f`/`--follow` option sends `Follow` activities to the specified > actors, it does not guarantee that they will accept the follow requests. > If the actors accept the follow requests, you will receive the `Accept` > activities in the inbox server, and the server will log them to the console: > > ``` > ╭────────────────┬─────────────────────────────────────╮ > │ Request #: │ 0 │ > ├────────────────┼─────────────────────────────────────┤ > │ Activity type: │ Accept │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP request: │ POST /i/inbox │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP response: │ 202 │ > ├────────────────┼─────────────────────────────────────┤ > │ Details │ https://f9285cf4974c86.lhr.life/r/0 │ > ╰────────────────┴─────────────────────────────────────╯ > ``` ### `-a`/`--accept-follow`: Accept follow requests The `-a`/`--accept-follow` option is used to accept follow requests from actors. You can specify the actor handle or URI to accept follow requests. Or you can accept all follow requests by specifying the wildcard `*`. For example, to accept follow requests from the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ```sh fedify inbox -a @john@doe.com -a @jane@doe.com ``` When the follow requests are received from the specified actors, the server will immediately send the `Accept` activities to them. Otherwise, the server will just log the `Follow` activities to the console without sending the `Accept` activities. ### `-T`/`--no-tunnel`: Local server without tunneling The `-T`/`--no-tunnel` option is used to disable the tunneling feature of the inbox server. By default, the inbox server tunnels the local server to the public internet, so that the server is accessible from the outside. If you want to disable the tunneling feature, run the below command: ```sh fedify inbox --no-tunnel ``` It would be useful when you want to test the server locally but are worried about the security implications of exposing the server to the public internet. > \[!NOTE] > If you disable the tunneling feature, the ephemeral ActivityPub instance will > be served via HTTP instead of HTTPS. ## `fedify node`: Visualizing an instance's NodeInfo *This command is available since Fedify 1.2.0.* ![The result of fedify lookup fosstodon.org. The NodeInfo document is visualized along with the favicon.](cli/fedify-node.png) The `fedify node` command fetches the given instance's [NodeInfo] document and visualizes it in [`neofetch`]-style. The argument can be either a bare hostname or a full URL. > \[!TIP] > Not all instances provide the NodeInfo document. If the given instance does > not provide the NodeInfo document, the command will output an error message. [NodeInfo]: https://nodeinfo.diaspora.software/ [`neofetch`]: https://github.com/dylanaraps/neofetch ### `-b`/`--best-effort`: Parsing with best effort The `-b`/`--best-effort` option is used to parse the NodeInfo document with best effort. If the NodeInfo document is not well-formed, the option will try to parse it as much as possible. ### `--no-favicon`: Disabling favicon fetching The `--no-favicon` option is used to disable fetching the favicon of the instance. ### `-m`/`--metadata`: Showing metadata The `-m`/`--metadata` option is used to show the extra metadata of the NodeInfo, i.e., the `metadata` field of the document. ### `-u`/`--user-agent`: Custom `User-Agent` header *This option is available since Fedify 1.3.0.* By default, the `fedify node` command sends the `User-Agent` header with the value `Fedify/1.3.0 (Deno/2.0.4)` (version numbers may vary). You can specify a custom `User-Agent` header by using the `-u`/`--user-agent` option. For example, to send the `User-Agent` header with the value `MyApp/1.0`, run the below command: ```sh fedify node --user-agent MyApp/1.0 mastodon.social ``` ## `fedify tunnel`: Exposing a local HTTP server to the public internet *This command is available since Fedify 0.13.0.* The `fedify tunnel` command is used to expose a local HTTP server to the public internet using a secure tunnel. It is useful when you want to test your local ActivityPub server with the real-world ActivityPub instances. To create a tunnel for a local server, for example, running on port 3000, run the below command: ```sh fedify tunnel 3000 ``` > \[!TIP] > > The HTTP requests through the tunnel have the following headers: > > `X-Forwarded-For` > : The IP address of the client. > > `X-Forwarded-Proto` > : The protocol of the client, either `http` or `https`. > > `X-Forwarded-Host` > : The host of the public tunnel server. > > If you want to make your local server aware of these headers, you can use > the [x-forwarded-fetch] middleware in front of your HTTP server. > > For more information, see [*How the `Federation` object recognizes the domain > name* section](./manual/federation.md#how-the-federation-object-recognizes-the-domain-name) > in the *Federation* document. [x-forwarded-fetch]: https://github.com/dahlia/x-forwarded-fetch ### `-s`/`--service`: The tunneling service The `-s`/`--service` option is used to specify the tunneling service to use. Available services can be found in the output of the `fedify tunnel --help` command. For example, to use the serveo.net, run the below command: ```sh fedify tunnel --service serveo.net 3000 ``` ## Shell completions The `fedify` command supports shell completions for [Bash](#bash), [Fish](#fish), and [Zsh](#zsh). ### Bash To enable Bash completions add the following line to your profile file (*~/.bashrc*, *~/.bash\_profile*, or *~/.profile*): ```bash source <(fedify completions bash) ``` ### Fish To enable Fish completions add the following line to your profile file (*~/.config/fish/config.fish*): ```fish source (fedify completions fish | psub) ``` ### Zsh To enable Zsh completions add the following line to your profile file (*~/.zshrc*): ```zsh source <(fedify completions zsh) ``` --- --- url: /manual/access-control.md description: >- Fedify provides a flexible access control system that allows you to control who can access your resources. This section explains how to use the access control system. --- # Access control *This API is available since Fedify 0.7.0.* Fedify provides a flexible access control system that allows you to control who can access your resources through the method named [authorized fetch], which is popularized by Mastodon. The method requires HTTP Signatures to be attached to even `GET` requests, and Fedify automatically verifies the signatures and derives the actor from the signature. > \[!NOTE] > Although the method is popularized by Mastodon, it is not a part of the > ActivityPub specification, and clients are not required to use the method. > Turning this feature on may limit the compatibility with some clients. [authorized fetch]: https://swicg.github.io/activitypub-http-signature/#authorized-fetch ## Enabling authorized fetch To enable authorized fetch, you need to register an `AuthorizePredicate` callback with `ActorCallbackSetters.authorize()` or `CollectionCallbackSetters.authorize()`, or `ObjectAuthorizePredicate` callback with `ObjectCallbackSetters.authorize()`. The below example shows how to enable authorized fetch for the actor dispatcher: ```typescript{9-11} twoslash // @noErrors: 2307 2345 import type { Actor, Federation } from "@fedify/fedify"; /** * A hypothetical `Federation` instance. */ const federation = null as unknown as Federation; /** * A hypothetical function that checks if the user blocks the actor. * @param userId The ID of the user to check if the actor is blocked. * @param signedKeyOwner The actor who signed the request. * @returns `true` if the actor is blocked; otherwise, `false`. */ async function isBlocked(userId: string, signedKeyOwner: Actor): Promise { return false; } // ---cut-before--- import { federation } from "./your-federation.ts"; import { isBlocked } from "./your-blocklist.ts"; federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // Omitted for brevity; see the related section for details. }) .authorize(async (ctx, identifier) => { const signedKeyOwner = await ctx.getSignedKeyOwner(); if (signedKeyOwner == null) return false; return !await isBlocked(identifier, signedKeyOwner); }); ``` The equivalent method is available for collections as well: ```typescript{9-11} twoslash // @noErrors: 2307 2345 import type { Actor, Federation } from "@fedify/fedify"; /** * A hypothetical `Federation` instance. */ const federation = null as unknown as Federation; /** * A hypothetical function that checks if the user blocks the actor. * @param userId The ID of the user to check if the actor is blocked. * @param signedKeyOwner The actor who signed the request. * @returns `true` if the actor is blocked; otherwise, `false`. */ async function isBlocked(userId: string, signedKeyOwner: Actor): Promise { return false; } // ---cut-before--- import { federation } from "./your-federation.ts"; import { isBlocked } from "./your-blocklist.ts"; federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier) => { // Omitted for brevity; see the related section for details. }) .authorize(async (ctx, identifier) => { const signedKeyOwner = await ctx.getSignedKeyOwner(); if (signedKeyOwner == null) return false; return !await isBlocked(identifier, signedKeyOwner); }); ``` If the predicate returns `false`, the request is rejected with a `401 Unauthorized` response. ## Fine-grained access control You may not want to block everything from an unauthorized user, but only filter some resources. For example, you may want to show some private posts to a specific group of users. In such cases, you can use the `RequestContext.getSignedKeyOwner()` method inside the dispatcher to get the actor who signed the request and make a decision based on the actor. The method returns the `Actor` object who signed the request (more precisely, the owner of the key that signed the request, if the key is associated with an actor). The below pseudo code shows how to filter out private posts: ```typescript{7} twoslash // @noErrors: 2307 import type { Actor, Create, Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; interface Post { /** * A hypothetical method that checks if the post is visible to the actor. * @param actor The actor who wants to access the post. * @returns `true` if the post is visible; otherwise, `false`. */ isVisibleTo(actor: Actor): boolean; } /** * A hypothetical function that gets posts from the database. * @param userId The ID of the user to get posts. * @returns The posts of the user. */ async function getPosts(userId: string): Promise { return []; } /** * A hypothetical function that converts a model object to an ActivityStreams object. * @param post The model object to convert. * @returns The ActivityStreams object. */ function toCreate(post: Post): Create { return {} as unknown as Create; } // ---cut-before--- import { federation } from "./your-federation.ts"; import { getPosts, toCreate } from "./your-model.ts"; federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier) => { const posts = await getPosts(identifier); // Get posts from the database const keyOwner = await ctx.getSignedKeyOwner(); // Get the actor who signed the request if (keyOwner == null) return { items: [] }; // Return an empty array if the actor is not found const items = posts .filter(post => post.isVisibleTo(keyOwner)) // [!code highlight] .map(toCreate); // Convert model objects to ActivityStreams objects return { items }; }); ``` ## Instance actor When you enable authorized fetch, you need to fetch actors from other servers to retrieve their public keys. However, fetching resources from other servers may cause an infinite loop if the other server also requires authorized fetch, which causes another request to your server for the public key, and so on. The most common way to prevent it is a pattern called [instance actor], which is an actor that represents the whole instance and exceptionally does not require authorized fetch. You can use the instance actor to fetch resources from other servers without causing an infinite loop. Usually, many ActivityPub implementations name their instance actor as their domain name, such as `example.com@example.com`. Here is an example of how to implement an instance actor: ```typescript{3-11,20-27} twoslash import { type Actor, Application, type Federation, Person } from "@fedify/fedify"; /** * A hypothetical `Federation` instance. */ const federation = null as unknown as Federation; /** * A hypothetical function that checks if the user blocks the actor. * @param userId The ID of the user to check if the actor is blocked. * @param signedKeyOwner The actor who signed the request. * @returns `true` if the actor is blocked; otherwise, `false`. */ async function isBlocked(userId: string, signedKeyOwner: Actor): Promise { return false; } // ---cut-before--- federation .setActorDispatcher("/actors/{identifier}", async (ctx, identifier) => { if (identifier === ctx.hostname) { // A special case for the instance actor: return new Application({ id: ctx.getActorUri(identifier), // Omitted for brevity; other properties of the instance actor... // Note that you have to set the `publicKey` property of the instance // actor. }); } // A normal case for a user actor: return new Person({ id: ctx.getActorUri(identifier), // Omitted for brevity; other properties of the user actor... }); }) .authorize(async (ctx, identifier) => { // Allow the instance actor to access any resources: if (identifier === ctx.hostname) return true; // Create an authenticated document loader behalf of the instance actor: const documentLoader = await ctx.getDocumentLoader({ identifier: ctx.hostname, }); // Get the actor who signed the request: const signedKeyOwner = await ctx.getSignedKeyOwner({ documentLoader }); if (signedKeyOwner == null) return false; return !await isBlocked(identifier, signedKeyOwner); }); ``` [instance actor]: https://swicg.github.io/activitypub-http-signature/#instance-actor --- --- url: /manual/actor.md description: >- You can register an actor dispatcher so that Fedify can dispatch an appropriate actor by its identifier. This section explains how to register an actor dispatcher and the key properties of an actor. --- # Actor dispatcher In ActivityPub, [actors] are entities that can perform [activities]. You can register an actor dispatcher so that Fedify can dispatch an appropriate actor by its identifier. Since the actor dispatcher is the most significant part of the Fedify, it is the first thing you need to do to make Fedify work. An actor dispatcher is a callback function that takes a `Context` object and an identifier, and returns an actor object. The actor object can be one of the following: * `Application` * `Group` * `Organization` * `Person` * `Service` The below example shows how to register an actor dispatcher: ```typescript{7-15} twoslash // @noErrors: 2451 2345 import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; interface User { } const user = null as User | null; // ---cut-before--- import { createFederation, Person } from "@fedify/fedify"; const federation = createFederation({ // Omitted for brevity; see the related section for details. }); federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // Work with the database to find the actor by the identifier. if (user == null) return null; // Return null if the actor is not found. return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, // Many more properties; see the next section for details. }); }); ``` In the above example, the `~Federatable.setActorDispatcher()` method registers an actor dispatcher for the `/users/{identifier}` path. This pattern syntax follows the [URI Template] specification. > \[!TIP] > By registering the actor dispatcher, `Federation.fetch()` automatically > deals with [WebFinger] requests for the actor. > \[!TIP] > By default, Fedify assumes that the actor's identifier is the WebFinger > username. If you want to decouple the WebFinger username from the actor's > identifier, you can register an actor handle mapper through the > `~ActorCallbackSetters.mapHandle()` method. > > See the [next section](#decoupling-actor-uris-from-webfinger-usernames) > for details. [actors]: https://www.w3.org/TR/activitystreams-core/#actors [activities]: https://www.w3.org/TR/activitystreams-core/#activities [URI Template]: https://datatracker.ietf.org/doc/html/rfc6570 [WebFinger]: https://datatracker.ietf.org/doc/html/rfc7033 ## Actor identifier and WebFinger username An actor *identifier* is a unique string that identifies the actor. It can be a username, a UUID, or any other unique string. The actor identifier is used as a URL parameter in the actor dispatcher and other dispatchers. It's usually used to find the actor in your database, i.e., primary key. A WebFinger *username* is a string that comes before the domain part of the fediverse handle, e.g., `hongminhee` in `@hongminhee@fosstodon.org`. The WebFinger username is also used as the `preferredUsername` property of the actor. It's usually displayed in the user interface, and used to find the actor by the WebFinger protocol, i.e., looking up the fediverse handle in the search box. It's also called the *bare handle*. By default, Fedify assumes that the actor's identifier is the WebFinger username, but you can decouple the WebFinger username from the actor's identifier if you want. You can think of the difference between these two approaches as analogous to [natural key] vs. [surrogate key] in the database design. There are pros and cons to using the WebFinger username as the actor's identifier (which is Fedify's default): Pros : - The actor URI is more predictable and human-readable, which makes debugging easier. \- The internal ID of the actor can be hidden from the public. Cons : - Changing the WebFinger username may break the existing network. Hence, the fediverse handle is immutable in practice. \- It's usually treated as an anti-pattern in the fediverse. You need to choose the best approach for you before implementing the actor dispatcher. If you decided to use the WebFinger username as the actor's identifier, there's nothing to do—Fedify assumes it by default. If you decided to decouple the WebFinger username from the actor's identifier, see the [next section](#decoupling-actor-uris-from-webfinger-usernames) for details. [natural key]: https://en.wikipedia.org/wiki/Natural_key [surrogate key]: https://en.wikipedia.org/wiki/Surrogate_key ## Key properties of an `Actor` Despite ActivityPub declares every property of an actor as optional, in practice, you need to set some of them to make the actor work properly with the existing ActivityPub implementations. The following shows the key properties of an `Actor` object: ### `id` The `~Object.id` property is the URI of the actor. It is a required property in ActivityPub. You can use the `Context.getActorUri()` method to generate the dereferenceable URI of the actor by its identifier. ### `preferredUsername` The `preferredUsername` property is the WebFinger username of the actor. Unless [you decouple the WebFinger username from the actor's identifier](#decoupling-actor-uris-from-webfinger-usernames), it is okay to set the `preferredUsername` property to the actor's identifier. ### `name` The `~Object.name` property is the full name of the actor. ### `summary` The `~Object.summary` property is usually a short biography of the actor. ### `url` The `~Object.url` property usually refers to the actor's profile page. ### `published` The `~Object.published` property is the date and time when the actor was created. Note that Fedify represents the date and time in the [`Temporal.Instant`] value. [`Temporal.Instant`]: https://tc39.es/proposal-temporal/docs/instant.html ### `inbox` The `inbox` property is the URI of the actor's inbox. You can use the `Context.getInboxUri()` method to generate the URI of the actor's inbox. See the [*Inbox listeners*](./inbox.md) section for details. ### `outbox` The `outbox` property is the URI of the actor's outbox. You can use the `Context.getOutboxUri()` method to generate the URI of the actor's outbox. ### `followers` The `followers` property is the URI of the actor's followers collection. You can use the `Context.getFollowersUri()` method to generate the URI of the actor's followers collection. ### `following` The `following` property is the URI of the actor's following collection. You can use the `Context.getFollowingUri()` method to generate the URI of the actor's following collection. ### `endpoints` The `endpoints` property is an `Endpoints` instance, an object that contains the URIs of the actor's endpoints. The most important endpoint is the `sharedInbox`. You can use the `Context.getInboxUri()` method with no arguments to generate the URI of the actor's shared inbox: ```typescript twoslash import { Endpoints, Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- new Endpoints({ sharedInbox: ctx.getInboxUri() }) ``` ### `publicKey` The `publicKey` property contains the public key of the actor. It is a `CryptographicKey` instance. This property is usually used for verifying [HTTP Signatures](./send.md#http-signatures). See the [next section](#public-keys-of-an-actor) for details. > \[!TIP] > In theory, an actor has multiple `publicKeys`, but in practice, the most > implementations have trouble with multiple keys. Therefore, it is recommended > to set only one key in the `publicKey` property. Usually, it contains > the first RSA-PKCS#1-v1.5 public key of the actor. > > If you need to set multiple keys, you can use the `assertionMethods` property > instead. ### `assertionMethods` *This API is available since Fedify 0.10.0.* The `assertionMethods` property contains the public keys of the actor. It is an array of `Multikey` instances. This property is usually used for verifying [Object Integrity Proofs](./send.md#object-integrity-proofs). > \[!TIP] > Usually, the `assertionMethods` property contains the Ed25519 public keys of > the actor. Although it is okay to include RSA-PKCS#1-v1.5 public keys too, > those RSA-PKCS#1-v1.5 keys are not used for verifying Object Integrity Proofs. ## Public keys of an `Actor` In order to sign and verify the activities, you need to set the `publicKey` and `assertionMethods` property of the actor. The `publicKey` property contains a `CryptographicKey` instance, and the `assertionMethods` property contains an array of `Multikey` instances. Usually you don't have to create them manually. Instead, you can register a key pairs dispatcher through the `~ActorCallbackSetters.setKeyPairsDispatcher()` method so that Fedify can dispatch appropriate key pairs by the actor's identifier: ```typescript{4-6,10-14,17-26} twoslash import { type Federation, Person } from "@fedify/fedify"; const federation = null as unknown as Federation; interface User {} const user = null as User | null; const publicKey1 = null as unknown as CryptoKey; const privateKey1 = null as unknown as CryptoKey; const publicKey2 = null as unknown as CryptoKey; const privateKey2 = null as unknown as CryptoKey; // ---cut-before--- federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // Work with the database to find the actor by the identifier. if (user == null) return null; // Return null if the actor is not found. // Context.getActorKeyPairs() method dispatches the key pairs of an actor // by the identifier, and returns an array of key pairs in various formats: const keys = await ctx.getActorKeyPairs(identifier); return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, // For the publicKey property, we only use first CryptographicKey: publicKey: keys[0].cryptographicKey, // For the assertionMethods property, we use all Multikey instances: assertionMethods: keys.map((key) => key.multikey), // Many more properties; see the previous section for details. }); }) .setKeyPairsDispatcher(async (ctx, identifier) => { // Work with the database to find the key pair by the identifier. if (user == null) return []; // Return null if the key pair is not found. // Return the loaded key pair. See the below example for details. return [ { publicKey: publicKey1, privateKey: privateKey1 }, { publicKey: publicKey2, privateKey: privateKey2 }, // ... ]; }); ``` In the above example, the `~ActorCallbackSetters.setKeyPairsDispatcher()` method registers a key pairs dispatcher. The key pairs dispatcher is a callback function that takes context data and an identifier, and returns an array of [`CryptoKeyPair`] object which is defined in the Web Cryptography API. Usually, you need to generate key pairs for each actor when the actor is created (i.e., when a new user is signed up), and securely store actor's key pairs in the database. The key pairs dispatcher should load the key pairs from the database and return them. How to generate key pairs and store them in the database is out of the scope of this document, but here's a simple example of how to generate key pairs and store them in a [Deno KV] database in form of JWK: ```typescript twoslash const identifier: string = ""; // ---cut-before--- import { generateCryptoKeyPair, exportJwk } from "@fedify/fedify"; const kv = await Deno.openKv(); const rsaPair = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"); const ed25519Pair = await generateCryptoKeyPair("Ed25519"); await kv.set(["keypair", "rsa", identifier], { privateKey: await exportJwk(rsaPair.privateKey), publicKey: await exportJwk(rsaPair.publicKey), }); await kv.set(["keypair", "ed25519", identifier], { privateKey: await exportJwk(ed25519Pair.privateKey), publicKey: await exportJwk(ed25519Pair.publicKey), }); ``` > \[!TIP] > Fedify currently supports two key types: > > * RSA-PKCS#1-v1.5 (`"RSASSA-PKCS1-v1_5"`) is used for [HTTP > Signatures](./send.md#http-signatures), [HTTP Message > Signatures](./send.md#http-message-signatures), and [Linked Data > Signatures](./send.md#linked-data-signatures). > * Ed25519 (`"Ed25519"`) is used for [Object Integrity > Proofs](./send.md#object-integrity-proofs). > > HTTP Signatures and Linked Data Signatures are de facto standards for signing > ActivityPub activities, and Object Integrity Proofs is a new standard for > verifying the integrity of the objects in the fediverse. While HTTP > Signatures and Linked Data Signatures are widely supported in the fediverse, > it's limited to the RSA-PKCS#1-v1.5 algorithm. > > If your federated app needs to support HTTP Signatures, Linked Data > Signatures, and Object Integrity Proofs at the same time, > you need to generate both RSA-PKCS#1-v1.5 and Ed25519 key > pairs for each actor, and store them in the database—and we recommend > you to support both key types. Here's an example of how to load key pairs from the database too: ```typescript{12-34} twoslash // @noErrors: 2345 import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- import { importJwk } from "@fedify/fedify"; interface KeyPairEntry { privateKey: JsonWebKey; publicKey: JsonWebKey; } federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // Omitted for brevity; see the previous example for details. }) .setKeyPairsDispatcher(async (ctx, identifier) => { const kv = await Deno.openKv(); const result: CryptoKeyPair[] = []; const rsaPair = await kv.get( ["keypair", "rsa", identifier], ); if (rsaPair?.value != null) { result.push({ privateKey: await importJwk(rsaPair.value.privateKey, "private"), publicKey: await importJwk(rsaPair.value.publicKey, "public"), }); } const ed25519Pair = await kv.get( ["keypair", "ed25519", identifier], ); if (ed25519Pair?.value != null) { result.push({ privateKey: await importJwk(ed25519Pair.value.privateKey, "private"), publicKey: await importJwk(ed25519Pair.value.publicKey, "public"), }); } return result; }); ``` [`CryptoKeyPair`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair [Deno KV]: https://deno.com/kv ## Constructing actor URIs To construct an actor URI, you can use the `Context.getActorUri()` method. This method takes an identifier and returns a dereferenceable URI of the actor. The below example shows how to construct an actor URI: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getActorUri("john_doe") ``` In the above example, the `Context.getActorUri()` method generates the dereferenceable URI of the actor with the identifier `"john_doe"`. If you [decouple the WebFinger username from the actor's identifier](#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the actor dispatcher to the `Context.getActorUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getActorUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getActorUri()` method does not guarantee that the actor > URI is always dereferenceable for every argument. Make sure that > the argument is a valid identifier before calling the method. ## Decoupling actor URIs from WebFinger usernames *This API is available since Fedify 0.15.0.* > \[!TIP] > The WebFinger username means the username part of the `acct:` URI or > the fediverse handle. For example, the WebFinger username of the > `acct:fedify@hollo.social` URI or the `@fedify@hollo.social` handle > is `fedify`. By default, Fedify uses the identifier as the WebFinger username. However, you can decouple the WebFinger username from the identifier by registering an actor handle mapper through the `~ActorCallbackSetters.mapHandle()` method: ```typescript twoslash // @noErrors: 2391 2345 import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; 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; }); ``` Decoupling the WebFinger username from the identifier is useful when you want to let users change their WebFinger username without breaking the existing network, because changing the WebFinger username does not affect the actor URI. > \[!NOTE] > We highly recommend you to set the actor's `preferredUsername` property to > the corresponding WebFinger username so that peers can find the actor's > fediverse handle by fetching the actor object. ## WebFinger links Some properties of an `Actor` returned by the actor dispatcher affect responses to WebFinger requests. ### `preferredUsername` *This API is available since Fedify 0.15.0.* The `preferredUsername` property is the bare handle of the actor. It is used as the WebFinger username, used in the `acct:` URI of the `aliases` property of the WebFinger response. ### `url` The `url` property usually refers to the actor's profile page. It is used as the `links` property of the WebFinger response, with the `rel` property set to . > \[!TIP] > You probably want to implement [actor aliases](#actor-aliases) if you want > to give different URLs to the actor URI and its web profile URL. If you want to provide links with other `rel` than , you can put `Link` objects in the `url` property: ```typescript{8-16} twoslash import { type Federation, Person, Link } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, urls: [ new URL(`/@${identifier}`, ctx.origin), new Link({ rel: "alternate", href: new URL(`/@${identifier}/atom.xml`, ctx.origin), mediaType: "application/atom+xml", }), new Link({ rel: "http://openid.net/specs/connect/1.0/issuer", href: new URL("/openid", ctx.origin), }), ], // Omitted for brevity; see the previous example for details. }); }); ``` With the above example, the WebFinger response will contain the following `links` property: ```json { "subject": "acct:johndoe@example.com", "aliases": [ "https://example.com/users/john_doe" ], "links": [ { "rel": "http://webfinger.net/rel/profile-page", "href": "https://example.com/@john_doe" }, { "rel": "alternate", "href": "https://example.com/@john_doe/atom.xml", "type": "application/atom+xml" }, { "rel": "http://openid.net/specs/connect/1.0/issuer", "href": "https://example.com/openid" } ] } ``` ### `icon` *This API is available since Fedify 1.0.0.* 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 . ## 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: 2345 2391 import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; 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: 2345 7006 > import { type Federation } from "@fedify/fedify"; > const federation = null as unknown as Federation; > 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. --- --- url: /manual/collections.md description: >- Fedify provides a generic way to construct and handle collections. This section explains how to work with collections in Fedify. --- # Collections In ActivityPub, a [collection] is a group of objects. For example, the followers collection consists of the followers of an actor, and the outbox collection consists of the activities that an actor has sent. Fedify provides a generic way to construct and handle collections. This section explains how to work with collections in Fedify. [collection]: https://www.w3.org/TR/activitypub/#collections ## Outbox > \[!TIP] > Since the way to construct an outbox collection is the same as the way to > construct any other collection, the following examples are also applicable to > constructing other collections. First, let's see how to construct an [outbox] collection. An outbox collection consists of the activities that an actor has sent. As each collection has its own URI, the outbox collection has its own URI, too. The URI of the outbox collection is determined by the first parameter of the `~Federatable.setOutboxDispatcher()` method: ```typescript twoslash // @noErrors: 2345 import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier) => { // Work with the database to find the activities that the actor has sent. // Omitted for brevity. See the next example for details. }); ``` Each actor has its own outbox collection, so the URI pattern of the outbox dispatcher should include the actor's `{identifier}`. The URI pattern syntax follows the [URI Template] specification. Since the outbox is a collection of activities, the outbox dispatcher should return an array of activities. The following example shows how to construct an outbox collection: ```typescript twoslash import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents a post. */ interface Post { /** * The ID of the post. */ id: string; /** * The title of the post. */ title: string; /** * The content of the post. */ content: string; } /** * A hypothetical function that returns the posts that an actor has sent. * @param identifier The actor's identifier. * @returns The posts that the actor has sent. */ function getPostsByUserId(userId: string): Post[] { return []; } // ---cut-before--- import { Article, Create } from "@fedify/fedify"; federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier) => { // Work with the database to find the activities that the actor has sent // (the following `getPostsByUserId` is a hypothetical function): const posts = await getPostsByUserId(identifier); // Turn the posts into `Create` activities: const items = posts.map(post => new Create({ id: new URL(`/posts/${post.id}#activity`, ctx.url), actor: ctx.getActorUri(identifier), object: new Article({ id: new URL(`/posts/${post.id}`, ctx.url), summary: post.title, content: post.content, }), }) ); return { items }; }); ``` If you try to access the outbox collection of an actor, the server will respond with a JSON object that contains the activities that the actor has sent: ```http GET /users/alice/outbox HTTP/1.1 Accept: application/activity+json Host: localhost ``` ```http HTTP/1.1 200 OK Content-Type: application/activity+json Vary: Accept { "@context": "https://www.w3.org/ns/activitystreams", "items": [ { "id": "http://localhost/posts/123#activity", "type": "Create", "actor": "http://localhost/users/alice", "object": { "id": "http://localhost/posts/123", "type": "Article", "summary": "Hello, world!", "content": "This is the first post." } }, // More items... ] } ``` As you can expect, the server responds with the whole activities that the actor has sent without any pagination. In the real world, you should implement pagination for the outbox collection. In the next section, we'll see how to implement pagination for a collection. [outbox]: https://www.w3.org/TR/activitypub/#outbox [URI Template]: https://datatracker.ietf.org/doc/html/rfc6570 ### Page A collection page is a subset of a collection. For example, the first page of the outbox collection is a collection page that contains the first few items of the outbox collection. Each page has its own URI which is determined by a unique cursor, and links to the next and previous pages if they exist. No random access is allowed for a collection page; you can only access the next and previous pages. Fedify abstracts the concept of a collection page as cursor-based pagination. The cursor is a string that represents the position in the collection. It can be either an opaque token or an offset numeric value; the way to interpret it is up to the server implementation. If your database system supports cursor-based pagination ([Deno KV], for example), you can just use the cursor that the database system provides as is. If your database system supports only offset-based pagination (the most relational databases), you can use the offset as the cursor. Although it's omitted in the previous example, there is the third parameter to a callback that `~Federatable.setOutboxDispatcher()` method takes: the cursor. When the request is for a collection page, the cursor is passed to the callback as the third parameter. When the request is for a whole collection, the cursor is `null` (that the previous example assumes). Here's an example of how to implement collection pages for the outbox collection with assuming that the database system supports cursor-based pagination: ```typescript twoslash import { Article, Create, type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents a post. */ interface Post { /** * The ID of the post. */ id: string; /** * The title of the post. */ title: string; /** * The content of the post. */ content: string; } /** * A hypothetical type that represents the result set of the posts. */ interface PostResultSet { /** * The posts that the actor has sent. */ posts: Post[]; /** * The next cursor that represents the position of the next page. */ nextCursor: string | null; /** * Whether the current page is the last page. */ last: boolean; } /** * A hypothetical type that represents the options for * the `getPostsByUserId` function. */ interface GetPostsByUserIdOptions { /** * The cursor that represents the position of the current page. */ cursor?: string | null; /** * The number of items per page. */ limit: number; } /** * A hypothetical function that returns the posts that an actor has sent. * @param userId The actor's identifier. * @returns The result set that contains the posts, the next cursor, and whether * the current page is the last page. */ function getPostsByUserId( userId: string, options: GetPostsByUserIdOptions, ): PostResultSet { return { posts: [], nextCursor: null, last: true }; } // ---cut-before--- federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => { // If a whole collection is requested, returns nothing as we prefer // collection pages over the whole collection: if (cursor == null) return null; // Work with the database to find the activities that the actor has sent // (the following `getPostsByUserId` is a hypothetical function): const { posts, nextCursor, last } = await getPostsByUserId(identifier, { cursor, limit: 10, }); // Turn the posts into `Create` activities: const items = posts.map(post => new Create({ id: new URL(`/posts/${post.id}#activity`, ctx.url), actor: ctx.getActorUri(identifier), object: new Article({ id: new URL(`/posts/${post.id}`, ctx.url), summary: post.title, content: post.content, }), }) ); return { items, // If `last` is `true`, it means that the current page is the last page: nextCursor: last ? null : nextCursor, } }); ``` In the above example, the hypothetical `getPostsByUserId()` function returns the `nextCursor` along with the `items`. The `nextCursor` represents the position of the next page, which is provided by the database system. If the `last` is `true`, it means that the current page is the last page, so the `nextCursor` is `null`. [Deno KV]: https://deno.com/kv ### First cursor The first cursor is a special cursor that represents the beginning of the collection. It's used to initialize a traversal of the collection. The first cursor is `null` if the collection is empty. The value for the first cursor is determined by `~CollectionCallbackSetters.setFirstCursor()` method: ```typescript twoslash import { Article, Create, type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents a post. */ interface Post { /** * The ID of the post. */ id: string; /** * The title of the post. */ title: string; /** * The content of the post. */ content: string; } /** * A hypothetical type that represents the result set of the posts. */ interface PostResultSet { /** * The posts that the actor has sent. */ posts: Post[]; /** * The next cursor that represents the position of the next page. */ nextCursor: string | null; /** * Whether the current page is the last page. */ last: boolean; } /** * A hypothetical type that represents the options for * the `getPostsByUserId` function. */ interface GetPostsByUserIdOptions { /** * The cursor that represents the position of the current page. */ cursor?: string | null; /** * The number of items per page. */ limit: number; } /** * A hypothetical function that returns the posts that an actor has sent. * @param userId The actor's identifier. * @returns The result set that contains the posts, the next cursor, and whether * the current page is the last page. */ function getPostsByUserId( userId: string, options: GetPostsByUserIdOptions, ): PostResultSet { return { posts: [], nextCursor: null, last: true }; } // ---cut-before--- // The number of items per page: const window = 10; federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => { if (cursor == null) return null; // The following `getPostsByUserId` is a hypothetical function: const { posts, nextCursor, last } = await getPostsByUserId( identifier, cursor === "" ? { limit: window } : { cursor, limit: window } ); // Turn the posts into `Create` activities: const items = posts.map(post => new Create({ id: new URL(`/posts/${post.id}#activity`, ctx.url), actor: ctx.getActorUri(identifier), object: new Article({ id: new URL(`/posts/${post.id}`, ctx.url), summary: post.title, content: post.content, }), }) ); return { items, nextCursor: last ? null : nextCursor } }) .setFirstCursor(async (ctx, identifier) => { // Let's assume that an empty string represents the beginning of the // collection: return ""; // Note that it's not `null`. }); ``` In the above example, the first cursor is an empty string. When the first cursor is requested, the server queries the database *without any cursor* to get the first few items of the collection. Of course, since the first cursor is also an opaque token, you can use any string as the first cursor. > \[!NOTE] > The first cursor is an enabler of the pagination. If you don't set the first > cursor, the collection is not considered as paginated, and the server will > respond with the whole collection without any pagination. ### Counter As the name suggests, the counter is a callback that counts the *total* number of items in the collection, which is useful for the client to show, for example, the total number of articles a user has posted. The following example shows how to implement the counter for the outbox collection: ```typescript twoslash // @noErrors: 2345 import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that counts the number of posts that an actor has * sent. * @param userId The actor's identifier. * @returns The number of posts that the actor has sent. */ async function countPostsByUserId(userId: string): Promise { return 0; } // ---cut-before--- federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => { // Omitted for brevity. }) .setCounter(async (ctx, identifier) => { // The following `countPostsByUserId` is a hypothetical function: return await countPostsByUserId(identifier); }); ``` > \[!TIP] > The counter can return either a `number` or a `bigint`. ### Last cursor The last cursor is a special cursor that represents the end of the collection. With the last cursor and `prevCursor`, the client can traverse the collection backwards. Since not all database systems support backward pagination, the last cursor is optional. If you don't set the last cursor, the client can only traverse the collection forwards, which is fine in most cases. So, the below example assumes that the database system supports offset-based pagination, which is easy to implement backward pagination: ```typescript twoslash import { Article, Create, type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents a post. */ interface Post { /** * The ID of the post. */ id: string; /** * The title of the post. */ title: string; /** * The content of the post. */ content: string; } /** * A hypothetical type that represents the options for * the `getPostsByUserId` function. */ interface GetPostsByUserIdOptions { /** * The offset of the current page. */ offset: number; /** * The number of items per page. */ limit: number; } /** * A hypothetical function that returns the posts that an actor has sent. * @param userId The actor's identifier. * @returns The result set that contains the posts, the next cursor, and whether * the current page is the last page. */ function getPostsByUserId( userId: string, options: GetPostsByUserIdOptions, ): Post[] { return []; } /** * A hypothetical function that counts the number of posts that an actor has * sent. * @param userId The actor's identifier. * @returns The number of posts that the actor has sent. */ async function countPostsByUserId(userId: string): Promise { return 0; } // ---cut-before--- // The number of items per page: const window = 10; federation .setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => { if (cursor == null) return null; // Here we use the offset numeric value as the cursor: const offset = parseInt(cursor); // The following `getPostsByUserId` is a hypothetical function: const posts = await getPostsByUserId( identifier, { offset, limit: window } ); // Turn the posts into `Create` activities: const items = posts.map(post => new Create({ id: new URL(`/posts/${post.id}#activity`, ctx.url), actor: ctx.getActorUri(identifier), object: new Article({ id: new URL(`/posts/${post.id}`, ctx.url), summary: post.title, content: post.content, }), }) ); return { items, nextCursor: (offset + window).toString() } }) .setFirstCursor(async (ctx, identifier) => "0") .setLastCursor(async (ctx, identifier) => { // The following `countPostsByUserId` is a hypothetical function: const total = await countPostsByUserId(identifier); // The last cursor is the offset of the last page: return (total - (total % window)).toString(); }); ``` ### Constructing outbox collection URIs To construct an outbox collection URI, you can use the `Context.getOutboxUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the outbox collection of the actor. The following shows how to construct an outbox collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getOutboxUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getOutboxUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getOutboxUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getOutboxUri()` method does not guarantee that the outbox > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Inbox *This API is available since Fedify 0.11.0.* The inbox collection is similar to the outbox collection, but it's a collection of activities that an actor has received. Cursors and counters for the inbox 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 an inbox collection: ```typescript twoslash import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that returns the activities that an actor has * received. * @param userId The actor's identifier. * @returns The activities that the actor has received. */ async function getInboxByUserId(userId: string): Promise { return []; } /** * A hypothetical function that counts the number of activities that an actor * has received. * @param userId The actor's identifier. * @returns The number of activities that the actor has received. */ async function countInboxByUserId(userId: string): Promise { return 0; } // ---cut-before--- import { Activity } from "@fedify/fedify"; federation .setInboxDispatcher("/users/{identifier}/inbox", async (ctx, identifier) => { // Work with the database to find the activities that the actor has received // (the following `getInboxByUserId` is a hypothetical function): const items: Activity[] = await getInboxByUserId(identifier); return { items }; }) .setCounter(async (ctx, identifier) => { // The following `countInboxByUserId` is a hypothetical function: return await countInboxByUserId(identifier); }); ``` > \[!NOTE] > The path for the inbox collection dispatcher must match the path for the inbox > listeners. ### Constructing inbox collection URIs To construct an inbox collection URI, you can use the `Context.getInboxUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the inbox collection of the actor. The following shows how to construct an inbox collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getInboxUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getInboxUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getInboxUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getInboxUri()` method does not guarantee that the inbox > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Following The following collection consists of the actors that an actor is following. The following collection is similar to the outbox collection, but it's a collection of actors instead of activities. More specifically, the following collection can consist of `Actor` objects or `URL` objects that represent the actors. Cursors and counters for the following 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 following collection: ```typescript twoslash import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents an actor in the database. */ interface User { /** * The URI of the actor. */ uri: string; } /** * A hypothetical type that represents the result set of the actors that * an actor is following. */ interface ResultSet { /** * The actors that the actor is following. */ users: User[]; /** * The next cursor that represents the position of the next page. */ nextCursor: string | null; /** * Whether the current page is the last page. */ last: boolean; } /** * A hypothetical function that returns the actors that an actor is following. * @param userId The actor's identifier. * @param options The options for the query. * @returns The actors that the actor is following, the next cursor, and whether * the current page is the last page. */ async function getFollowingByUserId( identifier: string, options: { cursor?: string | null; limit: number }, ): Promise { return { users: [], nextCursor: null, last: true }; } // ---cut-before--- federation .setFollowingDispatcher("/users/{identifier}/following", async (ctx, identifier, cursor) => { // If a whole collection is requested, returns nothing as we prefer // collection pages over the whole collection: if (cursor == null) return null; // Work with the database to find the actors that the actor is following // (the below `getFollowingByUserId` is a hypothetical function): const { users, nextCursor, last } = await getFollowingByUserId( identifier, cursor === "" ? { limit: 10 } : { cursor, limit: 10 } ); // Turn the users into `URL` objects: const items = users.map(actor => new URL(actor.uri)); return { items, nextCursor: last ? null : nextCursor } }) // The first cursor is an empty string: .setFirstCursor(async (ctx, identifier) => ""); ``` ### Constructing following collection URIs To construct a following collection URI, you can use the `Context.getFollowingUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the following collection of the actor. The following shows how to construct a following collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFollowingUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getFollowingUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFollowingUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > The `Context.getFollowingUri()` method does not guarantee that the following > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Followers The followers collection is very similar to the following collection, but it's a collection of actors that are following the actor. The followers collection has to consist of `Recipient` objects that represent the actors. The below example shows how to construct a followers collection: ```typescript twoslash import type { Federation, Recipient } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents an actor in the database. */ interface User { /** * The URI of the actor. */ uri: string; /** * The inbox URI of the actor. */ inboxUri: string; } /** * A hypothetical type that represents the result set of the actors that * are following an actor. */ interface ResultSet { /** * The actors that are following the actor. */ users: User[]; /** * The next cursor that represents the position of the next page. */ nextCursor: string | null; /** * Whether the current page is the last page. */ last: boolean; } /** * A hypothetical type that represents the options for * the `getFollowersByUserId` function. */ interface GetFollowersByUserIdOptions { /** * The cursor that represents the position of the current page. */ cursor?: string | null; /** * The number of items per page. If `null`, the entire collection is returned. */ limit?: number | null; } /** * A hypothetical function that returns the actors that are following an actor. * @param userId The actor's identifier. * @param options The options for the query. * @returns The actors that are following the actor, the next cursor, and * whether the current page is the last page. */ async function getFollowersByUserId( userId: string, options: GetFollowersByUserIdOptions = {}, ): Promise { return { users: [], nextCursor: null, last: true }; } // ---cut-before--- federation .setFollowersDispatcher( "/users/{identifier}/followers", async (ctx, identifier, cursor) => { // If a whole collection is requested, returns nothing as we prefer // collection pages over the whole collection: if (cursor == null) return null; // Work with the database to find the actors that are following the actor // (the below `getFollowersByUserId` is a hypothetical function): const { users, nextCursor, last } = await getFollowersByUserId( identifier, cursor === "" ? { limit: 10 } : { cursor, limit: 10 } ); // Turn the users into `Recipient` objects: const items: Recipient[] = users.map(actor => ({ id: new URL(actor.uri), inboxId: new URL(actor.inboxUri), })); return { items, nextCursor: last ? null : nextCursor }; } ) // The first cursor is an empty string: .setFirstCursor(async (ctx, identifier) => ""); ``` > \[!TIP] > > Every `Actor` object is also a `Recipient` object, so you can use the `Actor` > object as the `Recipient` object. ### One-shot followers collection for gathering recipients When you invoke `Context.sendActivity()` method with setting the `recipients` parameter to `"followers"`, Fedify automatically gathers the recipients from the followers collection. In this case, the followers collection dispatcher is not called by remote servers, but it's called in the same process. Therefore, you don't have much merit to paginate the followers collection, but instead you would want to gather all the followers at once. Under the hood, the `Context.sendActivity()` method tries to gather the recipients by calling the followers collection dispatcher with the `cursor` parameter set to `null`. However, if the followers collection dispatcher returns `null`, the method treats it as a signal that the followers collection is always paginated, and it gather the recipients by paginating the followers collection with multiple invocation of the followers collection dispatcher. If the followers collection dispatcher returns an object that contains the entire followers collection, the method gathers the recipients at once. Therefore, if you use `"followers"` as the `recipients` parameter of the `Context.sendActivity()` method, you should return the entire followers collection when the `cursor` parameter is `null`: ```typescript{5-17} twoslash import type { Federation, Recipient } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents an actor in the database. */ interface User { /** * The URI of the actor. */ uri: string; /** * The inbox URI of the actor. */ inboxUri: string; } /** * A hypothetical type that represents the result set of the actors that * are following an actor. */ interface ResultSet { /** * The actors that are following the actor. */ users: User[]; /** * The next cursor that represents the position of the next page. */ nextCursor: string | null; /** * Whether the current page is the last page. */ last: boolean; } /** * A hypothetical type that represents the options for * the `getFollowersByUserId` function. */ interface GetFollowersByUserIdOptions { /** * The cursor that represents the position of the current page. */ cursor?: string | null; /** * The number of items per page. If `null`, the entire collection is returned. */ limit?: number | null; } /** * A hypothetical function that returns the actors that are following an actor. * @param userId The actor's identifier. * @param options The options for the query. * @returns The actors that are following the actor, the next cursor, and * whether the current page is the last page. */ async function getFollowersByUserId( userId: string, options: GetFollowersByUserIdOptions = {}, ): Promise { return { users: [], nextCursor: null, last: true }; } // ---cut-before--- federation .setFollowersDispatcher( "/users/{identifier}/followers", async (ctx, identifier, cursor) => { // If a whole collection is requested, returns the entire collection // instead of paginating it, as we prefer one-shot gathering: if (cursor == null) { // Work with the database to find the actors that are following the actor // (the below `getFollowersByUserId` is a hypothetical function): const { users } = await getFollowersByUserId(identifier); return { items: users.map(actor => ({ id: new URL(actor.uri), inboxId: new URL(actor.inboxUri), })), }; } const { users, nextCursor, last } = await getFollowersByUserId( identifier, cursor === "" ? { limit: 10 } : { cursor, limit: 10 } ); // Turn the users into `Recipient` objects: const items: Recipient[] = users.map(actor => ({ id: new URL(actor.uri), inboxId: new URL(actor.inboxUri), })); return { items, nextCursor: last ? null : nextCursor }; } ) // The first cursor is an empty string: .setFirstCursor(async (ctx, identifier) => ""); ``` > \[!CAUTION] > The common pitfall is that the followers collection dispatcher returns > the first page of the followers collection when the `cursor` parameter is > `null`. If the followers collection dispatcher returns only the first page > when the `cursor` parameter is `null`, the `Context.sendActivity()` method > will treat it as the entire followers collection, and it will not gather > the rest of the followers collection. Therefore, it will send the activity > only to the followers in the first page. Watch out for this pitfall. ### Filtering by server *This API is available since Fedify 0.8.0.* The followers collection can be filtered by the base URI of the actor URIs. It can be useful to filter by a remote server to synchronize the followers collection with it. > \[!TIP] > However, the filtering is optional, and you can skip it if you don't need > [followers collection synchronization](./send.md#followers-collection-synchronization). In order to filter the followers collection by the server, you need to let your followers collection dispatcher be aware of the fourth argument: the base URI of the actor URIs to filter in. The base URI consists of the protocol, the authority (the host and the port), and the root path of the actor URIs. When the base URI is not `null`, the dispatcher should return only the actors whose URIs start with the base URI. If the base URI is `null`, the dispatcher should return all the actors. The following example shows how to filter the followers collection by the server: ```typescript{8-11} twoslash import type { Federation, Recipient } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical type that represents an actor in the database. */ interface User { /** * The URI of the actor. */ uri: URL; /** * The inbox URI of the actor. */ inboxUri: URL; } /** * A hypothetical function that returns the actors that are following an actor. * @param userId The actor's identifier. * @returns The actors that are following the actor. */ async function getFollowersByUserId(userId: string): Promise { return []; } // ---cut-before--- federation .setFollowersDispatcher( "/users/{identifier}/followers", async (ctx, identifier, cursor, baseUri) => { // Work with the database to find the actors that are following the actor // (the below `getFollowersByUserId` is a hypothetical function): let users = await getFollowersByUserId(identifier); // Filter the actors by the base URI: if (baseUri != null) { users = users.filter(actor => actor.uri.href.startsWith(baseUri.href)); } // Turn the users into `Recipient` objects: const items: Recipient[] = users.map(actor => ({ id: actor.uri, inboxId: actor.inboxUri, })); return { items }; } ); ``` > \[!NOTE] > 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. ### Constructing followers collection URIs To construct a followers collection URI, you can use the `Context.getFollowersUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the followers collection of the actor. The following shows how to construct a followers collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFollowersUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getFollowersUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFollowersUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getFollowersUri()` method does not guarantee that the followers > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Liked *This API is available since Fedify 0.15.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 `Object`s instead of `Activity` objects. 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 twoslash import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that returns the objects that an actor has liked. * @param userId The actor's identifier. * @returns The objects that the actor has liked. */ async function getLikedByUserId(userId: string): Promise { return []; } // ---cut-before--- import type { Object } from "@fedify/fedify"; federation .setLikedDispatcher("/users/{identifier}/liked", async (ctx, identifier, cursor) => { // Work with the database to find the objects that the actor has liked // (the below `getLikedPostsByUserId` is a hypothetical function): const items: Object[] = await getLikedByUserId(identifier); return { items }; }); ``` Or you can yield the object URIs instead of the objects: ```typescript twoslash import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that returns the objects that an actor has liked. * @param userId The actor's identifier. * @returns The objects that the actor has liked. */ async function getLikedByUserId(userId: string): Promise { return []; } // ---cut-before--- import type { Object } from "@fedify/fedify"; federation .setLikedDispatcher("/users/{identifier}/liked", async (ctx, identifier, cursor) => { // Work with the database to find the objects that the actor has liked // (the below `getLikedPostsByUserId` is a hypothetical function): const objects: Object[] = await getLikedByUserId(identifier); // Turn the objects into `URL` objects: const items: URL[] = objects.map(obj => obj.id).filter(id => id != null); return { items }; }); ``` ### Constructing liked collection URIs To construct a liked collection URI, you can use the `Context.getLikedUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the liked collection of the actor. The following shows how to construct a liked collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getLikedUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getLikedUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getLikedUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getLikedUri()` method does not guarantee that the liked > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Featured *This API is available since Fedify 0.11.0.* The featured collection is a collection of objects that an actor has featured on top of their profile, i.e., pinned statuses. The featured collection is similar to the outbox collection, but it's a collection of any ActivityStreams objects instead of activities. Cursor and counter for the featured 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 featured collection: ```typescript twoslash import type { Object, Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that returns the objects that an actor has featured. * @param userId The actor's identifier. * @returns The objects that the actor has featured. */ async function getFeaturedByUserId(userId: string): Promise { return []; } // ---cut-before--- federation .setFeaturedDispatcher("/users/{identifier}/featured", async (ctx, identifier, cursor) => { // Work with the database to find the objects that the actor has featured // (the below `getFeaturedPostsByUserId` is a hypothetical function): const items = await getFeaturedByUserId(identifier); return { items }; }); ``` ### Constructing featured collection URIs To construct a featured collection URI, you can use the `Context.getFeaturedUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the featured collection of the actor. The following shows how to construct a featured collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFeaturedUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getFeaturedUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFeaturedUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getFeaturedUri()` method does not guarantee that the featured > collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check if > the identifier is valid before calling the method. ## Featured tags *This API is available since Fedify 0.11.0.* The featured tags collection is a collection of tags that an actor has featured on top of their profile. The featured tags collection is similar to the featured collection, but it's a collection of `Hashtag` objects instead of any ActivityStreams objects. Cursor and counter for the featured tags 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 featured tags collection: ```typescript twoslash import { Hashtag, type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; /** * A hypothetical function that returns the tags that an actor has featured. * @param userId The actor's identifier. * @returns The tags that the actor has featured. */ async function getFeaturedTagsByUserId(userId: string): Promise { return []; } // ---cut-before--- federation .setFeaturedTagsDispatcher("/users/{identifier}/tags", async (ctx, identifier, cursor) => { // Work with the database to find the tags that the actor has featured // (the below `getFeaturedTagsByUserId` is a hypothetical function): const hashtags = await getFeaturedTagsByUserId(identifier); const items = hashtags.map(hashtag => new Hashtag({ href: new URL(`/tags/${encodeURIComponent(hashtag)}`, ctx.url), name: `#${hashtag}`, }) ); return { items }; }); ``` ### Constructing featured tags collection URIs To construct a featured tags collection URI, you can use the `Context.getFeaturedTagsUri()` method. This method takes the actor's identifier and returns the dereferenceable URI of the featured tags collection of the actor. The following shows how to construct a featured tags collection URI of an actor named `"alice"`: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFeaturedTagsUri("alice") ``` If you [decouple the WebFinger username from the actor's identifier](./actor.md#decoupling-actor-uris-from-webfinger-usernames), you should pass the identifier that is used in the [actor dispatcher](./actor.md) to the `Context.getFeaturedTagsUri()` method, not the WebFinger username: ```typescript twoslash import type { Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- ctx.getFeaturedTagsUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") ``` > \[!NOTE] > > The `Context.getFeaturedTagsUri()` method does not guarantee that the featured > tags collection actually exists. It only constructs a URI based on the given > identifier, which may respond with `404 Not Found`. Make sure to check > if the identifier is valid before calling the method. --- --- url: /manual/context.md description: >- The Context object is a container that holds the information of the current request. This section explains the key features of the Context object. --- # Context The `Context` object is a container that holds the information of the current request. It is passed to various callback functions that are registered to the `Federation` object, and also can be gathered from the outside of the callbacks. The key features of the `Context` object are as follows: * Carrying [`TContextData`](./federation.md#tcontextdata) * [Building the object URIs](#building-the-object-uris) (e.g., actor URIs, shared inbox URI) * [Dispatching Activity Vocabulary objects](#dispatching-objects) * Getting the current HTTP request * [Enqueuing an outgoing activity](#enqueuing-an-outgoing-activity) * [Getting a `DocumentLoader`](#getting-a-documentloader) * [Looking up remote objects](#looking-up-remote-objects) * [NodeInfo client](./nodeinfo.md#nodeinfo-client) ## Where to get a `Context` object You can get a `Context` object from the first parameter of the most of callbacks that are registered to the `Federation` object. The following shows a few callbacks that take a `Context` object as the first parameter: * [Actor dispatcher](./actor.md) * [Inbox listeners](./inbox.md) * [Outbox collection dispatcher](./collections.md#outbox) * [Inbox collection dispatcher](./collections.md#inbox) * [Following collection dispatcher](./collections.md#following) * [Followers collection dispatcher](./collections.md#followers) * [Liked collection dispatcher](./collections.md#liked) * [Featured collection dispatcher](./collections.md#featured) * [Featured tags collection dispatcher](./collections.md#featured-tags) * [NodeInfo dispatcher](./nodeinfo.md) Those are not all; there are more callbacks that take a `Context` object. You can also get a `Context` object from the `Federation` object by calling the `~Federation.createContext()` method. The following shows an example: ```typescript twoslash // @noErrors import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- import { federation } from "../federation.ts"; // Import the `Federation` object export async function handler(request: Request) { const ctx = federation.createContext(request, undefined); // [!code highlight] // Work with the `ctx` object... }; ``` ## Getting the base URL *This API is available since Fedify 0.12.0.* The `Context` object has properties to get the base URL of the current request: | Property | Description | Value example | |---------------------------|----------------------------------------------------------|----------------------------| | `Context.hostname` | A hostname | `"example.com"` | | `Context.host` | A hostname followed by an optional port | `"example.com:88"` | | `Context.origin` | A scheme followed by a host | `"https://example.com:88"` | | `Context.canonicalOrigin` | An explicitly configured `~FederationOptions.origin`\[^1] | `"https://example.com"` | For `RequestContext`, there is an additional property named `~RequestContext.url` that contains the full URL of the current request. \[^1]: If no canonical origin is explicitly configured, it is the same as the `Context.origin`. See also the [*Explicitly setting the canonical origin* section](./federation.md#explicitly-setting-the-canonical-origin) in the *Federation* document. ## Building the object URIs The `Context` object has a few methods to build the object URIs. The following shows the methods: * `~Context.getNodeInfoUri()` * `~Context.getActorUri()` * `~Context.getObjectUri()` * `~Context.getInboxUri()` * `~Context.getOutboxUri()` * `~Context.getFollowingUri()` * `~Context.getFollowersUri()` * `~Context.getLikedUri()` * `~Context.getFeaturedUri()` * `~Context.getFeaturedTagsUri()` You could hard-code the URIs, but it is better to use those methods to build the URIs because the URIs are subject to change in the future. Here's an example of using the `~Context.getActorUri()` method in the actor dispatcher: ```typescript twoslash import { type Federation, Person } from "@fedify/fedify"; const federation = null as unknown as Federation; interface User { } const user: User | null = true ? { } : null; // ---cut-before--- federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // Work with the database to find the actor by the identifier. if (user == null) return null; return new Person({ id: ctx.getActorUri(identifier), // [!code highlight] preferredUsername: identifier, // Many more properties... }); }); ``` On the other way around, you can use the `~Context.parseUri()` method to determine the type of the URI and extract the identifier or other values from the URI. ## Enqueuing an outgoing activity The `Context` object can enqueue an outgoing activity to the actor's outbox by calling the `~Context.sendActivity()` method. The following shows an example in an [inbox listener](./inbox.md): ```typescript{12-16} twoslash import { type Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- import { Accept, Follow } from "@fedify/fedify"; federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .on(Follow, async (ctx, follow) => { // In order to send an activity, we need the identifier of the sender actor: if (follow.objectId == null) return; const parsed = ctx.parseUri(follow.objectId); if (parsed?.type !== "actor") return; const recipient = await follow.getActor(ctx); if (recipient == null) return; await ctx.sendActivity( { identifier: parsed.identifier }, // sender recipient, new Accept({ actor: follow.objectId, object: follow }), ); }); ``` For more information about this topic, see the [*Sending activities* section](./send.md). > \[!NOTE] > The `~Context.sendActivity()` method works only if the [key pairs dispatcher] > is registered to the `Federation` object. If the key pairs dispatcher is not > registered, the `~Context.sendActivity()` method throws an error. > \[!TIP] > Why do you need to enqueue an outgoing activity, instead of directly sending > the activity to the recipient's inbox? The reason is that in distributed > systems, we need to consider the delivery failure. If the delivery fails, > the system needs to retry the delivery. Delivery failure can happen for > various reasons, such as network failure, recipient server failure, and so on. > > Anyway, you don't have to worry about the delivery failure because the > Fedify handles the delivery failure by enqueuing the outgoing > activity to the actor's outbox and retrying the delivery on failure. [key pairs dispatcher]: ./actor.md#public-keys-of-an-actor ## Dispatching objects *This API is available since Fedify 0.7.0.* 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 twoslash import { type Federation, Update } from "@fedify/fedify"; const federation = null as unknown as Federation; const request = new Request(""); const identifier: string = ""; // ---cut-before--- const ctx = federation.createContext(request, undefined); const actor = await ctx.getActor(identifier); // [!code highlight] if (actor != null) { await ctx.sendActivity( { identifier }, "followers", new Update({ actor: actor.id, object: actor }), ); } ``` > \[!NOTE] > The `RequestContext.getActor()` method is only available when the actor > 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 twoslash import { type Federation, Note } from "@fedify/fedify"; const federation = null as unknown as Federation; const request = new Request(""); const identifier: string = ""; const id: string = ""; // ---cut-before--- const ctx = federation.createContext(request, undefined); const note = await ctx.getObject(Note, { identifier, id }); // [!code highlight] ``` ## Getting a `DocumentLoader` The `Context.documentLoader` property carries a `DocumentLoader` object that is specified in the `Federation` constructor. It is used to load remote documents and contexts in the JSON-LD format. There are a few methods to take a `DocumentLoader` as an option in vocabulary API: * [`fromJsonLd()` static method](./vocab.md#json-ld) * [`toJsonLd()` method](./vocab.md#json-ld) * [`get*()` dereferencing accessors](./vocab.md#object-ids-and-remote-objects) * [`lookupObject()` function](./vocab.md#looking-up-remote-objects) All of those methods take options in the form of `{ documentLoader?: DocumentLoader, contextLoader?: DocumentLoader }` which is compatible with `Context`. So you can just pass a `Context` object to those methods: ```typescript twoslash import { type Context, Object } from "@fedify/fedify"; const ctx = null as unknown as Context; const jsonLd: unknown = {}; // ---cut-before--- const object = await Object.fromJsonLd(jsonLd, ctx); const json = await object.toJsonLd(ctx); ``` ## Getting an authenticated `DocumentLoader` *This API is available since Fedify 0.4.0.* Sometimes you need to load a remote document which requires authentication, such as an actor's following collection that is configured as private. In such cases, you can use the `Context.getDocumentLoader()` method to get an authenticated `DocumentLoader` object. The following shows an example: ```typescript twoslash import { type Actor, type Context, Person } from "@fedify/fedify"; const ctx = null as unknown as Context; const actor = new Person({}) as Actor; // ---cut-before--- const documentLoader = await ctx.getDocumentLoader({ identifier: "2bd304f9-36b3-44f0-bf0b-29124aafcbb4", }); const following = await actor.getFollowing({ documentLoader }); ``` In the above example, the `getFollowing()` method takes the `documentLoader` which is authenticated as the actor with an identifier of `2bd304f9-36b3-44f0-bf0b-29124aafcbb4`. If the `actor` allows actor `2bd304f9-36b3-44f0-bf0b-29124aafcbb4` to see the following collection, the `getFollowing()` method returns the following collection. > \[!TIP] > Inside a personal inbox listener, the `Context.documentLoader` property is > automatically set to an authenticated `DocumentLoader` object that is > identified by the inbox owner's key. So you don't need to call the > `Context.getDocumentLoader()` method in the personal inbox listener, > but just passing the `Context` object to dereferencing accessors is enough. > > See the [*`Context.documentLoader` on an inbox listener* > section](./inbox.md#context-documentloader-on-an-inbox-listener) for details. ## Document loader vs. context loader Both a document loader and a context loader are represented by `DocumentLoader` type, but they are used for different purposes: * A document loader is used to load remote documents, such as an actor's profile document, an object document, and so on. * A context loader is used to load remote contexts, such as the ActivityStreams context, the W3C security context, and so on. Sometimes a document loader needs to be authenticated to load a remote document which requires authorization, but a context loader mostly needs to be highly cached and doesn't require authorization. ## Looking up remote objects *This API is available since Fedify 0.15.0.* > \[!TIP] > In most cases, you don't need to look up remote objects explicitly. > Instead, you can use the dereferencing accessors to fetch the remote objects > implicitly. > > For example, you can get the `object` from an `Activity` object directly: > > ```typescript twoslash > import { Activity } from "@fedify/fedify"; > const activity = new Activity({}); > // ---cut-before--- > const object = await activity.getObject(); > ``` > > … instead of: > > ```typescript twoslash > import { Activity, type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context; > const activity = new Activity({}); > // ---cut-before--- > const object = activity.objectId == null > ? null > : await ctx.lookupObject(activity.objectId); > ``` Suppose your app has a search box that allows the user to look up a fediverse user by the handle or a post by the URI. In such cases, you need to look up the object from a remote server that your app haven't interacted with yet. The `Context.lookupObject()` method plays a role in such cases. The following shows an example of looking up an actor object from the handle: ```typescript twoslash import { type Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const actor = await ctx.lookupObject("@hongminhee@todon.eu"); ``` In the above example, the `~Context.lookupObject()` method queries the remote server's WebFinger endpoint to get the actor's URI from the handle, and then fetches the actor object from the URI. > \[!TIP] > The `~Context.lookupObject()` method accepts a fediverse handle without > prefix `@` as well: > > ```typescript twoslash > import { type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context; > // ---cut-before--- > const actor = await ctx.lookupObject("hongminhee@todon.eu"); > ``` > > Also an `acct:` URI: > > ```typescript twoslash > import { type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context; > // ---cut-before--- > const actor = await ctx.lookupObject("acct:hongminhee@todon.eu"); > ``` The `~Context.lookupObject()` method is not limited to the actor object. It can look up any object in the Activity Vocabulary. For example the following shows an example of looking up a `Note` object from the URI: ```typescript twoslash import { type Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const note = await ctx.lookupObject( "https://todon.eu/@hongminhee/112060633798771581" ); ``` > \[!NOTE] > Some objects require authentication to look up, such as a `Note` object with > a visibility of followers-only. In such cases, you need to use > the `Context.getDocumentLoader()` method to get an authenticated > `DocumentLoader` object. The `~Context.lookupObject()` method takes the > `documentLoader` option to specify the method to fetch the remote object: > > ```typescript twoslash > import { type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context; > // ---cut-before--- > const documentLoader = await ctx.getDocumentLoader({ identifier: "john" }); > const note = await ctx.lookupObject("...", { documentLoader }); > ``` > > See the [*Getting an authenticated > `DocumentLoader`*](#getting-an-authenticated-documentloader) > section for details. ## WebFinger lookups *This API is available since Fedify 1.6.0.* The `Context` provides a dedicated method for WebFinger lookups when you need to find information about accounts and resources across federated networks. The `~Context.lookupWebFinger()` method allows you to query a remote server's WebFinger endpoint directly: ```typescript twoslash import { type Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const webfingerData = await ctx.lookupWebFinger("acct:fedify@hollo.social"); ``` If the lookup fails or the account doesn't exist, the method returns `null`. The returned WebFinger document contains links to various resources associated with the account, such as profile pages, ActivityPub actor URIs, and more: ```typescript twoslash import { type Context } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const webfingerData = await ctx.lookupWebFinger("acct:fedify@hollo.social"); // Find the ActivityPub actor URI const activityPubActorLink = webfingerData?.links?.find(link => link.rel === "self" && link.type === "application/activity+json" ); if (activityPubActorLink?.href) { const actor = await ctx.lookupObject(activityPubActorLink.href); // Work with the actor... } ``` > \[!NOTE] > In most cases, you can use the higher-level `~Context.lookupObject()` method > which automatically performs WebFinger lookups when given a handle. > Use `~Context.lookupWebFinger()` when you need the raw WebFinger data or > want more direct control over the lookup process. ## Traversing remote collections *This API is available since Fedify 1.1.0.* Sometimes you need to traverse a remote collection from the beginning to the end, such as an actor's outbox, an actor's followers collection, and so on. The `Context.traverseCollection()` method plays a role in such cases. The following shows an example of traversing an actor's outbox: ```typescript twoslash import { type Context, isActor } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const actor = await ctx.lookupObject("@hongminhee@fosstodon.org"); if (isActor(actor)) { const outbox = await actor.getOutbox(); if (outbox != null) { for await (const activity of ctx.traverseCollection(outbox)) { console.log(activity); } } } ``` ## Replacing the context data *This API is available since Fedify 1.6.0.* You can replace the context data by calling the `Context.clone()` method. This is useful when you want to create a new context based on the existing one but with different data. The following shows an example of replacing the context data: ```typescript twoslash import { type Context } from "@fedify/fedify"; const ctx = null as unknown as Context<{ foo: string; bar: number }>; // ---cut-before--- const newCtx = ctx.clone({ ...ctx.data, foo: "new value" }); ``` --- --- url: /tutorial/microblog.md description: >- In this tutorial, we will build a small microblog that implements the ActivityPub protocol, similar to Mastodon or Misskey, using Fedify, an ActivityPub server framework. --- # Creating your own federated microblog > \[!TIP] > > This tutorial is also available in the following languages: [한국어] (Korean) > and [日本語] (Japanese). In this tutorial, we will build a small [microblog] that implements the ActivityPub protocol, similar to [Mastodon] or [Misskey], using [Fedify], an ActivityPub server framework. This tutorial will focus more on how to use Fedify rather than understanding its underlying operating principles. If you have any questions, suggestions, or feedback, please feel free to join our [Matrix chat space] or [Discord server] or [GitHub Discussions]. [한국어]: https://hackmd.io/@hongminhee/fedify-tutorial-ko [日本語]: https://zenn.dev/hongminhee/books/4a38b6358a027b [microblog]: https://en.wikipedia.org/wiki/Microblogging [Mastodon]: https://joinmastodon.org/ [Misskey]: https://misskey-hub.net/ [Fedify]: https://fedify.dev/ [Matrix chat space]: https://matrix.to/#/#fedify:matrix.org [Discord server]: https://discord.gg/bhtwpzURwd [GitHub Discussions]: https://github.com/fedify-dev/fedify/discussions ## Target audience This tutorial is aimed at those who want to learn Fedify and create ActivityPub server software. We assume that you have experience in creating web applications using HTML and HTTP, and that you understand command-line interfaces, SQL, JSON, and basic JavaScript. However, you don't need to know TypeScript, JSX, ActivityPub, or Fedify—we'll teach you what you need to know about these as we go along. You don't need experience in creating ActivityPub software, but we do assume that you've used at least one ActivityPub software like Mastodon or Misskey. This is so you have an idea of what we're trying to build. \*\[JSX]: JavaScript XML ## Goals In this tutorial, we'll use Fedify to create a single-user microblog that can communicate with other federated software and services via ActivityPub. This software will include the following features: * Only one account can be created. * Other accounts in the fediverse can follow the user. * Followers can unfollow the user. * The user can view their list of followers. * The user can create posts. * The user's posts are visible to followers in the fediverse. * The user can follow other accounts in the fediverse. * The user can view a list of accounts they are following. * The user can view a chronological list of posts from accounts they follow. To simplify the tutorial, we'll impose the following feature constraints: * Account profiles (bio, photos, etc.) cannot be set. * Once created, an account cannot be deleted. * Once posted, a post cannot be edited or deleted. * Once followed, another account cannot be unfollowed. * There are no likes, shares, or comments. * There is no search functionality. * There are no security features such as authentication or permission checks. Of course, after completing the tutorial, you're welcome to add these features—it would be good practice! The complete source code is available in the [GitHub repository], with commits separated according to each implementation step for your reference. [GitHub repository]: https://github.com/fedify-dev/microblog ## Setting up the development environment ### Installing Node.js Fedify supports three JavaScript runtimes: [Deno], [Bun], and [Node.js]. Among these, Node.js is the most widely used, so we'll use Node.js as the basis for this tutorial. > \[!TIP] > A JavaScript runtime is a platform that executes JavaScript code. Web browsers > are one type of JavaScript runtime, and for command-line or server use, > Node.js is widely used. Recently, cloud edge functions like > [Cloudflare Workers] have also gained popularity as JavaScript runtimes. To use Fedify, you need Node.js version 22.0.0 or higher. There are [various installation methods]—choose the one that suits you best. Once Node.js is installed, you'll have access to the `node` and `npm` commands: ```sh node --version npm --version ``` ### Installing the `fedify` command To set up a Fedify project, you need to install the [`fedify`](../cli.md) command on your system. There are [several installation methods](../cli.md#installation), but using the `npm` command is the simplest: ```sh npm install -g @fedify/cli ``` After installation, check if you can use the `fedify` command. You can check the version of the `fedify` command with this command: ```sh fedify --version ``` Make sure the version number is 1.0.0 or higher. If it's an older version, you won't be able to properly follow this tutorial. ### `fedify init` to initialize the project To start a new Fedify project, let's decide on a directory path to work in. In this tutorial, we'll name it *microblog*. Run the [`fedify init`](../cli.md#fedify-init-initializing-a-fedify-project) command followed by the directory path (it's okay if the directory doesn't exist yet): ```sh fedify init microblog ``` When you run the `fedify init` command, you'll see a series of prompts. Select *Node.js*, *npm*, *Hono*, *In-memory*, and *In-process* in order: ```console ___ _____ _ _ __ /'_') | ___|__ __| (_)/ _|_ _ .-^^^-/ / | |_ / _ \/ _` | | |_| | | | __/ / | _| __/ (_| | | _| |_| | <__.|_|-|_| |_| \___|\__,_|_|_| \__, | |___/ ? Choose the JavaScript runtime to use Deno Bun ❯ Node.js ? Choose the package manager to use ❯ npm Yarn pnpm ? Choose the web framework to integrate Fedify with Bare-bones Fresh ❯ Hono Express Nitro ? Choose the key-value store to use for caching ❯ In-memory Redis PostgreSQL Deno KV ? Choose the message queue to use for background jobs ❯ In-process Redis PostgreSQL AMQP (e.g., RabbitMQ) Deno KV ``` > \[!NOTE] > Fedify is not a full-stack framework, but rather a framework specialized for > implementing ActivityPub servers. Therefore, it's designed to be used > alongside other web frameworks. In this tutorial, we'll adopt [Hono] as > our web framework to use with Fedify. After a moment, you'll see files created in your working directory with the following structure: * *.vscode/* — Visual Studio Code related settings * *extensions.json* — Recommended extensions for Visual Studio Code * *settings.json* — Visual Studio Code settings * *node\_modules/* — Directory where dependent packages are installed (contents omitted) * *src/* — Source code * *app.tsx* — Server unrelated to ActivityPub * *federation.ts* — ActivityPub server * *index.ts* — Entry point * *logging.ts* — Logging configuration * *biome.json* — Formatter and linter settings * *package.json* — Package metadata * *tsconfig.json* — TypeScript settings As you might guess, we're using TypeScript instead of JavaScript, which is why we have *.ts* and *.tsx* files instead of *.js* files. The generated source code is a working demo. Let's first check if it runs properly: ```sh npm run dev ``` This command will keep the server running until you press Ctrl+C: ```console Server started at http://0.0.0.0:8000 ``` With the server running, open a new terminal tab and run the following command: ```sh fedify lookup http://localhost:8000/users/john ``` This command queries an actor (actor) on the ActivityPub server we've set up locally. In ActivityPub, an actor can be thought of as an account that's accessible across various ActivityPub servers. If you see output like this, it's working correctly: ```console ✔ Looking up the object... Person { id: URL "http://localhost:8000/users/john", name: "john", preferredUsername: "john" } ``` This result tells us that there's an actor object located at the */users/john* path, it's of type `Person`, its ID is *http://localhost:8000/users/john*, its name is *john*, and its username is also *john*. > \[!TIP] > [`fedify lookup`](../cli.md#fedify-lookup-looking-up-an-activitypub-object) > is a command to query ActivityPub objects. This is equivalent > to searching with the corresponding URI on Mastodon. (Of course, since your > server is only accessible locally at the moment, searching on Mastodon won't > yield any results yet.) > > If you prefer `curl` over the `fedify lookup` command, you can also query > the actor with this command (note that we're sending the Accept > header with the `-H` option): > > ```sh > curl -H"Accept: application/activity+json" http://localhost:8000/users/john > ``` > > However, if you query like this, the result will be in JSON format, > which is difficult to read with the naked eye. If you also have the `jq` > command installed on your system, you can use `curl` and `jq` together: > > ```sh > curl -H"Accept: application/activity+json" http://localhost:8000/users/john | jq . > ``` ### Visual Studio Code [Visual Studio Code] might not be your favorite editor. However, we recommend using Visual Studio Code while following this tutorial. This is because we need to use TypeScript, and Visual Studio Code is currently the most convenient and excellent TypeScript IDE. Also, the generated project setup already includes Visual Studio Code settings, so you don't have to wrestle with formatters or linters. > \[!WARNING] > Don't confuse this with Visual Studio. Visual Studio Code and Visual Studio > only share a brand name; they are completely different software. After [installing Visual Studio Code], open the working directory by selecting *File* → *Open Folder…* from the menu. If you see a popup in the bottom right asking Do you want to install the recommended 'Biome' extension from biomejs for this repository?, click the *Install* button to install the extension. Installing this extension will automatically format your TypeScript code, so you don't have to wrestle with code styles like indentation or spacing when writing TypeScript code. > \[!TIP] > If you're a loyal Emacs or Vim user, we won't discourage you from using your > favorite editor. However, we recommend setting up TypeScript LSP. > The difference in productivity depending on whether TypeScript LSP is set up > or not is significant. [Deno]: https://deno.com/ [Bun]: https://bun.sh/ [Node.js]: https://nodejs.org/ [Cloudflare Workers]: https://workers.cloudflare.com/ [various installation methods]: https://nodejs.org/en/download/package-manager [Hono]: https://hono.dev/ [Visual Studio Code]: https://code.visualstudio.com/ [installing Visual Studio Code]: https://code.visualstudio.com/docs/setup/setup-overview \*\[LSP]: Language Server Protocol ## Prerequisites ### TypeScript Before we start modifying code, let's briefly go over TypeScript. If you're already familiar with TypeScript, you can skip this section. TypeScript adds static type checking to JavaScript. The TypeScript syntax is almost the same as JavaScript, but the main difference is that you can specify types for variables and functions. Types are specified by adding a colon (`:`) after the variable or parameter. For example, the following code indicates that the `foo` variable is a `string`: ```typescript twoslash let foo: string; ``` If you try to assign a value of a different type to a variable declared like this, Visual Studio Code will show a red underline *before you even run it* and display a type error: ```typescript twoslash // @errors: 2322 let foo: string; // ---cut-before--- foo = 123; ``` When coding, don't ignore red underlines. If you ignore them and run the program, it's likely that an error will actually occur at that part. The most common type of error you'll encounter when coding in TypeScript is the `null` possibility error. For example, in the following code, the `bar` variable can be either a `string` or `null` (`string | null`): ```typescript twoslash function someFunction(): string | null { return ""; } // ---cut-before--- const bar: string | null = someFunction(); ``` What happens if you try to get the first character of this variable's content like this? ```typescript twoslash // @errors: 18047 function someFunction(): string | null { return ""; } const bar: string | null = someFunction(); // ---cut-before--- const firstChar = bar.charAt(0); ``` You'll get a type error like above. It's saying that `bar` might sometimes be `null`, and in that case, calling `null.charAt(0)` would cause an error, so you need to fix the code. In such cases, you need to add handling for the `null` case like this: ```typescript twoslash function someFunction(): string | null { return ""; } const bar: string | null = someFunction(); // ---cut-before--- const firstChar = bar === null ? "" : bar.charAt(0); ``` In this way, TypeScript helps prevent bugs by making you think of cases you might not have considered when coding. Another incidental advantage of TypeScript is that it enables auto-completion. For example, if you type `foo.`, a list of methods available for string objects will appear, allowing you to choose from them. This allows for faster coding without having to check documentation each time. We hope you'll feel the charm of TypeScript as you follow this tutorial. Above all, Fedify provides the best experience when used with TypeScript. > \[!TIP] > If you want to learn TypeScript properly and thoroughly, we recommend reading > *[The TypeScript Handbook]*. It takes about 30 minutes to read it all. ### JSX JSX is a syntax extension of JavaScript that allows you to insert XML or HTML into JavaScript code. It can also be used in TypeScript, in which case it's sometimes called TSX. In this tutorial, we'll write all HTML within JavaScript code using JSX syntax. Those who are already familiar with JSX can skip this section. For example, the following code assigns an HTML tree with a `
` element at the top to the `html` variable: ```tsx twoslash const html =

Hello, JSX!

; ``` You can also insert JavaScript expressions using curly braces (the following code assumes, of course, that there's a `getName()` function): ```tsx twoslash /** * A hypothetical function that returns a name. */ function getName(): string { return ""; } // ---cut-before--- const html =

Hello, {getName()}!

; ``` One of the features of JSX is that you can define your own tags called components. Components can be defined as ordinary JavaScript functions. For example, the following code defines and uses a `` component (component names typically follow PascalCase style): ```tsx twoslash import type { Child, FC } from "hono/jsx"; function getName() { return "JSX"; } interface ContainerProps { name: string; children: Child; } const Container: FC = (props) => { return
{props.children}
; }; const html =

Hello, {getName()}!

; ``` In the above code, `FC` is provided by [Hono], the web framework we'll use, and it helps define the type of the component. `FC` is a generic type, and the types that go inside the angle brackets after `FC` are type arguments. Here, we specify the props type as the type argument. Props refer to the parameters passed to the component. In the above code, we declared and used the `ContainerProps` interface as the props type for the `` component. > \[!TIP] > Type arguments for generic types can be multiple, separated by commas. > For example, `Foo` applies type arguments `A` and `B` to the generic > type `Foo`. > > There are also generic functions, which are written as > `someFunction(foo, bar)`. > > When there's only one type argument, the angle brackets enclosing the type > argument may look like XML/HTML tags, but they have nothing to do with JSX > functionality. > > `FC` > : Applies the type argument `ContainerProps` to the generic type `FC`. > > `` > : Opens a component tag named ``. Must be closed with > ``. Among the things passed as props, `children` is worth noting specifically. This is because the child elements of the component are passed as the `children` prop. As a result, in the above code, the `html` variable is assigned the HTML tree `

Hello, JSX!

`. > \[!TIP] > JSX was invented in the React project and started to be widely used. > If you want to know more about JSX, read the *[Writing Markup with JSX]* and > *[JavaScript in JSX with Curly Braces]* sections of the React documentation. [The TypeScript Handbook]: https://www.typescriptlang.org/docs/handbook/intro.html [Writing Markup with JSX]: https://react.dev/learn/writing-markup-with-jsx [JavaScript in JSX with Curly Braces]: https://react.dev/learn/javascript-in-jsx-with-curly-braces ## Account creation page The first thing we'll create is the account creation page. We need to create an account before we can post or follow other accounts. Let's start by building the visible part. First, let's create a file named *src/views.tsx*. Inside this file, we'll define a `` component using JSX: ```tsx twoslash [src/views.tsx] import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ( Microblog
{props.children}
); ``` To avoid spending too much time on design, we'll use a CSS framework called [Pico CSS]. > \[!TIP] > When the type of a variable or parameter can be inferred by TypeScript's type > checker, like `props` above, it's fine to omit the type annotation. Even when > the type annotation is omitted in such cases, you can check the type of > a variable by hovering your mouse cursor over the variable name in > Visual Studio Code. Next, in the same file, let's define a `` component that will go inside the layout: ```tsx twoslash [src/views.tsx] import type { FC } from "hono/jsx"; // ---cut-before--- export const SetupForm: FC = () => ( <>

Set up your microblog

); ``` In JSX, you can only have one top-level element, but the `` component has two top-level elements: `

` and `
`. That's why we've wrapped them with `<>` and ``. This is called a fragment. Now it's time to use the components we've defined. Open the *src/app.tsx* file and `import` the two components we just defined: ```tsx twoslash [src/app.tsx] // @noErrors: 2395 2307 import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const SetupForm: FC = () => <>; // ---cut-before--- import { Layout, SetupForm } from "./views.tsx"; ``` Then, display the account creation form we just made on the */setup* page: ```tsx twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const SetupForm: FC = () => <>; // ---cut-before--- app.get("/setup", (c) => c.html( , ), ); ``` Now, let's open the page in a web browser. If you see a screen like this, it's working correctly: ![Account creation page](./microblog/account-creation-page.png) > \[!NOTE] > To use JSX, the source file extension must be *.jsx* or *.tsx*. > Note that both files we edited in this section have the *.tsx* extension. ### Database setup Now that we've implemented the visible part, it's time to implement the functionality. We need a place to store account information, so let's use [SQLite]. SQLite is a relational database suitable for small-scale applications. First, let's declare a table to hold account information. From now on, we'll write all table declarations in the *src/schema.sql* file. We'll store account information in the `users` table: ```sql [src/schema.sql] CREATE TABLE IF NOT EXISTS users ( id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1), username TEXT NOT NULL UNIQUE CHECK (trim(lower(username)) = username AND username <> '' AND length(username) <= 50) ); ``` Since the microblog we're creating can only have one account, we've put a constraint on the `id` column, which is the primary key, to not allow values other than `1`. This ensures that the `users` table can't contain more than one record. We've also put constraints on the `username` column to not allow empty strings or strings that are too long. Now we need to run the *src/schema.sql* file to create the users table. For this, we need the `sqlite3` command, which you can [get from the SQLite website] or install from your platform's package manager. For macOS, you don't need to get it separately as it is built into the system. If you get it directly, you can get the *sqlite-tools-\*.zip* file for your OS and unzip it. If you use a package manager, you can also install it with the following command: ::: code-group ```sh [Debian & Ubuntu] sudo apt install sqlite3 ``` ```sh [Fedora & RHEL] sudo dnf install sqlite ``` ```powershell [Chocolatey] choco install sqlite ``` ```powershell [Scoop] scoop install sqlite ``` ```powershell [Windows Package Manager] winget install SQLite.SQLite ``` ::: Okay, now that we have the `sqlite3` command, let's use it to create a database file: ```sh sqlite3 microblog.sqlite3 < src/schema.sql ``` The above command will create a *microblog.sqlite3* file, which will store your SQLite data. [get from the SQLite website]: https://www.sqlite.org/download.html ### Connecting to the database in the app Now we need to use the SQLite database in our app. To use SQLite database in Node.js, we need a SQLite driver library, and we'll use the *[better-sqlite3]* package. You can easily install the package with the `npm` command: ```sh npm add better-sqlite3 npm add --save-dev @types/better-sqlite3 ``` > \[!TIP] > The *[@types/better-sqlite3]* package contains type information about > the *better-sqlite3* package's API for TypeScript. You need to install this > package to enable auto-completion and type checking when editing in Visual > Studio Code. > > Packages like this in the *@types/* scope are called [Definitely Typed] > packages. When a library is not written in TypeScript, the community adds > type information and makes it into a package. Now that we've installed the package, let's write code to connect to the database using this package. Create a new file named *src/db.ts* and code it as follows: ```typescript twoslash [src/db.ts] import Database from "better-sqlite3"; const db = new Database("microblog.sqlite3"); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); export default db; ``` > \[!TIP] > The settings made through the `db.pragma()` function have the following > effects: > > [`journal_mode = WAL`] > : Adopts [Write-Ahead Logging] mode as a way to implement atomic commits and > rollbacks in SQLite. This mode is generally more performant than > the default [rollback journal] mode. > > [`foreign_keys = ON`] > : By default, SQLite does not check foreign key constraints. Turning on this > setting makes it check foreign key constraints, which helps maintain data > integrity. Now let's declare a type in JavaScript to represent the record stored in the `users` table. Create a *src/schema.ts* file and define the `User` type as follows: ```typescript twoslash [src/schema.ts] export interface User { id: number; username: string; } ``` [better-sqlite3]: https://github.com/WiseLibs/better-sqlite3 [@types/better-sqlite3]: https://www.npmjs.com/package/@types/better-sqlite3 [Definitely Typed]: https://github.com/DefinitelyTyped/DefinitelyTyped [`journal_mode = WAL`]: https://www.sqlite.org/wal.html [Write-Ahead Logging]: https://en.wikipedia.org/wiki/Write-ahead_logging [rollback journal]: https://www.sqlite.org/lockingv3.html#rollback [`foreign_keys = ON`]: https://www.sqlite.org/foreignkeys.html#fk_enable ### Record insertion Now that we've connected to the database, it's time to write code to insert records. Open the *src/app.tsx* file and `import` the `db` object and `User` type that will be used for record insertion: ```typescript twoslash [src/app.tsx] // @noErrors: 2307 import Database from "better-sqlite3"; const db = new Database(""); interface User {} // ---cut-before--- import db from "./db.ts"; import type { User } from "./schema.ts"; ``` Implement the `POST /setup` handler: ```typescript twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import Database from "better-sqlite3"; const db = new Database(""); interface User {} // ---cut-before--- app.post("/setup", async (c) => { // Check if an account already exists const user = db.prepare("SELECT * FROM users LIMIT 1").get(); if (user != null) return c.redirect("/"); const form = await c.req.formData(); const username = form.get("username"); if (typeof username !== "string" || !username.match(/^[a-z0-9_-]{1,50}$/)) { return c.redirect("/setup"); } db.prepare("INSERT INTO users (username) VALUES (?)").run(username); return c.redirect("/"); }); ``` Add code to check if an account already exists to the `GET /setup` handler we created earlier: ```tsx{2-4} twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const SetupForm: FC = () => <>; import Database from "better-sqlite3"; const db = new Database(""); interface User {} // ---cut-before--- app.get("/setup", (c) => { // Check if an account already exists const user = db.prepare("SELECT * FROM users LIMIT 1").get(); if (user != null) return c.redirect("/"); return c.html( , ); }); ``` ### Testing Now that we've roughly implemented the account creation feature, let's try it out. Open the page in a web browser and create an account. In this tutorial, we'll assume that we used *johndoe* as the username. If it's created, let's also check if the record was properly inserted into the SQLite database: ```sh echo "SELECT * FROM users;" | sqlite3 -table microblog.sqlite3 ``` If the record was properly inserted, you should see output like this (of course, `johndoe` will be whatever username you entered): | `id` | `username` | |------|------------| | `1` | `johndoe` | [Pico CSS]: https://picocss.com/ [SQLite]: https://www.sqlite.org/ ## Profile page Now that we've created an account, let's implement a profile page to display the account information. Although we don't have much information to show yet. Let's start with the visible part again. Open the *src/views.tsx* file and define a `` component: ```tsx twoslash [src/views.tsx] import type { FC } from "hono/jsx"; // ---cut-before--- export interface ProfileProps { name: string; handle: string; } export const Profile: FC = ({ name, handle }) => ( <>

{name}

{handle}

); ``` Then, open the *src/app.tsx* file and `import` the component we just defined: ```tsx twoslash [src/app.tsx] // @noErrors: 2395 2307 import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const Profile: FC = () => <>; export const SetupForm: FC = () => <>; // ---cut-before--- import { Layout, Profile, SetupForm } from "./views.tsx"; ``` And add a `GET /users/{username}` request handler that displays the `` component: ```tsx twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const Profile: FC = () => <>; import Database from "better-sqlite3"; const db = new Database(""); interface User { username: string; } // ---cut-before--- app.get("/users/:username", async (c) => { const user = db .prepare("SELECT * FROM users WHERE username = ?") .get(c.req.param("username")); if (user == null) return c.notFound(); const url = new URL(c.req.url); const handle = `@${user.username}@${url.host}`; return c.html( , ); }); ``` Now let's test if it displays correctly. Open the page in your web browser (if you created an account with a username other than `johndoe`, change the URL accordingly). You should see a screen like this: ![Profile page](./microblog/profile-page.png) > \[!TIP] > A fediverse handle, or simply handle, is a unique address that identifies > an account in the fediverse. For example, it looks like > `@hongminhee@fosstodon.org`. It's similar to an email address, > and its structure is also similar to an email address. It starts with `@`, > followed by a name, then another `@`, and finally the domain name of > the server the account belongs to. Sometimes the initial `@` is omitted. > > Technically, handles are implemented using two standards: [WebFinger] and > the [`acct:` URI scheme]. Thanks to Fedify implementing these, you don't need > to know the implementation details while following this tutorial. [WebFinger]: https://datatracker.ietf.org/doc/html/rfc7033 [`acct:` URI scheme]: https://datatracker.ietf.org/doc/html/rfc7565 ## Implementing the actor As the name suggests, ActivityPub is a protocol for exchanging activities. Writing a post, editing a post, deleting a post, liking a post, commenting, editing a profile… All actions that happen in social media are expressed as activities. And all activities are transmitted from actor to actor. For example, when John Doe writes a post, a writing (`Create(Note)`) activity is sent from Joh Doe to John Doe's followers. If Jane Doe likes that post, a liking (`Like`) activity is sent from Jane Doe to John Doe. Therefore, the first step in implementing ActivityPub is to implement the actor. The demo app generated by the `fedify init` command already has a very simple actor implemented, but to communicate with actual software like Mastodon or Misskey, we need to implement the actor more properly. First, let's take a look at the current implementation. Open the *src/federation.ts* file: ```typescript{12-18} twoslash [src/federation.ts] import { Person, createFederation } from "@fedify/fedify"; import { InProcessMessageQueue, MemoryKvStore } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; const logger = getLogger("microblog"); const federation = createFederation({ kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), }); federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, name: identifier, }); }); export default federation; ``` The part we should focus on is the `~Federation.setActorDispatcher()` method. This method defines the URL and behavior that other ActivityPub software will use when querying an actor on our server. For example, if we query */users/johndoe* as we did earlier, the `identifier` parameter of the callback function will receive the string value `"johndoe"`. And the callback function returns an instance of the `Person` class to convey the information of the queried actor. The `ctx` parameter receives a `Context` object, which contains various functions related to the ActivityPub protocol. For example, the `~Context.getActorUri()` method used in the above code returns the unique URI of the actor with the `identifier` passed as a parameter. This URI is being used as the unique identifier of the `Person` object. As you can see from the implementation code, currently it's *making up* actor information and returning it for any identifier that comes after the */users/* path. But what we want is to only allow queries for accounts that are actually registered. Let's modify this part to only return for accounts in the database. ### Table creation We need to create an `actors` table. Unlike the `users` table which only contains accounts on the current instance server, this table will also include remote actors belonging to federated servers. The table looks like this. Add the following SQL to the *src/schema.sql* file: ```sql [src/schema.sql] CREATE TABLE IF NOT EXISTS actors ( id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER REFERENCES users (id), uri TEXT NOT NULL UNIQUE CHECK (uri <> ''), handle TEXT NOT NULL UNIQUE CHECK (handle <> ''), name TEXT, inbox_url TEXT NOT NULL UNIQUE CHECK (inbox_url LIKE 'https://%' OR inbox_url LIKE 'http://%'), shared_inbox_url TEXT CHECK (shared_inbox_url LIKE 'https://%' OR shared_inbox_url LIKE 'http://%'), url TEXT CHECK (url LIKE 'https://%' OR url LIKE 'http://%'), created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> '') ); ``` * The `user_id` column is a foreign key to connect with the `users` column. If the record represents a remote actor, it will be `NULL`, but if it's an account on the current instance server, it will contain the `users.id` value of that account. * The `uri` column contains the unique URI of the actor, also called the actor ID. All ActivityPub objects, including actors, have a unique ID in URI form. Therefore, it cannot be empty and cannot be duplicated. * The `handle` column contains the fediverse handle in the form of `@johndoe@example.com`. Likewise, it cannot be empty and cannot be duplicated. * The `name` column contains the name displayed in the UI. It usually contains a full name or nickname. However, according to the ActivityPub specification, this column can be empty. * The `inbox_url` column contains the URL of the actor's inbox. We'll explain in detail what an inbox is below, but for now, just know that it must exist for the actor. This column also cannot be empty or duplicated. * The `shared_inbox_url` column contains the URL of the actor's shared inbox, which we'll also explain below. It's not mandatory, so it can be empty, and as the column name suggests, it can share the same shared inbox URL with other actors. * The `url` column contains the profile URL of the actor. A profile URL means the URL of the profile page that can be opened in a web browser. Sometimes the actor's ID and profile URL are the same, but they can be different depending on the service, so in that case, the profile URL is stored in this column. It can be empty. * The `created` column records when the record was created. It cannot be empty, and by default, the insertion time is recorded. Now, let's apply the *src/schema.sql* file to the *microblog.sqlite3* database file: ```sh sqlite3 microblog.sqlite3 < src/schema.sql ``` And let's define a type in *src/schema.ts* to represent records stored in the `actors` table in JavaScript: ```typescript twoslash [src/schema.ts] export interface Actor { id: number; user_id: number | null; uri: string; handle: string; name: string | null; inbox_url: string; shared_inbox_url: string | null; url: string | null; created: string; } ``` ### Actor record Although we currently have one record in the `users` table, there's no corresponding record in the `actors` table. This is because we didn't add a record to the `actors` table when creating the account. We need to modify the account creation code to add records to both `users` and `actors`. First, let's modify the `SetupForm` in *src/views.tsx* to also input a name that will go into the `actors.name` column along with the username: ```tsx{16-18} twoslash [src/views.tsx] import type { FC } from "hono/jsx"; // ---cut-before--- export const SetupForm: FC = () => ( <>

Set up your microblog

); ``` Now `import` the Actor type we defined earlier in *src/app.tsx*: ```typescript [src/app.tsx] import type { Actor, User } from "./schema.ts"; ``` Now let's add code to the `POST /setup` handler to create a record in the `actors` table with the input name and other necessary information: ```typescript{7,19-24,26,30-44} twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import Database from "better-sqlite3"; const db = new Database(""); interface User {} import type { Federation } from "@fedify/fedify"; const fedi = null as unknown as Federation; // ---cut-before--- app.post("/setup", async (c) => { // Check if an account already exists const user = db .prepare( ` SELECT * FROM users JOIN actors ON (users.id = actors.user_id) LIMIT 1 `, ) .get(); if (user != null) return c.redirect("/"); const form = await c.req.formData(); const username = form.get("username"); if (typeof username !== "string" || !username.match(/^[a-z0-9_-]{1,50}$/)) { return c.redirect("/setup"); } const name = form.get("name"); if (typeof name !== "string" || name.trim() === "") { return c.redirect("/setup"); } const url = new URL(c.req.url); const handle = `@${username}@${url.host}`; const ctx = fedi.createContext(c.req.raw, undefined); db.transaction(() => { db.prepare("INSERT OR REPLACE INTO users (id, username) VALUES (1, ?)").run( username, ); db.prepare( ` INSERT OR REPLACE INTO actors (user_id, uri, handle, name, inbox_url, shared_inbox_url, url) VALUES (1, ?, ?, ?, ?, ?, ?) `, ).run( ctx.getActorUri(username).href, handle, name, ctx.getInboxUri(username).href, ctx.getInboxUri().href, ctx.getActorUri(username).href, ); })(); return c.redirect("/"); }); ``` When checking if an account already exists, we modified it to determine that there's no account yet not only when there's no record in the `users` table, but also when there's no matching record in the `actors` table. Apply the same condition to the `GET /setup` handler and the `GET /users/{username}` handler: ```tsx{7} twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import Database from "better-sqlite3"; const db = new Database(""); interface User {} import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const SetupForm: FC = () => <>; // ---cut-before--- app.get("/setup", (c) => { // Check if the user already exists const user = db .prepare( ` SELECT * FROM users JOIN actors ON (users.id = actors.user_id) LIMIT 1 `, ) .get(); if (user != null) return c.redirect("/"); return c.html( , ); }); ``` ```tsx{6} twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import Database from "better-sqlite3"; const db = new Database(""); interface Actor { name: string; } interface User { username: string; } import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const Profile: FC = () => <>; // ---cut-before--- app.get("/users/:username", async (c) => { const user = db .prepare( ` SELECT * FROM users JOIN actors ON (users.id = actors.user_id) WHERE username = ? `, ) .get(c.req.param("username")); if (user == null) return c.notFound(); const url = new URL(c.req.url); const handle = `@${user.username}@${url.host}`; return c.html( , ); }); ``` > \[!TIP] > In TypeScript, `A & B` means an object that is both type `A` and type `B`. > For example, given the type `{ a: number } & { b: string }`, `{ a: 123 }` or > `{ b: "foo" }` do not satisfy this type, but `{ a: 123, b: "foo" }` does > satisfy this type. Finally, open the *src/federation.ts* file and add the following code below the actor dispatcher: ```typescript twoslash [src/federation.ts] import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); ``` Don't worry about the `~Federation.setInboxListeners()` method for now. We'll cover this when we explain about the inbox. Just note that the `~Context.getInboxUri()` method used in the account creation code needs the above code to work properly. If you've modified all the code, open the page in your browser and create an account again: ![Account creation page](./microblog/account-creation-page-2.png) ### Actor dispatcher Now that we've created the `actors` table and filled in a record, let's modify *src/federation.ts* again. First, `import` the `db` object, and `Endpoints` and Actor types: ```typescript twoslash [src/federation.ts] // @noErrors: 2307 import { Endpoints, Person, createFederation } from "@fedify/fedify"; import db from "./db.ts"; import type { Actor, User } from "./schema.ts"; ``` Now that we've `import`ed what we need, let's modify the `~Federation.setActorDispatcher()` method: ```typescript{2-11,16-21} twoslash [src/federation.ts] import { Endpoints, Person, type Federation } from "@fedify/fedify"; import Database from "better-sqlite3"; const db = new Database(""); interface User {} interface Actor { name: string; } const federation = null as unknown as Federation; // ---cut-before--- federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const user = db .prepare( ` SELECT * FROM users JOIN actors ON (users.id = actors.user_id) WHERE users.username = ? `, ) .get(identifier); if (user == null) return null; return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, name: user.name, inbox: ctx.getInboxUri(identifier), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), url: ctx.getActorUri(identifier), }); }); ``` In the changed code, we now query the `users` table in the database and return `null` if it's not an account on the current server. In other words, it will respond with a proper `Person` object with `200 OK` for a `GET /users/johndoe` request (assuming the account was created with the username `johndoe`), and respond with `404 Not Found` for other requests. Let's look at how the part creating the `Person` object has changed. First, a `name` property has been added. This property uses the value from the `actors.name` column. We'll cover the `inbox` and `endpoints` properties when we explain about the inbox. The `url` property contains the profile URL of this account, and in this tutorial, we'll make the actor ID and the actor's profile URL match. > \[!TIP] > Sharp-eyed readers may have noticed that we're defining overlapping handlers > for `GET /users/{identifier}` on both Hono and Fedify sides. So what happens > when an actual request is sent to this path? The answer is that it depends on > the Accept header of the request. If a request is sent with > the `Accept: text/html` header, the request handler on the Hono side responds. > If a request is sent with the `Accept: application/activity+json` header, > the request handler on the Fedify side responds. > > This way of giving different responses according to the Accept > header of the request is called HTTP [content negotiation], and Fedify itself > implements content negotiation. More specifically, all requests go through > Fedify once, and if it's not an ActivityPub-related request, it's passed on to > the integrated framework, which in this tutorial is Hono. > \[!TIP] > In Fedify, all URIs and URLs are represented as [`URL`] instances. [content negotiation]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation [`URL`]: https://developer.mozilla.org/ ### Testing Now, let's test if the actor dispatcher is working well. With the server running, open a new terminal tab and enter the following command: ```sh fedify lookup http://localhost:8000/users/alice ``` Since there's no account named `alice`, you'll get an error like this, unlike before: ```console ✔ Looking up the object... Failed to fetch the object. It may be a private object. Try with -a/--authorized-fetch. ``` Now let's look up the `johndoe` account: ```sh fedify lookup http://localhost:8000/users/johndoe ``` Now you get a good result: ```console ✔ Looking up the object... Person { id: URL "http://localhost:8000/users/johndoe", name: "John Doe", url: URL "http://localhost:8000/users/johndoe", preferredUsername: "johndoe", inbox: URL "http://localhost:8000/users/johndoe/inbox", endpoints: Endpoints { sharedInbox: URL "http://localhost:8000/inbox" } } ``` ## Cryptographic key pairs The next thing we'll implement is the actor's cryptographic keys for signing. In ActivityPub, when an actor creates and sends an activity, it uses a [digital signature] to prove that the activity was really created by that actor. For this, each actor creates and holds their own matching private key (secret key) and public key pair, and makes the public key visible to other actors. When actors receive an activity, they compare the sender's public key with the activity's signature to verify that the activity was indeed created by the sender. Fedify handles the signing and signature verification automatically, but you need to implement the generation and preservation of the key pairs yourself. > \[!WARNING] > As the name suggests, the private key (secret key) should not be accessible > to anyone other than the signing subject. On the other hand, the public key's > purpose is to be public, so it's fine for anyone to access it. [digital signature]: https://en.wikipedia.org/wiki/Digital_signature ### Table creation Let's define a `keys` table in *src/schema.sql* to store the private and public key pairs: ```sql [src/schema.sql] CREATE TABLE IF NOT EXISTS keys ( user_id INTEGER NOT NULL REFERENCES users (id), type TEXT NOT NULL CHECK (type IN ('RSASSA-PKCS1-v1_5', 'Ed25519')), private_key TEXT NOT NULL CHECK (private_key <> ''), public_key TEXT NOT NULL CHECK (public_key <> ''), created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> ''), PRIMARY KEY (user_id, type) ); ``` If you look closely at the table, you can see that the `type` column only allows two types of values. One is the [RSA-PKCS#1-v1.5] type and the other is the [Ed25519] type. (What each of these means is not important for this tutorial.) Since the primary key is on `(user_id, type)`, there can be a maximum of two key pairs for one user. > \[!TIP] > We can't go into detail in this tutorial, but as of November 2024, > the ActivityPub network is in the process of transitioning from > the RSA-PKCS#1-v1.5 type to the Ed25519 type. Some software only > accepts the RSA-PKCS#1-v1.5 type, while some software accepts > the Ed25519 type. Therefore, to communicate with both sides, > both pairs of keys are needed. The `private_key` and `public_key` columns can receive strings, and we'll put JSON data in them. We'll cover how to encode private and public keys as JSON later. Now let's create the `keys` table: ```sh sqlite3 microblog.sqlite3 < src/schema.sql ``` Let's also define a `Key` type in the *src/schema.ts* file to represent records stored in the `keys` table in JavaScript: ```typescript twoslash [src/schema.ts] export interface Key { user_id: number; type: "RSASSA-PKCS1-v1_5" | "Ed25519"; private_key: string; public_key: string; created: string; } ``` [RSA-PKCS#1-v1.5]: https://www.rfc-editor.org/rfc/rfc2313 [Ed25519]: https://ed25519.cr.yp.to/ ### Key pairs dispatcher Now we need to write code to generate and load key pairs. Open the *src/federation.ts* file and `import` the `exportJwk()`, `generateCryptoKeyPair()`, `importJwk()` functions provided by Fedify and the `Key` type we defined earlier: ```typescript{5-7,9} twoslash [src/federation.ts] // @noErrors: 2307 import { Endpoints, Person, createFederation, exportJwk, generateCryptoKeyPair, importJwk, } from "@fedify/fedify"; import type { Actor, Key, User } from "./schema.ts"; ``` Now let's modify the actor dispatcher part as follows: ```typescript twoslash [src/federation.ts] import { Endpoints, type Federation, Person, exportJwk, generateCryptoKeyPair, importJwk, } from "@fedify/fedify"; const federation = null as unknown as Federation; import { type Logger } from "@logtape/logtape"; const logger = null as unknown as Logger; import Database from "better-sqlite3"; const db = new Database(""); interface User { id: number; } interface Actor { name: string; } interface Key { type: "RSASSA-PKCS1-v1_5" | "Ed25519"; private_key: string; public_key: string; } // ---cut-before--- federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { const user = db .prepare( ` SELECT * FROM users JOIN actors ON (users.id = actors.user_id) WHERE users.username = ? `, ) .get(identifier); if (user == null) return null; const keys = await ctx.getActorKeyPairs(identifier); return new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier, name: user.name, inbox: ctx.getInboxUri(identifier), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), url: ctx.getActorUri(identifier), publicKey: keys[0].cryptographicKey, assertionMethods: keys.map((k) => k.multikey), }); }) .setKeyPairsDispatcher(async (ctx, identifier) => { const user = db .prepare("SELECT * FROM users WHERE username = ?") .get(identifier); if (user == null) return []; const rows = db .prepare("SELECT * FROM keys WHERE keys.user_id = ?") .all(user.id); const keys = Object.fromEntries( rows.map((row) => [row.type, row]), ) as Record; const pairs: CryptoKeyPair[] = []; // For each of the two key formats (RSASSA-PKCS1-v1_5 and Ed25519) that // the actor supports, check if they have a key pair, and if not, // generate one and store it in the database: for (const keyType of ["RSASSA-PKCS1-v1_5", "Ed25519"] as const) { if (keys[keyType] == null) { logger.debug( "The user {identifier} does not have an {keyType} key; creating one...", { identifier, keyType }, ); const { privateKey, publicKey } = await generateCryptoKeyPair(keyType); db.prepare( ` INSERT INTO keys (user_id, type, private_key, public_key) VALUES (?, ?, ?, ?) `, ).run( user.id, keyType, JSON.stringify(await exportJwk(privateKey)), JSON.stringify(await exportJwk(publicKey)), ); pairs.push({ privateKey, publicKey }); } else { pairs.push({ privateKey: await importJwk( JSON.parse(keys[keyType].private_key), "private", ), publicKey: await importJwk( JSON.parse(keys[keyType].public_key), "public", ), }); } } return pairs; }); ``` First of all, we should pay attention to the `~ActorCallbackSetters.setKeyPairsDispatcher()` method called in succession after the `~Federation.setActorDispatcher()` method. This method connects the key pairs returned by the callback function to the account. By connecting the key pairs in this way, Fedify automatically adds digital signatures with the registered private keys when sending activities. The `generateCryptoKeyPair()` function generates a new private key and public key pair and returns it as a [`CryptoKeyPair`] object. For your reference, the [`CryptoKeyPair`] type has the type `{ privateKey: CryptoKey; publicKey: CryptoKey; }`. The `exportJwk()` function returns an object representing the [`CryptoKey`] object in JWK format. You don't need to know what the JWK format is. Just understand that it's a standard format for representing cryptographic keys in JSON. [`CryptoKey`] is a web standard type for representing cryptographic keys as JavaScript objects. The `importJwk()` function converts a key represented in JWK format to a [`CryptoKey`] object. You can understand it as the opposite of the `exportJwk()` function. Now, let's turn our attention back to the `~Federation.setActorDispatcher()` method. We're using a method called `~Context.getActorKeyPairs()`, which, as the name suggests, returns the key pairs of the actor. The actor's key pairs are those very key pairs we just loaded with the `~ActorCallbackSetters.setKeyPairsDispatcher()` method. We loaded two pairs of keys in RSA-PKCS#1-v1.5 and Ed25519 formats, so the `~Context.getActorKeyPairs()` method returns an array of two key pairs. Each element of the array is an object representing the key pair in various formats, which looks like this: ```typescript twoslash import type { CryptographicKey, Multikey } from "@fedify/fedify"; // ---cut-before--- interface ActorKeyPair { privateKey: CryptoKey; // Private key publicKey: CryptoKey; // Public key keyId: URL; // Unique identification URI of the key cryptographicKey: CryptographicKey; // Another format of the public key multikey: Multikey; // Yet another format of the public key } ``` It's complex to explain here how [`CryptoKey`], `CryptographicKey`, and `Multikey` differ, and why there need to be so many formats. For now, let's just note that when initializing the `Person` object, the `publicKey` property accepts the `CryptographicKey` type and the `assertionMethods` property accepts the `MultiKey[]` (TypeScript notation for an array of `Multikey`) type. By the way, why are there two properties in the `Person` object that hold public keys, `publicKey` and `assertionMethods`? Originally in ActivityPub, there was only the `publicKey` property, but later the `assertionMethods` property was added to allow registration of multiple keys. Similar to how we generated both RSA-PKCS#1-v1.5 and Ed25519 keys earlier, we're setting both properties for compatibility with various software. If you look closely, you can see that we're only registering the RSA-PKCS#1-v1.5 key to the legacy `publicKey` property (the first item in the array is the RSA-PKCS#1-v1.5 key pair, and the second item is the Ed25519 key pair). > \[!TIP] > Actually, the `publicKey` property can contain multiple keys too. However, > many software are already implemented under the assumption that > the `publicKey` property will only contain one key, so they often malfunction. > The `assertionMethods` property was proposed to avoid this. > > For those interested in this, refer to the [FEP-521a] document. [`CryptoKeyPair`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair [`CryptoKey`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey [FEP-521a]: https://w3id.org/fep/521a ### Testing Now that we've registered the cryptographic keys to the actor object, let's check if it's working well. Query the actor with the following command: ```sh fedify lookup http://localhost:8000/users/johndoe ``` If it's working correctly, you should see output like this: ```console{7,14,22-23,30,38,44} ✔ Looking up the object... Person { id: URL "http://localhost:8000/users/johndoe", name: "John Doe", url: URL "http://localhost:8000/users/johndoe", preferredUsername: "johndoe", publicKey: CryptographicKey { id: URL "http://localhost:8000/users/johndoe#main-key", owner: URL "http://localhost:8000/users/johndoe", publicKey: CryptoKey { type: "public", extractable: true, algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: Uint8Array(3) [ 1, 0, 1 ], hash: { name: "SHA-256" } }, usages: [ "verify" ] } }, assertionMethods: [ Multikey { id: URL "http://localhost:8000/users/johndoe#main-key", controller: URL "http://localhost:8000/users/johndoe", publicKey: CryptoKey { type: "public", extractable: true, algorithm: { name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: Uint8Array(3) [ 1, 0, 1 ], hash: { name: "SHA-256" } }, usages: [ "verify" ] } }, Multikey { id: URL "http://localhost:8000/users/johndoe#key-2", controller: URL "http://localhost:8000/users/johndoe", publicKey: CryptoKey { type: "public", extractable: true, algorithm: { name: "Ed25519" }, usages: [ "verify" ] } } ], inbox: URL "http://localhost:8000/users/johndoe/inbox", endpoints: Endpoints { sharedInbox: URL "http://localhost:8000/inbox" } } ``` You can see that the `Person` object's `publicKey` property contains one `CryptographicKey` object in RSA-PKCS#1-v1.5 type, and the `assertionMethods` property contains two `Multikey` objects in RSA-PKCS#1-v1.5 and Ed25519 formats. ## Interoperating with Mastodon Now let's check if we can actually view the actor we've created in Mastodon. ### Exposing to the public internet Unfortunately, the current server is only accessible locally. However, it would be inconvenient to deploy somewhere every time we modify the code for testing. Wouldn't it be great if we could expose our local server to the internet without deployment for immediate testing? Here's where the [`fedify tunnel`](../cli.md#fedify-tunnel-exposing-a-local-http-server-to-the-public-internet) command comes in handy. In a terminal, open a new tab and enter this command followed by the port number of your local server: ```sh fedify tunnel 8000 ``` This creates a disposable domain name and relays to your local server. It will output a URL that's accessible from the outside: ```console ✔ Your local server at 8000 is now publicly accessible: https://temp-address.serveo.net/ Press ^C to close the tunnel. ``` Of course, you'll see your own unique URL different from the one above. You can check if it's connecting well by opening in your web browser (replace with your unique temporary domain): ![Profile page exposed to the public internet](./microblog/profile-page-2.png) Copy your fediverse handle shown on the above web page, then go into Mastodon and paste it into the search box in the upper left corner: ![Search results for the fediverse handle in Mastodon](./microblog/search-results.png) If the actor we created appears in the search results as shown above, it's working correctly. You can also click on the actor's name in the search results to go to their profile page: ![Actor's profile viewed in Mastodon](./microblog/remote-profile.png) But this is as far as we can go. Don't try to follow yet! For our actor to be followable from other servers, we need to implement an inbox. > \[!NOTE] > The `fedify tunnel` command automatically disconnects after a while if not > used. When this happens, you need to press Ctrl+C to > stop it, then run the `fedify tunnel 8000` command again to establish a new > connection. ## Inbox In ActivityPub, an inbox is an endpoint where an actor receives incoming activities from other actors. All actors have their own inbox, which is a URL that can receive activities via HTTP `POST` requests. When another actor sends a follow request, writes a post, comments, or performs any other interaction, the corresponding activity is delivered to the recipient's inbox. The server processes the activities that come into the inbox and responds appropriately, allowing it to communicate and function as part of the federated network. For now, we'll start by implementing the reception of follow requests. ### Table creation We need to create a `follows` table to hold the actors who follow you (followers) and the actors you follow (following). Add the following SQL to the *src/schema.sql* file: ```sql [src/schema.sql] CREATE TABLE IF NOT EXISTS follows ( following_id INTEGER REFERENCES actors (id), follower_id INTEGER REFERENCES actors (id), created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> ''), PRIMARY KEY (following_id, follower_id) ); ``` Let's create the `follows` table by executing *src/schema.sql* once again: ```sh sqlite3 microblog.sqlite3 < src/schema.sql ``` Open the *src/schema.ts* file and define a type to represent records stored in the `follows` table in JavaScript: ```typescript twoslash [src/schema.ts] export interface Follow { following_id: number; follower_id: number; created: string; } ``` ### Receiving `Follow` activity Now it's time to implement the inbox. Actually, we've already written the following code in the *src/federation.ts* file earlier: ```typescript twoslash [src/federation.ts] import type { Federation } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); ``` Before modifying this code, let's `import` the `Accept` and `Follow` classes and the `getActorHandle()` function provided by Fedify: ```typescript{2,4,9} twoslash [src/federation.ts] import { Accept, Endpoints, Follow, Person, createFederation, exportJwk, generateCryptoKeyPair, getActorHandle, importJwk, } from "@fedify/fedify"; ``` Now let's modify the code calling the `~Federation.setInboxListeners()` method as follows: ```typescript twoslash [src/federation.ts] import { Accept, Endpoints, type Federation, Follow, Person, exportJwk, generateCryptoKeyPair, getActorHandle, importJwk, } from "@fedify/fedify"; const federation = null as unknown as Federation; import type { Logger } from "@logtape/logtape"; const logger = null as unknown as Logger; import Database from "better-sqlite3"; const db = new Database(""); interface Actor { id: number; } // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .on(Follow, async (ctx, follow) => { if (follow.objectId == null) { logger.debug("The Follow object does not have an object: {follow}", { follow, }); return; } const object = ctx.parseUri(follow.objectId); if (object == null || object.type !== "actor") { logger.debug("The Follow object's object is not an actor: {follow}", { follow, }); return; } const follower = await follow.getActor(); if (follower?.id == null || follower.inboxId == null) { logger.debug("The Follow object does not have an actor: {follow}", { follow, }); return; } const followingId = db .prepare( ` SELECT * FROM actors JOIN users ON users.id = actors.user_id WHERE users.username = ? `, ) .get(object.identifier)?.id; if (followingId == null) { logger.debug( "Failed to find the actor to follow in the database: {object}", { object }, ); } const followerId = db .prepare( ` -- Add a new follower actor record or update if it already exists INSERT INTO actors (uri, handle, name, inbox_url, shared_inbox_url, url) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (uri) DO UPDATE SET handle = excluded.handle, name = excluded.name, inbox_url = excluded.inbox_url, shared_inbox_url = excluded.shared_inbox_url, url = excluded.url WHERE actors.uri = excluded.uri RETURNING * `, ) .get( follower.id.href, await getActorHandle(follower), follower.name?.toString(), follower.inboxId.href, follower.endpoints?.sharedInbox?.href, follower.url?.href, )?.id; db.prepare( "INSERT INTO follows (following_id, follower_id) VALUES (?, ?)", ).run(followingId, followerId); const accept = new Accept({ actor: follow.objectId, to: follow.actorId, object: follow, }); await ctx.sendActivity(object, follower, accept); }); ``` Let's examine the code carefully. The `~InboxListenerSetters.on()` method defines the action to take when a specific type of activity is received. Here, we've written code to record the follower information in the database when a `Follow` activity is received, and then send an `Accept(Follow)` activity back to the actor who sent the follow request. The `follow.objectId` should contain the URI of the actor being followed. We use the `~Context.parseUri()` method to check if the URI inside it points to the actor we created. The `getActorHandle()` function returns the fediverse handle as a string from the given actor object. If there's no information about the actor who sent the follow request in the `actors` table yet, we first add a record. If a record already exists, we update it with the latest data. Then, we add the follower to the `follows` table. Once the record is completed in the database, we use the `~Context.sendActivity()` method to send an `Accept(Follow)` activity as a reply to the actor who sent the activity. It takes the sender as the first parameter, the recipient as the second parameter, and the activity object to send as the third parameter. ### ActivityPub.Academy Now it's time to check if follow requests are being received properly. While it would be fine to test from a regular Mastodon server, let's use the [ActivityPub.Academy] server, which allows us to see exactly how activities are exchanged. ActivityPub.Academy is a special Mastodon server for education and debugging purposes, where you can easily create temporary accounts with just one click. ![ActivityPub.Academy homepage](./microblog/academy.jpg) After agreeing to the privacy policy, click the *Sign Up* button to create a new account. The created account will have a randomly generated name and handle, and will disappear on its own after a day. Instead, you can create new accounts as many times as you want. Once you're logged in, paste the handle of the actor we created into the search box in the top left corner of the screen: ![Search results for our actor's handle on ActivityPub.Academy](./microblog/academy-search-results.png) If our actor appears in the search results, click the follow button on the right to send a follow request. Then click on *Activity Log* in the right menu: ![ActivityPub.Academy's Activity Log](./microblog/activity-log.png) You'll see an indication that a `Follow` activity was sent from the ActivityPub.Academy server to the inbox of the actor we created by clicking the follow button just now. You can see the contents of the activity by clicking *show source* in the bottom right: ![Activity Log screen after clicking show source](./microblog/activity-log-2.png) [ActivityPub.Academy]: https://activitypub.academy/ ### Testing Now that we've confirmed that the activity was sent well, it's time to check if our inbox code is working properly. First, let's see if a record was created properly in the `follows` table: ```sh echo "SELECT * FROM follows;" | sqlite3 -table microblog.sqlite3 ``` If the follow request was processed successfully, you should see a result like this (of course, the time will be different): | `following_id` | `follower_id` | `created` | |----------------|---------------|-----------------------| | `1` | `2` | `2024-09-01 10:19:41` | Let's also check if a new record was created in the `actors` table: ```sh echo "SELECT * FROM actors WHERE id > 1;" | sqlite3 -table microblog.sqlite3 ``` | `id` | `user_id` | `uri` | `handle` | `name` | `inbox_url` | `shared_inbox_url` | `url` | `created` | |------|-----------|--------------------------------------------------------|-------------------------------------------|----------------------|--------------------------------------------------------------|------------------------------------|---------------------------------------------------|-----------------------| | `2` | | `https://activitypub.academy/users/dobussia_dovornath` | `@dobussia_dovornath@activitypub.academy` | `Dobussia Dovornath` | `https://activitypub.academy/users/dobussia_dovornath/inbox` | `https://activitypub.academy/inbox` | `https://activitypub.academy/@dobussia_dovornath` | `2024-09-01 10:19:41` | Now, let's look at ActivityPub.Academy's *Activity Log* again. If the `Accept(Follow)` activity sent by our actor arrived well, it should be displayed as follows: ![Accept(Follow) activity displayed in Activity Log](./microblog/activity-log-3.png) This way, you've implemented your first interaction via ActivityPub! ## Unfollow What happens if an actor from another server unfollows our actor after following it? Let's test this in [ActivityPub.Academy]. As before, enter our actor's fediverse handle in the ActivityPub.Academy search box: ![Search results in ActivityPub.Academy](./microblog/academy-search-results-2.png) If you look closely, you'll see an unfollow button in place of the follow button to the right of the actor name. Click this button to unfollow, then go to the *Activity Log* to see what activity is sent: ![Activity Log showing the sent Undo(Follow) activity](./microblog/activity-log-4.png) As you can see, an `Undo(Follow)` activity has been sent. If you click *show source* in the bottom right, you can see the detailed contents of the activity: ```json { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://activitypub.academy/users/dobussia_dovornath#follows/3283/undo", "type": "Undo", "actor": "https://activitypub.academy/users/dobussia_dovornath", "object": { "id": "https://activitypub.academy/98b131b8-89ea-49ba-b2bd-3ee0f5a87694", "type": "Follow", "actor": "https://activitypub.academy/users/dobussia_dovornath", "object": "https://temp-address.serveo.net/users/johndoe" } } ``` Looking at this JSON object, you can see that the `Undo(Follow)` activity includes the `Follow` activity that was received by our inbox earlier. However, since we haven't defined any behavior for when the inbox receives an `Undo(Follow)` activity, nothing has happened. ### Receiving `Undo(Follow)` Activity To implement unfollow, open the *src/federation.ts* file and `import` the `Undo` class provided by Fedify: ```typescript twoslash [src/federation.ts] import { Accept, Endpoints, Follow, Person, Undo, // [!code highlight] createFederation, exportJwk, generateCryptoKeyPair, getActorHandle, importJwk, } from "@fedify/fedify"; ``` Then add `on(Undo, ...)` in succession after `on(Follow, ...)`: ```typescript{6-23} twoslash [src/federation.ts] // @errors: 1160 import { type Federation, Follow, Undo } from "@fedify/fedify"; const federation = null as unknown as Federation; import Database from "better-sqlite3"; const db = new Database(""); // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .on(Follow, async (ctx, follow) => { // ... omitted ... }) .on(Undo, async (ctx, undo) => { const object = await undo.getObject(); if (!(object instanceof Follow)) return; if (undo.actorId == null || object.objectId == null) return; const parsed = ctx.parseUri(object.objectId); if (parsed == null || parsed.type !== "actor") return; db.prepare( ` DELETE FROM follows WHERE following_id = ( SELECT actors.id FROM actors JOIN users ON actors.user_id = users.id WHERE users.username = ? ) AND follower_id = (SELECT id FROM actors WHERE uri = ?) `, ).run(parsed.identifier, undo.actorId.href); }); ``` This time, the code is shorter than when processing follow requests. It checks if the thing inside the `Undo(Follow)` activity is a `Follow` activity, uses the `parseUri()` method to check if the follow target of the `Follow` activity to be canceled is our actor, and then deletes the corresponding record from the `follows` table. ### Testing We can't unfollow once more since we already clicked the unfollow button in [ActivityPub.Academy] earlier. We'll have to follow again and then unfollow to test. But before that, we need to empty the `follows` table. Otherwise, there will be an error when the follow request comes in because the record already exists. Let's empty the `follows` table using the `sqlite3` command: ```sh echo "DELETE FROM follows;" | sqlite3 microblog.sqlite3 ``` Now press the follow button again, then check the database: ```sh echo "SELECT * FROM follows;" | sqlite3 -table microblog.sqlite3 ``` If the follow request was processed successfully, you should see a result like this: | `following_id` | `follower_id` | `created` | |----------------|---------------|-----------------------| | `1` | `2` | `2024-09-02 01:05:17` | Now press the unfollow button again, then check the database one more time: ```sh echo "SELECT count(*) FROM follows;" | sqlite3 -table microblog.sqlite3 ``` If the unfollow request was processed successfully, the record should have disappeared, so you should see a result like this: | `count(*)` | |------------| | `0` | ## Followers list It's cumbersome to view the followers list using the `sqlite3` command every time, so let's make it possible to view the followers list on the web. Let's start by adding a new component to the *src/views.tsx* file. First, `import` the Actor type: ```typescript twoslash [src/views.tsx] // @noErrors: 2307 import type { Actor } from "./schema.ts"; ``` Then define the `` component and the `` component: ```tsx twoslash [src/views.tsx] import type { FC } from "hono/jsx"; interface Actor { id: number; uri: string; name: string | null; handle: string; url: string | null; } // ---cut-before--- export interface FollowerListProps { followers: Actor[]; } export const FollowerList: FC = ({ followers }) => ( <>

Followers

    {followers.map((follower) => (
  • ))}
); export interface ActorLinkProps { actor: Actor; } export const ActorLink: FC = ({ actor }) => { const href = actor.url ?? actor.uri; return actor.name == null ? (
{actor.handle} ) : ( <> {actor.name}{" "} ( {actor.handle} ) ); }; ``` The `` component is used to represent a single actor, and the `` component uses the `` component to represent the list of followers. As you can see, since JSX doesn't have conditional statements or loops, we're using the ternary operator and the [`Array.map()`] method. Now let's create an endpoint to display the followers list. Open the *src/app.tsx* file and `import` the `` component: ```typescript twoslash [src/app.tsx] // @noErrors: 2307 import { FollowerList, Layout, Profile, SetupForm } from "./views.tsx"; ``` Then add a request handler for `GET /users/{username}/followers`: ```tsx twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export const FollowerList: FC = () => <>; import Database from "better-sqlite3"; const db = new Database(""); interface Actor {} // ---cut-before--- app.get("/users/:username/followers", async (c) => { const followers = db .prepare( ` SELECT followers.* FROM follows JOIN actors AS followers ON follows.follower_id = followers.id JOIN actors AS following ON follows.following_id = following.id JOIN users ON users.id = following.user_id WHERE users.username = ? ORDER BY follows.created DESC `, ) .all(c.req.param("username")); return c.html( , ); }); ``` Now, shall we check if it's displaying correctly? There should be followers, so with `fedify tunnel` running, follow our actor from another Mastodon server or [ActivityPub.Academy]. After the follow request is accepted, open the page in your web browser, and you should see something like this: ![Followers list page](./microblog/followers-list.png) Now that we've created the followers list, it would be nice to display the number of followers on the profile page as well. Open the *src/views.tsx* file again and modify the `` component as follows: ```tsx{20-23} twoslash [src/views.tsx] import type { FC } from "hono/jsx"; // ---cut-before--- export interface ProfileProps { name: string; username: string; // [!code highlight] handle: string; followers: number; // [!code highlight] } export const Profile: FC = ({ name, username, // [!code highlight] handle, followers, // [!code highlight] }) => ( <>

{name}

{handle} ·{" "} {followers === 1 ? "1 follower" : `${followers} followers`}

); ``` Two props have been added to `ProfileProps`. `followers` is a prop that holds the number of followers, as the name suggests. `username` receives the username that will go into the URL to link to the followers list. Now go back to the *src/app.tsx* file and modify the `GET /users/{username}` request handler as follows: ```tsx{5-15,21,23} twoslash [src/app.tsx] import { Hono } from "hono"; const app = new Hono(); import type { FC } from "hono/jsx"; export const Layout: FC = (props) => ; export interface ProfileProps { name: string; username: string; handle: string; followers: number; } export const Profile: FC = () => <>; import Database from "better-sqlite3"; const db = new Database(""); interface User { id: number; username: string; } interface Actor { name: string; } const user = {} as unknown as User & Actor; const handle = "" as string; // ---cut-before--- app.get("/users/:username", async (c) => { // ... omitted ... if (user == null) return c.notFound(); // biome-ignore lint/style/noNonNullAssertion: Always returns a single record const { followers } = db .prepare( ` SELECT count(*) AS followers FROM follows JOIN actors ON follows.following_id = actors.id WHERE actors.user_id = ? `, ) .get(user.id)!; // ... omitted ... return c.html( , ); }); ``` SQL that counts the number of records in the `follows` table in the database has been added. Now, let's check the changed profile page. When you open the page in your web browser, you should see something like this: ![Changed profile page](./microblog/profile-page-3.png) [`Array.map()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map ## Followers collection However, there's one problem. Let's look up our actor from a Mastodon server that is *not* ActivityPub.Academy. (You know how to look it up, right? With the server exposed to the public internet, enter the actor's handle in the Mastodon search box.) When you view our actor's profile in Mastodon, you might notice something strange: ![Our actor's profile viewed in Mastodon](./microblog/remote-profile-2.png) The number of followers is shown as 0. This is because our actor is not exposing the followers list via ActivityPub. To expose the followers list in ActivityPub, we need to define a followers collection. Open the *src/federation.ts* file and `import` the `Recipient` type provided by Fedify: ```typescript twoslash [src/federation.ts] import { Accept, Endpoints, Follow, Person, Undo, createFederation, exportJwk, generateCryptoKeyPair, getActorHandle, importJwk, type Recipient, // [!code highlight] } from "@fedify/fedify"; ``` Then add a followers collection dispatcher at the bottom: ```typescript twoslash [src/federation.ts] import { type Federation, type Recipient } from "@fedify/fedify"; const federation = null as unknown as Federation; import Database from "better-sqlite3"; const db = new Database(""); interface Actor { uri: string; inbox_url: string; shared_inbox_url: string | null; } // ---cut-before--- federation .setFollowersDispatcher( "/users/{identifier}/followers", (ctx, identifier, cursor) => { const followers = db .prepare( ` SELECT followers.* FROM follows JOIN actors AS followers ON follows.follower_id = followers.id JOIN actors AS following ON follows.following_id = following.id JOIN users ON users.id = following.user_id WHERE users.username = ? ORDER BY follows.created DESC `, ) .all(identifier); const items: Recipient[] = followers.map((f) => ({ id: new URL(f.uri), inboxId: new URL(f.inbox_url), endpoints: f.shared_inbox_url == null ? null : { sharedInbox: new URL(f.shared_inbox_url) }, })); return { items }; }, ) .setCounter((ctx, identifier) => { const result = db .prepare( ` SELECT count(*) AS cnt FROM follows JOIN actors ON actors.id = follows.following_id JOIN users ON users.id = actors.user_id WHERE users.username = ? `, ) .get(identifier); return result == null ? 0 : result.cnt; }); ``` The `~Federation.setFollowersDispatcher()` method creates a followers collection object to respond to when a `GET /users/{identifier}/followers` request comes in. Although the SQL is a bit long, it essentially gets the list of actors following the actor with the `identifier` parameter. The `items` contains `Recipient` objects, and the `Recipient` type looks like this: ```typescript twoslash export interface Recipient { readonly id: URL | null; readonly inboxId: URL | null; readonly endpoints?: { sharedInbox: URL | null; } | null; } ``` The `id` property contains the actor's unique IRI, and `inboxId` contains the URL of the actor's personal inbox. `endpoints.sharedInbox` contains the URL of the actor's shared inbox. Since we have all that information in our `actors` table, we can fill the `items` array with that information. The `~CollectionCallbackSetters.setCounter()` method gets the total number of the followers collection. Here too, the SQL is a bit complex, but in summary, it's counting the number of actors following the actor with the `identifier` parameter. Now, let's check if the followers collection is working properly by using the `fedify lookup` command: ```sh fedify lookup http://localhost:8000/users/johndoe/followers ``` If implemented correctly, you should see a result like this: ```console ✔ Looking up the object... OrderedCollection { totalItems: 1, items: [ URL "https://activitypub.academy/users/dobussia_dovornath" ] } ``` However, just creating a followers collection like this doesn't let other servers know where the followers collection is. So we need to link to the followers collection in the actor dispatcher: ```typescript twoslash [src/federation.ts] import { type Federation, Person } from "@fedify/fedify"; const federation = null as unknown as Federation; // ---cut-before--- federation .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // ... omitted ... return new Person({ // ... omitted ... followers: ctx.getFollowersUri(identifier), // [!code highlight] }); }) ``` Let's look up the actor with `fedify lookup` again: ```sh fedify lookup http://localhost:8000/users/johndoe ``` If you see a `"followers"` property included in the result as shown below, it's correct: ```console ✔ Looking up the object... Person { ... omitted ... inbox: URL "http://localhost:8000/users/johndoe/inbox", followers: URL "http://localhost:8000/users/johndoe/followers", endpoints: Endpoints { sharedInbox: URL "http://localhost:8000/inbox" } } ``` Now, let's look up our actor in Mastodon again. But the result might be a bit disappointing: ![Our actor's profile viewed again in Mastodon](./microblog/remote-profile-2.png) The number of followers is still shown as 0. This is because Mastodon caches information about actors from other servers. There are ways to update this, but they're not as easy as pressing the F5 key: * One way is to wait for a week. Mastodon clears the cache that holds information about actors from other servers 7 days after the last update. * Another way is to send an `Update` activity, but this requires tedious coding. * Or you could try looking it up from another Mastodon server where the cache hasn't been created yet. * The last method is to turn off and on `fedify tunnel` to get a new temporary domain assigned. If you want to see the correct number of followers displayed on another Mastodon server, try one of the methods I've listed. ## Posts Now, it's finally time to implement posts. Unlike a typical blog, the microblog we're creating should be able to store posts created on other servers as well. Let's design with this in mind. ### Table creation Let's start by creating a `posts` table. Open the *src/schema.sql* file and add the following SQL: ```sql [src/schema.sql] CREATE TABLE IF NOT EXISTS posts ( id INTEGER NOT NULL PRIMARY KEY, uri TEXT NOT NULL UNIQUE CHECK (uri <> ''), actor_id INTEGER NOT NULL REFERENCES actors (id), content TEXT NOT NULL, url TEXT CHECK (url LIKE 'https://%' OR url LIKE 'http://%'), created TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) CHECK (created <> '') ); ``` * The `id` column is the primary key of the table. * The `uri` column holds the unique URI of the post. As mentioned earlier, all ActivityPub objects must have a unique URI. * The `actor_id` column points to the actor who wrote the post. * The `content` column contains the content of the post. * The `url` column contains the URL where the post is displayed in a web browser. There are cases where the URI of an ActivityPub object and the URL of the page displayed in a web browser match, but there are also cases where they don't, so a separate column is necessary. However, it can be empty. * The `created` column contains the time the post was created. Let's execute the SQL to create the `posts` table: ```sh sqlite3 microblog.sqlite3 < src/schema.sql ``` Also define a `Post` type in the *src/schema.ts* file to represent records that will be stored in the `posts` table in JavaScript: ```typescript twoslash [src/schema.ts] export interface Post { id: number; uri: string; actor_id: number; content: string; url: string | null; created: string; } ``` ### Home page To write a post, there needs to be a form somewhere, right? Come to think of it, we haven't properly created the home page yet. Let's add a post creation form to the home page. First, open the *src/views.tsx* file and `import` the `User` type: ```typescript twoslash [src/views.tsx] // @noErrors: 2307 import type { Actor, User } from "./schema.ts"; ``` Then define the `` component: ```tsx twoslash [src/views.tsx] import type { FC } from "hono/jsx"; interface User { username: string; } interface Actor { name: string; } // ---cut-before--- export interface HomeProps { user: User & Actor; } export const Home: FC = ({ user }) => ( <>

{user.name}'s microblog

{user.name}'s profile