You can find the index and other parts of this series here.
We are almost done! Thank you for coming all the way into this journey. In this part we will learn how to broadcast (aka federate) your site posts to your folowers.
Overview
The federation of your posts (aka. sending your posts to live infitely in the fediverse) is pretty straighforward:
sequenceDiagram BroadcastTool->>Storage: Retrieve followers (actor uris) BroadcastTool->>Follower-instance: Get actor info (including inbox uri) BroadcastTool->>Filesystem: Get note json (post) BroadcastTool->>Follower-inbox: Send a create action (wrapper of note)
In a few words, you will iterate your list of followers, get their inbox url, and send a request to such url to create an fediverse object (note, article, etc.).
Details
On part 4, we already generated the notes. These are static files living in our site. However, we need to send such notes to the fediverse again, so they are aware that these exists.
Once a note hits one fediverse server, it is very likely that if this object gets shared and visible to other instances, these instances will already be aware of your post. Your notes are being federated. So how a fediverse instance know if a note is already in their database? With the object id. This is what a create note looks like:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://hachyderm.io/users/mapache/statuses/111876223052500122/activity",
"type": "Create",
"actor": "https://hachyderm.io/users/mapache",
"published": "2024-02-05T01:14:48Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://hachyderm.io/users/mapache/followers",
"https://maho.dev/@blog"
],
"object": {
<original note>
},
"signature": {
"type": "RsaSignature2017",
"creator": "https://hachyderm.io/users/mapache#main-key",
"created": "2024-02-05T01:14:48Z",
"signatureValue": "dYIs1ZFExg5LeqHrZ5o905/fDv7WGyxC71kl1Jjlq9kqSDjMlhXP3/159fgwF+ouJRjBWEOIUUAwHhHFmbcuMbqFNxxPZ1nQ6A10HSL8osb4CycUsTOtu0XoOp2x5eufB8dgsVqb8LfCOlpGyuNLewbAheqUN3mjO+QyaKQJ0PTRqxKt5gUcAlpkGYTGLx8f+b9dIwHUM9F4VzueWJ7UiVqOMxS1Lb7mXn2qmEyj3hN81W5jttHyUmQ+qB5nNjKc/u8eOGcV+p5yy/auCSq4im2JsvmwK4fCpsfC2gqSl4gRknTng8T/DAoVUbW1bQC5YJg14uqeEKN5QGkqZxWikA=="
}
}
This id is unique (“https://hachyderm.io/users/mapache/statuses/111876223052500122/activity") and in the example above is a mastodon generated one. At difference that the original note or object, these does not need to be reachable, in my case I get away with just adding “/create” to the original id. But it is important this id is consistent, to avoid generating multiple notes.
Another important part to mention is the cc, which I have not figure it out yet, honestly. Mastodon seems to either ignore this, probably because it already has a database of who follows who, and decides to show the timeline of posts not based in the create requests received, but based in what should they see. In any case, is important to at least cc your followers url.
Finally the object, really important that the signature matches the object.
One important aspect that I am figure it out is the “published” field. This can be different from the date the note is mention to publish. But I am pretty sure, mastodon uses this to decide if a post will hit the “#hashtag” list or timeline. Mastodon seems to determine if the post is “recent” so it appears in the hashtags. I have not mastered this yet, so I may have an update weeks later.
Another important consideration is taht, if you have more than one follower with the same inbox, you don’t need to send the same note again.
Putting everything together
I have implemented this in my favorite language (C#), but should be straighforward to implement in other places. You can find the code (and contribute) here.
Remember, as long as the id is the same, there is no harm in sending the same create note again. It won’t get duplicated, but to avoid wasting bandwith, processing power and bytes, you don’t want to do this. This is what my bash script to broadcast looks like:
# hugo deploy code ...
# azure deploy code ...
# at this point the generated notes are in public/socialweb/notes of the local runner (local dev or cicd worker)
for file in public/socialweb/notes/*; do
# getting the base filename
fileName=$(basename "$file")
# echo for logging purposes
echo "socialweb/notes/$fileName"
# Use Azure CLI to set content type of the note
az storage blob update -c "\$web" -n "socialweb/notes/$fileName" --content-type "application/activity+json;" --account-key "$AZURE_STORAGE_KEY" --account-name "$AZURE_STORAGE_ACCOUNT"
# if the note is in the file "broadcasted.txt" then we don't need to broadcast it again
grep -q "$fileName" broadcasted.txt && continue
# Broadcast post
BroadcastPost --notePath $file
# Add the note to the list of broadcasted notes
echo "$fileName" >> broadcasted.txt
done
As you can see the BroadcastPost command does all the magic. The broadcast post needs these 3 environment variables:
export ACTIVITYPUB_DOTNET_PRIVATEKEY=`cat privkey.pem`
export ACTIVITYPUB_DOTNET_KEYID="https://maho.dev/blog#main-key"
export ACTIVITYPUB_DOTNET_STORAGE_CONNECTIONSTRING="DefaultEndpointsProtocol=https;...secrets...;EndpointSuffix=core.windows.net"
Next steps
- If you want to integrate this in a CI/CD check my example of github actions and some utility scripts here.
- Another alternative instead of using a broadcasted.txt local file, would be to create a table in a database (e.g. local sqlite) or in the same Azure Storage.
- What happens if you update a post? There is a
update
action in activitypub, I have not implemented that yet, but the more complex part would be to “detect” changes on posts. Maybe with a hash? or datetime? Or maybe manually, idk. - I have some crazy ideas of how we should send different objects for the same origin. Keep posted, because that is some crazy shit.
So, as you can see this is not super complicated, and I will in this moment broadcast this post.