With the release of Threads (though not in Europe) ActivityPub is a hot topic once again while Elon Musk seems to continue on world record pace for the any% speedrun how to kill twitter.
If Threads indeed federates as they said they would this might just be the final nail in the coffin of twitter. Last month alone there was an influx of over 4,5 million users (for a total of just over 10)
(source: fedidb.org)
You can definitely tell the Fediverse is working on it’s second run, so why not join in? Of course I’m already on mastodon (link) but I also own a blog (if you can call this a blog 2 articles in 3 years, so much for that proper try). I’ve build this blog in Umbraco, mostly as a playground for me to at the time learn the ropes of Umbraco, test custom code and plugins etc. so lets test the next new thing, putting my blog on the Fediverse!
Here's how I envisioned this would go down:
- Read up on ActivityPub
- Prepare my umbraco setup (document types and such)
- Implement ActivityPub
- Profit?
What is ActivityPub?
Though I think most of the people reading this are aware of what ActivityPub is, a short rundown is never amiss.
ActivityPub is an open networking protocol (as part of the W3C) that can be used to communicate between social decentralized social networking platforms (like Mastodon). Though I can also imagine ActivityPub (if it is here to stay) might catch on for other uses as communication protocol between different parties. It defines both a client to server API as well as an federated server-to-server API.
In the this case I’ll be implementing the server-to-server part of ActivityPub to put my blog on the Fediverse (the name of the “federated” universe).
Preparing Umbraco
My Umbraco setup is anything but complicated, it’s a simple home page, with 7-page types (if you include the sitemap, error page and random content page type that I’m not using) of which the one that is the most important for this is of course the Article page type you’re looking at right now and even that one is straightforward. The idea I had was that you wanted to “post” blog items on the Fediverse as the user you’re logged into in in Umbraco. This way if you have a blog with more then one person writing on it you could follow that specific person on your Federated social media platform and get an update every time that person posts. (At some point I want to try and make a proper Umbraco package of this so it can be easily implemented by whomever wants to, like someone who actually fills their blog). Though later on I might also add an site white actor that would post all blog items, not just from that user. Anyway, since I’m making the Umbraco user the key for the actor, I need to link the blog posts to the specific user. I had a textbox field on my page that allowed me to write my name down as the “author” of the blog post but in order to link the post to the User I changed this to an “User Picker” field and changed the views accordingly to display the name form it.
- Creating the actor
- Creating the inbox
- Creating the inbox actions
- Mastodon (why checking things is important)
- Weird things about the spec (@context is both and array and a string)
- Next steps
Getting found on the Fediverse
Having prepared Umbraco for the Fediverse the next step is to get found on the Fediverse.
For other servers to find my blog (or more precisely users on other servers) there are two things that need to be done, first I need to create an ActivityPub actor and publish it on an URL. After that I need to implement the WebFinger Protocol so that servers can query my server on where to find said actor (and if it even exists).
The WebFinger protocol
The WebFinger protocol is used to query servers on a known location about information on that server, in this case about an actor. The protocol is not only used for Fediverse platforms but for example also for the OpenID Connect standard. The query that a Fediverse platform like Mastodon (I’ll use that as the example, but it should work for the majority of ActivityPub platforms) use the protocol to request the information about an ActivityPub actor. In my case that actor would be [email protected] like with a user on a different Mastodon server then I’m on it contains both the username (of the local server) as well as the server information, separated by the @.
The request path that Mastodon does (in the case of my blog) is then:
https://eduwardpost.nl/.well-known/webfinger?resource=acct:[email protected]
Where the first part (the path) is a fixed path on every server implementing it (like with the other .well-known paths) and then in the query string information about what you want to know, in this case a resource, specifically the account (acct) [email protected]
My server then responds with the JSON to the right.
The document is pretty simple, it describes the subject of the document (the account) and links to where to find more information, in this case the “self” of type “application/activity+json” and the actual link. The content type was new to me though as it is part of the ActivityPub specifications.
The technical implementation of this in Umbraco is about as simple as the document, it’s a POCO object that is being return by an custom controller that extends the UmbracoApiController. Then in the Action I look at the query string, check if it contains resource and if its value starts with acct: if so, I string split it twice, first on : then on the @ which gives me the username the requester is interested in which I then use to find the IUser object with the IUserService from Umbraco.
I’ve used the IUser object at all the parts where I need to know something about the actor as this would make it possible to automatically create all the documents for the different users in the Umbraco implementation.
{
"subject": "acct:[email protected]",
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost"
}
]
}
The ActivityPub Actor
So now that Mastodon is able to check if the user exists on my server, and knows where to find it, I need to prepare the next part; the Actor.
This one not as simple as the WebFinger file, and is the first ActivityPub part of the implementation. In short it holds a list of locations (url’s) of specific ActivityPub parts (more on those later). Some basic information like username, preferred name, icon and some default fields that any ActivityPub object has like an id, object type and a context property as defined in the “JSON-LD” specification. All straight forward information.
Except for one, the public key field, though this field is not specified in the ActivityPub protocol it is part of an other W3C protocol that is used for security. In this case it allows severs to check if a message received on their inbox (more on that later to) is indeed send by the actor that it says it is. And it uses normal RSA encryption to do so. Therefor we need a way for servers to get our public key, the security protocol specifies it to be in the publicKey property and in PEM format.
In the end it looks like the JSON on the right here, note that the public key has it’s new lines replaced by \n characters.
Though most information is about as straightforward as the webfinger document and thus easy to create a POCO for with a custom controller, which is how I implemented this in Umbraco. The key part was a bit more involved, to prevent any security issues I didn’t want to use any of the SSL keys that where already in use (like for serving HTTPS traffic). And since I wanted it to be possible for all Umbraco users to be on the Fediverse I needed a way to generate the keys in the application (since you can add users later to).
The followers, inbox and outbox links I’ll come back to later
So on first request if there’s no key pair I generate a new pair using the .net RSA implementation (though this can also be changed to openssl later on, again if it becomes a package). This I then store on the server (this to should / could be changed later to something like say… an Azure key vault, but since I don’t use the key elsewhere I’m not that concerned right now).
From the controller perspective, like with the WebFinger controller it’s a custom Api controller that uses the path part to get the IUser and creates the Actor document with that.
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"preferredUsername": "eduwardpost",
"name": "eduwardpost",
"inbox": "https://www.eduwardpost.nl/activitypub/inbox/eduwardpost",
"outbox": "https://www.eduwardpost.nl/activitypub/outbox/eduwardpost",
"followers": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost/followers",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://www.gravatar.com/avatar/9c65ad8480dc81d41caecdfd742119c7"
},
"published": "2023-07-17T12:00:00",
"manuallyApprovesFollowers": false,
"publicKey": {
"id": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost#main-key",
"owner": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost",
"publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCg[...]5wIDAQAB\n-----END RSA PUBLIC KEY-----"
},
"id": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost",
"type": "Person"
}
Am I federated?
From the controller perspective, like with the WebFinger controller it’s a custom Api controller that uses the path part to get the IUser and creates the Actor document with that.
So far so good, at this point I deployed it to production and went to Mastodon, entered [email protected] in the search field, waited for the results to load and… there it was! Mastodon found a user known as eduwardpost on the server www.eduwardpost.nl, it works my server is now on the Fediverse!
Of course the immediate next step is to press that follow button, with high (or vain) hopes I press the button and to my shock it says “following” seemed easy, to easy… after a quick reload of the page that then changed to “cancel follow request” turns out, you need to accept a follow from someone in the Fediverse. And thus it’s on to the next part, the inbox.
You’ve got mail! In your inbox…
So, Mastodon can find me, I press follow and that tells the mastodon server of the instance I’m on to send a message to my Umbraco server and deliver it to my actor’s inbox. So yes, I am now federated! Mastodon looks at the actor file for an location of the inbox (as defined in the ActivityPub protocol) to deliver said message.
The ActivityPub inbox is “just” and HTTP endpoint that listens to a POST request with in it’s body an ActivityPub Activity. In this activity there’s a bit of information mostly an id and type (like any ActivityPub object) the actor sending (publishing) said activity and an object that describes the activity, in this case the actor that is going to be followed.
In JSON that looks like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://mastodon.social/00000000-0000-0000-0000-000000000000",
"type": "Follow",
"actor": "https://mastodon.nl/users/eduwardpost",
"object": "https://www.eduwardpost.nl/activitypub/actor/eduwardpost"
}
As with the actor this to is "just" and Simple HTTP Post action on the custom controller I made, in which I store the activity in a database (for later things like placing reactions beneath the blog post that have been made on Mastodon for example).
So, I process the request return a nice 200 OK result back to mastodon, repress the follow button… same thing happens, it changes to cancel request, even though I stored it in the database, turns out you need to create an accept message and hand that back to the requestor.
This is where things get complicated…
(request-target): {REQUET VERB} {REQUEST PATH}
host: {TARGET HOST}
date: {CURRENT DATE in RFC1123}
digest: SHA-256={HASH OF BODY}
(request-target): {request.Method.Method.ToLowerInvariant()} {requestUri.AbsolutePath.ToLowerInvariant()}
host: {requestUri.Host.ToLowerInvariant()}
date: {DateTime.UtcNow:R}
digest: {contentHash}"
Accepting the follow
At first I thought this was pretty simple, I checked the documentation did some quick googling, browsed some Github pages on people’s ActivityPub implementations and quickly found how the accept message should be modeled, it’s an Activity message just like the Follow request which I already have a POCO for. Only differences are that the type now is Accept instead of Follow, the actor is now my instead of the Mastodon actor and the object contains the entire Activity as an JSON object.
This didn’t take to long to code, reference the request object in the response object, set the type and actor, put it in the response body and go!
Trying to follow again (though by now I also figured out there’s an Undo action that is being send when you cancel the follow, so after implementing that which doesn’t do much but “undo’s” the database record that the follow created). Same thing still.
This took me longer then I like to admit, turns out I didn’t read well enough (theme for the rest of this part) instead of returning it in the body of the request, you need to send the accept activity message to the inbox of the actor that send you the activity with the follow request…
I can see know how the first rush of the Fediverse caught some Mastodon admins off guard with all that additional network traffic.
Sending messages to Mastodon
Ok so, we need to send a message to the requester’s inbox. Most of this is pretty straight forward, get an HttpClient, put in the address serialize the body and send, except remember that publicKey on the actor? This message needs to be signed, this to is not rocket science and with some quick googling, githubing, stackoverflowing, etc. I figured out Mastodon (and many others) uses part of an non finalized specification for HTTP header signatures.
On top of that it requires a specific message to be singed specifically (to the left here)
Including the newline (\n) characters, all not hard information to gather but sure made the idea of “lets quickly build this in one evening” go out of the window.
So… that should be it right?! Wrong, sending this nicely gave a 202 accepted response back, but didn’t change the button on Mastodon, so what’s up with that. First it turns out Mastodon practices in silent fails (or at least the instance I’m on) so it said it accepted it, but it didn’t. So much for evening two, which mostly was spend in frustration and google actions. Trying and erroring many different things (there’s quite some old documentation about ActivityPub/Mastodon on the web to that no longer works which I stumbled on and tried)
For day three I went through it all with a fine comb again and I found it! Turns out I again was not paying attention to a specific detail, remember that context field that is not part of the specification of ActivityPub? If you look at the JSON’s on the page you’ll see that it’s property key is “@context”. Which I forgot to nicely defined in my POCO with a JsonPropertyNameAttribute. So it did not get serialized in the message I send with that key but with “context” instead, Mastodon didn’t like that and silently fails my request.
Having found that and fixed it, I ran into the next weird thing. All of the sudden I was no longer able to parse the message on my Http Action controller now that it is nicely picked up but the JSON converter.
All the documentation I’d seen so far that had this property defined it as an array of strings in the JSON, and for the actor it even had two values in it. Though most ActivityPub messages only have one. Turns out if it only has one Mastodon returns it with a string instead of an array with one string…
Turns out that’s allowed in the spec, the @context field can be about anything, string, array object it’s all fair game for this field.
One quick JsonConverter further the messages where parsing again, and on top of that my Accept ActivityPub Activity message finally got accepted and I’m not following myself! (From Mastodon to my Umbraco blog that is)
So now what?
As the title already indicated this is Part 1 of a multi part (at least two) series. Because the whole idea of following someone is that you get to see their posts on your timeline, which in this case would be the blog posts of course! Which is where the outbox part of the actor comes into play.
Also one of the other next steps is make a proper code view on this blog (and update this page with them) because this looks appalling.
edit: this has since been done.
If you do read this before I post part 2 you can follow my blog on your Fediverse instance (hopefully, as I so far only tested with Mastodon) by following [email protected] and hopefully you get a post on your timeline when it shows up. And if you’re scared I’d spam your timeline, there’s no need with 2 articles in 3 years I wouldn’t expect much