This was my first question when I joined to MoodleNet team. Now, after studying it deeply for a few weeks, I feel able to give a proper answer. So I thought of writing a blog post, with my goal being to offer a nice introduction to ActivityPub for technical people. If you’re interested in my more general impressions on the fediverse and our approach to building a federated app, I recommend you to read my previous blog post about our goals.
ActivityPub is a protocol for decentralized social networks, which can also be extended to create all kinds of federated apps. So if you are thinking of building one it is very interesting to know how this standard works. It is important to notice it has two different parts:
- Client to Server API
- Server to Server API
A service can decide if it wants to implement only one or both of them. This decision just depends on the needs of the project. If you’re a frontend developer, you might use the Client to Server API. You will be focused on the Server to Server API when you only care about the federation part though.
ActivityStreams 2.0
The ActivityPub message format is JSON-LD, which can be used just like regular JSON and we’ll do this for this blog post. The vocabulary used in this protocol is defined by ActivityStreams 2.0 and ActivityPub just adds a couple of requirements more on top of it. In this vocabulary only exists two kind of entities: Link
and Object
. Here we will focus on Object
and its subtypes. An Object
has several fields and only two of them are required by ActivityPub: id
and type
. The most important properties are:
type
: which defines the type of object. It can be a string or an array if the object implements more than a type. This is mandatory in ActivityPub.id
: a unique identifier for the object. For ActivityPub this field should be a URL. The interesting part is that you can retrieve the full object information making aGET
request to this URL with the ActivityStreams accept header. This allows us to send just theid
instead the full object information in many cases.content
: The content or textual representation of the Object, usually an HTML string.name
: A simple, human-readable, plain-text name for the object.summary
: A natural language summary of the object encoded as HTML.image
: A link to an image that represents an object.@context
: This is the only necessary property to accomplish with JSON-LD specification. In our examples we just set to"https://www.w3.org/ns/activitystreams"
.
Natural Language Values
So any object can have those fields. It is interesting that any field likely to be translated, like content
, name
or summary
, can be renamed to a “map variant” to include different languages, ie:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://mycoolserver.com/notes/1",
"content": "This is a translated note"
}
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://mycoolserver.com/notes/1",
"contentMap": {
"es": "Esto es una nota traducida",
"en": "This is a translated note"
}
}
“Data Container” Objects
You may have noticed that the type of the previous object is "Note"
instead of "Object"
. A Note
is a subtype of object defined by ActivityStreams too, and it is used to represent any short written work. A subtype means that share the same fields, but it may define more if needed.
There are some more defined types like Article
, Image
, Page
, Document
, Video
, and more. While it may feel strange to not see something like comment or similar, ActivityStreams is very generic and we have to keep this in mind. The recommendation is to use Note
because it “Represents a short written work”.
Those objects may be considerated “data container” objects. They don’t define more fields and they are used to transmit information.
Activity
In addition to those “data container” objects, ActivityStream defines Activity
objects. The type of those objects may be more intuitive:
- Create
- Delete
- Update
- Add
- Remove
- Join
- Follow
- etc
They are the type of actions that you can expect from a social network. Special attention should be paid to the Announce
activity, as it used to call attention to particular objects or notifications. It can be understood like a “retweet” on Twitter, or a simple way to share an already created object in your social network.
Activities may have 6 additional properties compared to a simple object:
object
actor
target
origin
result
instrument
Of course, not all of them make sense in every type of activities, so they are optional. So imagine you want to represent that Luke created the previous note, you can write:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://mycoolserver.com/activity/1",
"actor": "https://mycoolserver.com/actor/luke",
"object": "https://mycoolserver.com/note/1"
}
Here, you only used actor
and object
. The more common ones. You don’t need to use the rest of them if they do not make sense in your activity.
One important thing to keep in mind is that the ids of the objects will not always appear, and the complete specification of them may appear instead. The previous example could be written in the following way, both being completely valid:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://mycoolserver.com/activity/1",
"actor": {
"type": "Person",
"id": "https://mycoolserver.com/actor/luke",
"name": "Luke"
}
"object": {
"type": "Note",
"id": "https://mycoolserver.com/notes/1",
"contentMap": {
"es": "Esto es una nota traducida",
"en": "This is a translated note"
}
}
Actor
If you take a close look at the previous example you will see that Luke is a Person
, this is a type of Object we haven’t yet mentioned. Person
is one of the 5 types of actors
that ActivityStreams defines:
Person
Group
Organization
Application
Service
Actors are objects that may execute the different activities, this means they can be used as the actor
field on an Activity. An actor has two mandatory properties as well:
inbox
outbox
The previous two properties are mandatory for any actor. The following properties are not but they are recommended to be implemented:
following
follower
liked
Collection
All these five properties are from a core type we haven’t talked about yet: Collections
. As you’d expect, a Collection
is simply a group of objects. They also have additional properties, ie: totalItems
with the number of elements in the Collection
or items
where you’ll find the objects in the collection.
So this means that the inbox
of an actor is just a Collection
containing all the activities received by the actor. The outbox
contains all the activities sent by the actor. Any time an actor likes an object, this is added to their liked
collection.
Obviously, after a while, those collections cannot be managed with just an array. It will be impractical to manage all the followers of a popular person in a simple array. For this reason, we have the last core type of ActivityStreams: the CollectionPage
which should be used to paginate a full Collection
.
Core types
So those are the 5 core object types that ActivityStreams defines:
Object
Actor
Activity
Collection
CollectionPage
All of them are also an Object
, so they share most of the fields. Once you understand this vocabulary it becomes easier to work with the ActivityPub protocol.
Targeting
A big difference from working with other private social networks is the “Audience Targeting”. All objects can use 5 optional fields for this:
to
bto
cc
bcc
audience
Not only that, but it is the client application that fills in these fields. It can be just an actor, but maybe is your followers collection or it is just “public”. This can be better understood with the following examples.
Examples
Sending a private message
First of all, messages are always activities in ActivityPub. So if you want to send a private message to Ben, you have to build a Create
activity with a Note
. It is equally important to set the field to
to only Ben’s id, because is a private message between Alyssa and Ben. You don’t want to make it public or to share with your followers. The JSON could be something like the following:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://social.example/alyssa/posts/a29a6843-9feb-4c74-a7f7-081b9c9201d3",
"to": [
"https://chatty.example/ben/"
],
"actor": "https://social.example/alyssa/",
"object": {
"type": "Note",
"id": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
"attributedTo": "https://social.example/alyssa/",
"to": [
"https://chatty.example/ben/"
],
"content": "Say, did you finish reading that book I lent you?"
}
}
This is a Create
activity because, of course, the message didn’t already exist. As you can see, the actor property is just a Link with Alyssa’s ID. Not need to send the full object. Similarly with the property to
with Ben’s ID, but in this case in an array.
ActivityPub defines that a client should always create new activities talking to the server where the user’s account resides. In this example, Alyssa account resides at “https://social.example/”. Once the message is received by the server it is persisted so it can be requested at any time, but also, it detects that the recipient of the message does not belong to this server. This means that it’s now the server which should communicate this message to Ben’s server, in this case: “https://chatty.example/ben/”. It will send a post request to Ben’s inbox. It should then be Ben’s server that notifies Ben that he has a new message.
Add an image to an album
In this example we are working with images. The user Alyssa just move an image from her “Camera Roll” album to “My Cat Pictures”. The client application generates the following activity to represent the action that just happened:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://social.example/alyssa/activities/10239"
"summary": "Alyssa added a picture of her cat to her cat picture collection",
"type": "Add",
"actor": "https://social.example/alyssa",
"to": "https://social.example/alyssa/followers",
"object": {
"type": "Image",
"name": "A picture of my cat",
"id": "http://example.org/img/cat.png"
},
"origin": {
"type": "Collection",
"name": "Camera Roll"
"id": "https://social.example/alyssa/collection/1"
},
"target": {
"type": "Collection",
"name": "My Cat Pictures",
"id": "https://social.example/alyssa/collection/278"
}
}
This activity is public only for the Alyssa’s followers. The client application send this activity to the Alyssa’s server. Now it is the server which has to locate all her followers and make sure that they receive this activity in their inbox
. Maybe they are all locals, they resides on the same server, or maybe it has to send this activity to many external servers.
Follow request
In this scenario, Sally asks John to be friends in our application:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://example.org/connection-requests/123",
"summary": "Sally requested to be a friend of John",
"type": "Offer",
"actor": "https://sally.example.org",
"object": {
"summary": "Sally and John's friendship",
"id": "http://example.org/connections/123",
"type": "Relationship",
"subject": "https://sally.example.org",
"relationship": "http://purl.org/vocab/relationship/friendOf",
"object": "https://john.example.org"
},
"target": "https://john.example.org",
"to": "https://john.example.org",
"cc": "https://www.w3.org/ns/activitystreams#Public"
}
We make this request using the Offer
activity type with a Relationship
object and with the target John. Now John can accept the offering with:
{
"@context": "https://www.w3.org/ns/activitystreams",
"summary": "John accepted Sally's friend request",
"id": "http://example.org/activities/122",
"type": "Accept",
"actor": "https://john.example.org",
"object": "http://example.org/connection-requests/123",
"inReplyTo": "http://example.org/connection-requests/123",
"context": "http://example.org/connections/123",
"to": "https://sally.example.org/",
"cc": "https://www.w3.org/ns/activitystreams#Public"
}
It is interesting to see that in this application, the creator decided to make public this kind of interactions. To make public an object we have to add to any “targeting property” the value "https://www.w3.org/ns/activitystreams#Public"
, in this case in the cc
property.
Conclusions
It is important to remember that ActivityPub is a protocol. It defines the expected behaviour but never the implementation details behind it. The client sends activities to the server. The server federates the same activities to other servers. The client can fetch for activities and the server should provide them. The way you do it is completely up to you.
There are a lot more things around ActivityPub, here we only scratched the surface. There are more types in the vocabulary, the possibility of extending it with your own types and properties, security recommendations, side effects, client and server responsibilities, etc. The goal of this article was to give a small introduction without getting lost in technical formalities. I hope you enjoyed.
This post was also published in MoodleNet blog.
Photo by Marius Christensen
Leave a Comment