Skip to main content

Sending Activities

This guide covers how to create and deliver activities to other servers. You'll learn about activity construction, addressing, signing, and reliable delivery.

Creating Activities​

Basic Activity Structure​

function createActivity(type, actor, object, recipients) {
return {
'@context': 'https://www.w3.org/ns/activitystreams',
type,
id: `https://${DOMAIN}/activities/${uuid()}`,
actor,
object,
published: new Date().toISOString(),
to: recipients.to || [],
cc: recipients.cc || []
};
}

Creating a Post​

async function createPost(username, content, options = {}) {
const actor = `https://${DOMAIN}/users/${username}`;

// Create the Note object
const note = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
id: `https://${DOMAIN}/notes/${uuid()}`,
attributedTo: actor,
content: sanitizeHtml(content),
published: new Date().toISOString(),
to: options.to || ['https://www.w3.org/ns/activitystreams#Public'],
cc: options.cc || [`${actor}/followers`],
sensitive: options.sensitive || false,
summary: options.contentWarning || null,
inReplyTo: options.inReplyTo || null,
tag: extractTags(content),
attachment: options.attachments || []
};

// Wrap in Create activity
const activity = createActivity('Create', actor, note, {
to: note.to,
cc: note.cc
});

// Store locally
await db.objects.insert(note);
await db.activities.insert(activity);

// Deliver to recipients
await deliverActivity(activity, username);

return { activity, object: note };
}

Addressing​

Public Post​

const publicRecipients = {
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`https://${DOMAIN}/users/${username}/followers`]
};

Followers Only​

const followersRecipients = {
to: [`https://${DOMAIN}/users/${username}/followers`],
cc: []
};

Direct Message​

const directRecipients = {
to: ['https://remote.example/users/bob'],
cc: []
};

With Mentions​

function getRecipientsWithMentions(content, baseRecipients) {
const mentions = extractMentions(content);
const mentionedActors = mentions.map(m => m.href);

return {
to: [...baseRecipients.to, ...mentionedActors],
cc: baseRecipients.cc
};
}

Delivery Pipeline​

async function deliverActivity(activity, username) {
// Get all recipients
const recipients = [...(activity.to || []), ...(activity.cc || [])];

// Resolve to inboxes
const inboxes = await resolveInboxes(recipients, username);

// Deduplicate
const uniqueInboxes = [...new Set(inboxes)];

// Remove private addressing before sending
const deliverableActivity = { ...activity };
delete deliverableActivity.bto;
delete deliverableActivity.bcc;

// Queue for delivery
for (const inbox of uniqueInboxes) {
await queueDelivery(inbox, deliverableActivity, username);
}
}

Resolving Inboxes​

async function resolveInboxes(recipients, username) {
const inboxes = [];

for (const recipient of recipients) {
// Skip public address
if (recipient === 'https://www.w3.org/ns/activitystreams#Public') {
continue;
}

// Check if it's our followers collection
if (recipient === `https://${DOMAIN}/users/${username}/followers`) {
const followers = await db.followers.find({
actor: `https://${DOMAIN}/users/${username}`
});

for (const follow of followers) {
const actor = await fetchActorCached(follow.follower);
if (actor) {
inboxes.push(getInbox(actor));
}
}
continue;
}

// It's an individual actor
const actor = await fetchActorCached(recipient);
if (actor) {
inboxes.push(getInbox(actor));
}
}

return inboxes;
}

function getInbox(actor) {
// Prefer shared inbox for efficiency
return actor.endpoints?.sharedInbox || actor.inbox;
}

Signing and Sending​

async function sendSignedRequest(inbox, activity, username) {
const body = JSON.stringify(activity);
const user = await db.users.findOne({ username });

const url = new URL(inbox);
const date = new Date().toUTCString();

// Create digest
const digest = `SHA-256=${crypto
.createHash('sha256')
.update(body)
.digest('base64')}`;

// Create signing string
const signedHeaders = '(request-target) host date digest';
const signingString = [
`(request-target): post ${url.pathname}`,
`host: ${url.host}`,
`date: ${date}`,
`digest: ${digest}`
].join('\n');

// Sign
const signer = crypto.createSign('RSA-SHA256');
signer.update(signingString);
const signature = signer.sign(user.privateKey, 'base64');

const signatureHeader = [
`keyId="https://${DOMAIN}/users/${username}#main-key"`,
'algorithm="rsa-sha256"',
`headers="${signedHeaders}"`,
`signature="${signature}"`
].join(',');

// Send
const response = await fetch(inbox, {
method: 'POST',
headers: {
'Content-Type': 'application/activity+json',
'Date': date,
'Digest': digest,
'Signature': signatureHeader,
'Host': url.host,
'User-Agent': 'MyActivityPubServer/1.0'
},
body,
timeout: 10000 // 10 second timeout
});

if (!response.ok && response.status !== 202) {
throw new Error(`Delivery failed: ${response.status}`);
}

return response;
}

Reliable Delivery​

Job Queue​

const Queue = require('bull');
const deliveryQueue = new Queue('activity-delivery');

async function queueDelivery(inbox, activity, username) {
await deliveryQueue.add({
inbox,
activity,
username,
attempt: 0
}, {
attempts: 10,
backoff: {
type: 'exponential',
delay: 60000 // Start with 1 minute
}
});
}

deliveryQueue.process(async (job) => {
const { inbox, activity, username } = job.data;

// Check if server is dead
if (await isServerDead(inbox)) {
throw new Error('Server is dead');
}

await sendSignedRequest(inbox, activity, username);
});

deliveryQueue.on('failed', async (job, error) => {
console.error(`Delivery to ${job.data.inbox} failed:`, error.message);

if (job.attemptsMade >= 10) {
await markServerDead(job.data.inbox);
}
});

Dead Server Tracking​

async function isServerDead(inbox) {
const domain = new URL(inbox).hostname;
const dead = await db.deadServers.findOne({ domain });

if (!dead) return false;

// Check if enough time has passed to retry
const hoursSinceDead = (Date.now() - dead.since) / (1000 * 60 * 60);
return hoursSinceDead < 24; // Consider dead for 24 hours
}

async function markServerDead(inbox) {
const domain = new URL(inbox).hostname;
await db.deadServers.upsert({
domain,
since: Date.now()
});
}

Common Activities​

Follow Someone​

async function followActor(username, targetActorUrl) {
const actor = `https://${DOMAIN}/users/${username}`;

const activity = createActivity('Follow', actor, targetActorUrl, {
to: [targetActorUrl],
cc: []
});

// Store pending follow
await db.pendingFollows.insert({
actor,
target: targetActorUrl,
activityId: activity.id,
createdAt: new Date()
});

// Deliver
const targetActor = await fetchActor(targetActorUrl);
await sendSignedRequest(targetActor.inbox, activity, username);
}

Like a Post​

async function likeObject(username, objectUrl) {
const actor = `https://${DOMAIN}/users/${username}`;

const activity = createActivity('Like', actor, objectUrl, {
to: [await getObjectAuthor(objectUrl)],
cc: []
});

await db.likes.insert({
actor,
object: objectUrl,
activityId: activity.id
});

const objectActor = await fetchActorOfObject(objectUrl);
await sendSignedRequest(objectActor.inbox, activity, username);
}

Boost a Post​

async function announceObject(username, objectUrl) {
const actor = `https://${DOMAIN}/users/${username}`;

const activity = createActivity('Announce', actor, objectUrl, {
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [
`${actor}/followers`,
await getObjectAuthor(objectUrl)
]
});

await db.announces.insert({
actor,
object: objectUrl,
activityId: activity.id
});

await deliverActivity(activity, username);
}

Undo an Activity​

async function undoActivity(username, originalActivityId) {
const actor = `https://${DOMAIN}/users/${username}`;
const original = await db.activities.findOne({ id: originalActivityId });

if (!original || original.actor !== actor) {
throw new Error('Cannot undo this activity');
}

const activity = createActivity('Undo', actor, original, {
to: original.to,
cc: original.cc
});

await deliverActivity(activity, username);

// Clean up local state
await db.activities.remove({ id: originalActivityId });
}

Next Steps​