Skip to content

Sending activities

In ActivityPub, an actor can deliver an activity to another actor by sending an HTTP POST request to the recipient's inbox. Fedify provides an abstracted way to send activities to other actors' inboxes.

Prerequisite: actor key pairs

Before sending an activity to another actor, you need to have the sender's key pairs. The key pairs are used to sign the activity so that the recipient can verify the sender's identity. The key pairs can be registered by calling setKeyPairsDispatcher() method.

For more information about this topic, see Public keys of an Actor section in the Actor dispatcher section.

Sending an activity

To send an activity to another actor, you can use the Context.sendActivity() method. The following shows how to send a Follow activity to another actor:

typescript
import { type 
Context
,
Follow
, type Recipient } from "@fedify/fedify";
async function
sendFollow
(
ctx
:
Context
<void>,
senderId
: string,
recipient
: Recipient,
) { await
ctx
.
sendActivity
(
{
identifier
:
senderId
},
recipient
,
new
Follow
({
actor
:
ctx
.
getActorUri
(
senderId
),
object
:
recipient
.
id
,
}), ); }

TIP

Wonder where you can acquire a Context object? See the Where to get a Context object section in the Context section.

Specifying a sender

The first argument of the sendActivity() method is the sender of the activity. It can be three types of values:

{ identifier: string }

If you specify an object with the identifier property, the sender is the actor with the given identifier. The identifier is used to find the actor's key pairs to sign the activity:

typescript
await 
ctx
.
sendActivity
(
{
identifier
: "2bd304f9-36b3-44f0-bf0b-29124aafcbb4" },
"followers",
activity
,
);

{ username: string }

If you specify an object with the username property, the sender is the actor with the given WebFinger username. The username is used to find the actor's key pairs to sign the activity:

typescript
await 
ctx
.
sendActivity
(
{
username
: "john" },
"followers",
activity
,
);

If you don't decouple the username from the identifier, this is the same as the { identifier: string } case.

SenderKeyPair | SenderKeyPair[]

If you specify a SenderKeyPair object or an array of SenderKeyPair objects, the sender is the set of the given key pairs:

typescript
await 
ctx
.
sendActivity
(
await
ctx
.
getActorKeyPairs
("2bd304f9-36b3-44f0-bf0b-29124aafcbb4"),
recipients
, // You need to specify the recipients manually
activity
,
);

However, you probably don't want to use this option directly; instead, you should use above two options to specify the sender.

Enqueuing an outgoing activity

The delivery failure can happen for various reasons, such as network failure, recipient server failure, and so on. For reliable delivery, Fedify enqueues an outgoing activity to the queue instead of immediately sending it to the recipient's inbox if possible; the system retries the delivery on failure.

This queueing mechanism is enabled only if a queue option is set to the 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 further information, see the Message queue section.

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 outboxRetryPolicy option to the createFederation() function.

If the queue is not set, the sendActivity() method immediately sends the activity to the recipient's inbox. If the delivery fails, it throws an error and does not retry the delivery.

Immediately sending an activity

Sometimes you may want to send an activity immediately without queueing it. You can do this by calling the sendActivity() method with the immediate option:

typescript
import { type 
Context
,
Follow
, type Recipient } from "@fedify/fedify";
async function
sendFollow
(
ctx
:
Context
<void>,
senderId
: string,
recipient
: Recipient,
) { await
ctx
.
sendActivity
(
{
identifier
:
senderId
},
recipient
,
new
Follow
({
actor
:
ctx
.
getActorUri
(
senderId
),
object
:
recipient
.
id
,
}), {
immediate
: true },
); }

Shared inbox delivery

The shared inbox delivery is an efficient way to deliver an activity to multiple recipients belonging to the same server at once. It is useful for broadcasting activities, such as a public post.

By default, sendActivity() method delivers an activity to the recipient's personal inbox. To deliver an activity to the shared inbox, you can pass the preferSharedInbox option:

typescript
import {
  type 
Context
,
Create
,
Note
,
type Recipient,
PUBLIC_COLLECTION
,
} from "@fedify/fedify"; async function
sendNote
(
ctx
:
Context
<void>,
senderId
: string,
recipient
: Recipient,
) { await
ctx
.
sendActivity
(
{
identifier
:
senderId
},
recipient
,
new
Create
({
actor
:
ctx
.
getActorUri
(
senderId
),
to
:
PUBLIC_COLLECTION
,
object
: new
Note
({
attribution
:
ctx
.
getActorUri
(
senderId
),
to
:
PUBLIC_COLLECTION
,
}), }), {
preferSharedInbox
: true },
); }

TIP

PUBLIC_COLLECTION constant contains a URL object of https://www.w3.org/ns/activitystreams#Public, a special IRI that represents the public audience. By setting the to property to this IRI, the activity is visible to everyone. See also the Public Addressing section in the ActivityPub specification.

NOTE

To deliver an activity to the shared inbox, the recipient server must support the shared inbox delivery. Otherwise, Fedify silently falls back to the personal inbox delivery.

Followers collection synchronization

This API is available since Fedify 0.8.0.

NOTE

For efficiency, you should implement filtering-by-server of the followers collection, otherwise the synchronization may be slow.

If an activity needs to be delivered to only followers of the sender through the shared inbox, the server of the recipients has to be aware of the list of followers residing on the server. However, synchronizing the followers collection every time an activity is sent is inefficient. To solve this problem, Mastodon, etc., use a mechanism called followers collection synchronization.

The idea is to send a digest of the followers collection with the activity so that the recipient server can check if it needs to resynchronize the followers collection. Fedify provides a way to include the digest of the followers collection in the activity delivery request by specifying the recipients parameter of the sendActivity() method as the "followers" string:

typescript
await 
ctx
.
sendActivity
(
{
identifier
:
senderId
},
"followers", new
Create
({
actor
:
ctx
.
getActorUri
(
senderId
),
to
:
ctx
.
getFollowersUri
(
senderId
),
object
: new
Note
({
attribution
:
ctx
.
getActorUri
(
senderId
),
to
:
ctx
.
getFollowersUri
(
senderId
),
}), }), {
preferSharedInbox
: true },
);

If you specify the "followers" string as the recipients parameter, it automatically sends the activity to the sender's followers and includes the digest of the followers collection in the payload.

NOTE

The to and cc properties of an Activity and its object should be set to the followers collection IRI to ensure that the activity is visible to the followers. If you set the to and cc properties to the PUBLIC_COLLECTION, the activity is visible to everyone regardless of the recipients parameter.

Excluding same-server recipients

This API is available since Fedify 0.9.0.

Usually, you don't want to send messages through ActivityPub to followers on the same server because they share the same database, so there's no need to.

For example, if @foo@example.com creates a post, it's already stored in the database at example.com, so there's no need to send a Create(Note) activity to @bar@example.com, because @bar@example.com already has access to the post in the database.

To exclude same-server recipients, you can pass the excludeBaseUris option to the sendActivity() method:

typescript
await 
ctx
.
sendActivity
(
{
identifier
:
senderId
},
"followers",
activity
,
{
excludeBaseUris
: [
ctx
.
getInboxUri
()] },
);

Excluded recipients do not receive the activity, even if they are included in the recipients parameter.

NOTE

Only the origin parts of the specified URIs are compared with the inbox URLs of the recipients. Even if they have pathname or search parts, they are ignored when comparing the URIs.

Error handling

This API is available since Fedify 0.6.0.

Since an outgoing activity is not immediately processed, but enqueued to the queue, the sendActivity() method does not throw an error even if the delivery fails. Instead, the delivery failure is reported to the queue and retried later.

If you want to handle the delivery failure, you can register an error handler to the queue:

typescript
import { 
createFederation
,
InProcessMessageQueue
} from "@fedify/fedify";
const
federation
=
createFederation
({
// Omitted for brevity; see the related section for details.
queue
: new
InProcessMessageQueue
(),
onOutboxError
: (
error
,
activity
) => {
console
.
error
("Failed to deliver an activity:",
error
);
console
.
error
("Activity:",
activity
);
}, });

NOTE

The onOutboxError callback can be called multiple times for the same activity, because the delivery is retried according to the backoff schedule until it succeeds or reaches the maximum retry count.

HTTP Signatures

HTTP Signatures is a de facto standard for signing ActivityPub activities. It is widely used in the fediverse to verify the sender's identity and the integrity of the activity.

Fedify automatically signs activities with the sender's private key if the actor keys dispatcher is set and the actor has any RSA-PKCS#1-v1.5 key pair. If there are multiple key pairs, Fedify selects the first RSA-PKCS#1-v1.5 key pair among them.

Linked Data Signatures

This API is available since Fedify 1.0.0.

Linked Data Signatures is a more advanced and widely used, but obsolete, mechanism for signing portable ActivityPub activities. As of November 2024, major ActivityPub implementations, such as Mastodon, et al., still rely on Linked Data Signatures for signing portable activities, despite they declare that Linked Data Signatures is outdated.

It shares the similar concept with HTTP Signatures, but unlike HTTP Signatures, it can be used for signing portable activities. For example, it can be used for forwarding from inbox and several other cases that HTTP Signatures cannot handle.

Fedify automatically includes the Linked Data Signature of activities by signing them with the sender's private key if the actor keys dispatcher is set and the actor has any RSA-PKCS#1-v1.5 key pair. If there are multiple key pairs, Fedify uses the first RSA-PKCS#1-v1.5 key pair among them.

TIP

The combination of HTTP Signatures and Linked Data Signatures is the most widely supported way to sign activities in the fediverse, as of September 2024. Despite Linked Data Signatures is outdated and not recommended for new implementations, it is still widely used in the fediverse due to Mastodon and other major implementations' reliance on it.

However, for new implementations, you should consider using both Object Integrity Proofs and Linked Data Signatures for maximum compatibility and future-proofing. Fortunately, Fedify supports both Object Integrity Proofs and Linked Data Signatures simultaneously, in addition to HTTP Signatures.

NOTE

If an activity is signed with both HTTP Signatures and Linked Data Signatures, the recipient verifies the Linked Data Signatures first when it is supported, and ignores the HTTP Signatures if the Linked Data Signatures are valid. If the recipient does not support Linked Data Signatures, it falls back to verifying the HTTP Signatures.

Object Integrity Proofs

This API is available since Fedify 0.10.0.

Object Integrity Proofs is a mechanism to ensure the integrity of ActivityPub objects (not only activities!) in the fediverse. It shares the similar concept with Linked Data Signatures, but it has more functionalities and is more flexible. However, as it is relatively new, it is not widely supported yet.

Fedify automatically includes the integrity proof of activities by signing them with the sender's private key if the actor keys dispatcher is set and the actor has any Ed25519 key pair. If there are multiple key pairs, Fedify creates the number of integrity proofs equal to the number of Ed25519 key pairs.

TIP

HTTPS Signatures, Linked Data Signatures, and Object Integrity Proofs can coexist in an application and be used together for maximum compatibility.

If an activity is signed with HTTP Signatures, Linked Data Signatures, and Object Integrity Proofs, the recipient verifies the Object Integrity Proofs first when it is supported, and ignores the HTTP Signatures and Linked Data Signatures if the Object Integrity Proofs are valid. If the recipient does not support Object Integrity Proofs, it falls back to verifying the HTTP Signatures and Linked Data Signatures.

To support HTTP Signatures, Linked Data Signatures, and Object Integrity Proofs simultaneously, you need to generate both RSA-PKCS#1-v1.5 and Ed25519 key pairs for each actor, and store them in the database.