Skip to content

Inbox listeners

In ActivityPub, an inbox is where an actor receives incoming activities from other actors. Fedify provides a way to register inbox listeners so that you can handle incoming activities from other actors.

Registering an inbox listener

An inbox is basically an HTTP endpoint that receives webhook requests from other servers. There are two types of inboxes in ActivityPub: the shared inbox and the personal inbox. The shared inbox is a single inbox that receives activities for all actors in the server, while the personal inbox is an inbox for a specific actor.

With Fedify, you can register an inbox listener for both types of inboxes at a time. The following shows how to register an inbox listener:

typescript
import { 
createFederation
,
Accept
,
Follow
} from "@fedify/fedify";
const
federation
=
createFederation
({
// Omitted for brevity; see the related section for details. });
federation
.
setInboxListeners
("/users/{identifier}/inbox", "/inbox")
.
on
(
Follow
, async (
ctx
,
follow
) => {
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
},
recipient
,
new
Accept
({
actor
:
follow
.
objectId
,
object
:
follow
}),
); });

In the above example, the setInboxListeners() method registers path patterns for the personal inbox and the shared inbox, and the following on() method registers an inbox listener for the Follow activity. The on() method takes a class of the activity and a callback function that takes a Context object and the activity object.

Note that the on() method can be chained to register multiple inbox listeners for different activity types.

WARNING

Activities of any type that are not registered with the on() method are silently ignored. If you want to catch all types of activities anyway, add a listener for the Activity class.

TIP

You can get a personal or shared inbox URI by calling the getInboxUri() method. It takes an optional parameter identifier to get the personal inbox URI for the actor with the given identifier. If the identifier parameter is not provided, the method returns the shared inbox URI.

Determining the recipient of an activity

Looking at the to, cc, bto, and bcc fields

When you receive an activity, you may want to determine the recipient of the activity. The recipient is usually the actor who is mentioned in the to, cc, bto, or bcc field of the activity. The following shows how to determine the recipient of a Create activity:

typescript
.
on
(
Create
, async (
ctx
,
create
) => {
if (
create
.
toId
== null) return;
const
to
=
ctx
.
parseUri
(
create
.
toId
);
if (
to
?.
type
!== "actor") return;
const
recipient
=
to
.
identifier
;
// Do something with the recipient });

The to, cc, bto, and bcc fields can contain multiple recipients, so you may need to iterate over them to determine the recipient of the activity:

typescript
.
on
(
Create
, async (
ctx
,
create
) => {
for (const
toId
of
create
.
toIds
) {
const
to
=
ctx
.
parseUri
(
toId
);
if (
to
?.
type
!== "actor") continue;
const
recipient
=
to
.
identifier
;
// Do something with the recipient } });

Also, the to, cc, bto, and bcc fields can contain both actor and collection objects. In such cases, you may need to recursively resolve the collection objects to determine the recipients of the activity:

typescript
.
on
(
Create
, async (
ctx
,
create
) => {
for await (const
to
of
create
.
getTos
()) {
if (
isActor
(
to
)) {
// `to` is a recipient of the activity // Do something with the recipient } else if (
to
instanceof
Collection
) {
// `to` is a collection object for await (const
actor
of
to
.
getItems
()) {
if (!
isActor
(
actor
)) continue;
// `actor` is a recipient of the activity // Do something with the recipient } } } });

TIP

It might look strange, non-scalar accessor methods for to, cc, bto, and bcc fields are named as getTos(), getCcs(), getBtos(), and getBccs(), respectively.

Looking at the InboxContext.recipient property

This API is available since Fedify 1.2.0.

However, the to, cc, bto, and bcc fields are not always present in an activity. In such cases, you can determine the recipient by looking at the InboxContext.recipient property. The below example shows how to determine the recipient of a Follow activity:

typescript
.
on
(
Follow
, async (
ctx
,
follow
) => {
const
recipient
=
ctx
.
recipient
;
// Do something with the recipient });

The recipient property is set to the identifier of the actor who is the recipient of the activity. If the invocation is not for a personal inbox, but for a shared inbox, the recipient property is set to null.

Context.documentLoader on an inbox listener

The Context.documentLoader property carries a DocumentLoader object that you can use to fetch a remote document. If a request is made to a shared inbox, the Context.documentLoader property is set to the default documentLoader that is specified in the createFederation() function. However, if a request is made to a personal inbox, the Context.documentLoader property is set to an authenticated DocumentLoader object that is identified by the inbox owner's key.

This means that you can pass the Context object to dereferencing accessors[1] inside a personal inbox listener so that they can fetch remote documents with the correct authentication.

Shared inbox key dispatcher

This API is available since Fedify 0.11.0.

TIP

We highly recommend configuring the shared inbox key dispatcher to avoid potential incompatibility issues with ActivityPub servers that require authorized fetch (i.e., secure mode).

If you want to use an authenticated DocumentLoader object as the Context.documentLoader for a shared inbox, you can set the identity for the authentication using setSharedKeyDispatcher() method. For example, the following shows how to implement the instance actor pattern:

typescript
import { 
Application
,
Person
} from "@fedify/fedify";
federation
.
setInboxListeners
("/users/{identifier}/inbox", "/inbox")
// The following line assumes that there is an instance actor named `~actor` // for the server. The leading tilde (`~`) is just for avoiding conflicts // with regular actor handles, but you don't have to necessarily follow this // convention: .
setSharedKeyDispatcher
((
_ctx
) => ({
identifier
: "~actor" }));
federation
.
setActorDispatcher
("/users/{identifier}", async (
ctx
,
identifier
) => {
if (
identifier
=== "~actor") {
// Returns an Application object for the instance actor: return new
Application
({
// ... }); } // Fetches the regular actor from the database and returns a Person object: return new
Person
({
// ... }); });

Or you can manually configure the key pair instead of referring to an actor by its identifier:

typescript
import { 
importJwk
} from "@fedify/fedify";
interface InstanceActor {
privateKey
: JsonWebKey;
publicKeyUri
: string;
}
federation
.
setInboxListeners
("/users/{identifier}/inbox", "/inbox")
.
setSharedKeyDispatcher
(async (
_ctx
) => {
// The following getInstanceActor() is just a hypothetical function that // fetches information about the instance actor from a database or some // other storage: const
instanceActor
: InstanceActor = await
getInstanceActor
();
return {
privateKey
: await
importJwk
(
instanceActor
.
privateKey
, "private"),
keyId
: new
URL
(
instanceActor
.
publicKeyUri
),
}; });

NOTE

If a shared inbox key dispatcher returns null, the default documentLoader, which is not authenticated, is used for the shared inbox.

Making inbox listeners non-blocking

This API is available since Fedify 0.12.0.

Usually, processes inside an inbox listener should be non-blocking because they may involve long-running tasks. Fortunately, you can easily turn inbox listeners into non-blocking by providing a queue option to createFederation() function:

typescript
import { 
createFederation
,
InProcessMessageQueue
} from "@fedify/fedify";
const
federation
=
createFederation
({
// Omitted for brevity; see the related section for details.
queue
: new
InProcessMessageQueue
(),
});

NOTE

The InProcessMessageQueue is a simple in-memory message queue that is suitable for development and testing. For production use, you should consider using a more robust message queue, such as DenoKvMessageQueue from @fedify/fedify/x/deno module or RedisMessageQueue from @fedify/redis package.

For more information, see the Message queue section.

If it is not present, incoming activities are processed immediately and block the response to the sender until the processing is done.

While the queue option is not mandatory, it is highly recommended to use it in production environments to prevent the server from being overwhelmed by incoming activities.

With the queue enabled, the failed activities are automatically retried after a certain period of time. The default retry strategy is exponential backoff with a maximum of 10 retries, but you can customize it by providing an inboxRetryPolicy option to the createFederation() function.

NOTE

Activities with invalid signatures/proofs are silently ignored and not queued.

TIP

If your inbox listeners are mostly I/O-bound, consider parallelizing message processing by using the ParallelMessageQueue class. For more information, see the Parallel message processing section.

If your inbox listeners are CPU-bound, consider running multiple nodes of your application so that each node can process messages in parallel with the shared message queue.

Error handling

Since an incoming activity can be malformed or invalid, you may want to handle such cases. Also, your listener itself may throw an error. The onError() method registers a callback function that takes a Context object and an error object. The following shows an example of handling errors:

typescript
federation
.
setInboxListeners
("/users/{identifier}/inbox", "/inbox")
.
on
(
Follow
, async (
ctx
,
follow
) => {
// Omitted for brevity }) .
onError
(async (
ctx
,
error
) => {
console
.
error
(
error
);
});

NOTE

Activities with invalid signatures/proofs are silently ignored and not passed to the error handler.

Forwarding activities to another server

This API is available since Fedify 1.0.0.

Sometimes, you may want to forward incoming activities to another server. For example, you may want to forward Flag activities to a moderation server. Or you may want to forward Create activities which reply to your server to your followers so that they can see the replies.

The problem is that the recipients of the forwarded activities will not trust the forwarded activities unless they are signed by the original sender, not by you. You might think that you can just sendActivity() the received activity to the recipient in your inbox listener, but it doesn't work because the signature made by the original sender is stripped when the received activity is passed to the inbox listener, and sendActivity() will sign the activity with your key.

To solve this problem, you can use the forwardActivity() method in your inbox listener. It forwards the received activity without any modification, so the signature made by the original sender is preserved (if the activity is signed using by the original sender).

The following shows an example of forwarding Create activities to followers:

typescript
.
on
(
Create
, async (
ctx
,
create
) => {
if (
create
.
toId
== null) return;
const
to
=
ctx
.
parseUri
(
create
.
toId
);
if (
to
?.
type
!== "actor") return;
const
forwarder
=
to
.
identifier
;
await
ctx
.
forwardActivity
({
identifier
:
forwarder
}, "followers");
})

NOTE

The forwardActivity() method does not guarantee that the forwarded activity is successfully delivered to the recipient, since the original sender might neither sign the activity using Linked Data Signatures nor Object Integrity Proofs. In such cases, the recipient probably won't trust the forwarded activity.[2]

If you don't want to forward unsigned activities, you can turn on the skipIfUnsigned option in the forwardActivity() method:

typescript
await 
ctx
.
forwardActivity
(
{
identifier
: "alice" },
"followers", {
skipIfUnsigned
: true },
);

Constructing inbox URIs

To construct an inbox URI, you can use the getInboxUri() method. This method optionally takes an identifier of an actor and returns a dereferenceable URI of the inbox of the actor. If no argument is provided, the method returns the shared inbox URI.

The following shows how to construct an inbox URI of an actor identified by 5fefc9bb-397d-4949-86bb-33487bf233fb:

typescript
ctx
.
getInboxUri
("5fefc9bb-397d-4949-86bb-33487bf233fb")

NOTE

The getInboxUri() method does not guarantee that the inbox 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.

The following shows how to construct a shared inbox URI:

typescript
ctx
.
getInboxUri
()

Manual routing

This API is available since Fedify 1.3.0.

If you want to manually route an activity to the appropriate inbox listener with no actual HTTP request, you can use the Context.routeActivity() method. The method takes an identifier of the recipient (or null for the shared inbox) and an Activity object to route. The point of this method is that it verifies if the Activity object is made by the its actor, and unless it is, the method silently ignores the activity.

The following code shows how to route an Activity object enclosed in top-level Announce object to the corresponding inbox listener:

typescript
  .
on
(
Announce
, async (
ctx
,
announce
) => {
// Get an object enclosed in the `Announce` object: const
object
= await
announce
.
getObject
();
if (
object
instanceof
Activity
) {
// Route the activity to the appropriate inbox listener (shared inbox): await
ctx
.
routeActivity
(
ctx
.
recipient
,
object
);
} })

As another example, the following code shows how to invoke the corresponding inbox listeners for a remote actor's activities:

typescript
const 
actor
= await
context
.
lookupObject
("@hongminhee@fosstodon.org");
if (!
isActor
(
actor
)) return;
const
collection
= await
actor
.
getOutbox
();
if (
collection
== null) return;
for await (const
item
of
context
.
traverseCollection
(
collection
)) {
if (
item
instanceof
Activity
) {
await
context
.
routeActivity
(null,
item
);
} }

TIP

The Context.routeActivity() method trusts the Activity object only if one of the following conditions is met:

  • The Activity has its Object Integrity Proofs and the proofs are signed by its actor.

  • The Activity is dereferenceable by its id and the dereferenced object has an actor that belongs to the same origin as the Activity object.


  1. See the Object IDs and remote objects section if you are not familiar with dereferencing accessors. ↩︎

  2. Some implementations may try to verify the unsigned activity by fetching the original object from the original sender's server even if they don't trust the forwarded activity. However, it is not guaranteed that all implementations do so. ↩︎