May 9, 2023

Understanding ActivityPub Part 1: Protocol Fundamentals

This article is part of the series Understanding ActivityPub, which takes a look at the ActivityPub protocol through the lens of real-world examples. The protocol exchanges are taken from ActivityPub.Academy, a modified Mastodon instance that shows ActivityPub messages in real time (see the announcement post).

The following articles are available.

Part 1: Protocol Fundamentals (this article)

Part 2: Lemmy

Part 3: The State of Mastodon

Part 4: Threads

In this blog post, I’m using ActivityPub.Academy (see the announcement post) to explore the ActivityPub protocol. We’ll see how different instances communicate that one user wants to follow another and how a social graph is built; how messages are distributed to followers on different instances; and lastly, what happens on the protocol level when an account is moved to a different one.

The blog post has two purposes. First, it can hopefully be read as a stand-alone post if you are interested in ActivityPub, but you don’t want to read the entire ActivityPub Spec. And second, it serves as an introduction on how to use ActivityPub.Academy to explore the protocol. It is an invitation to do your own experiments and explorations and learn about the protocol in this way. And once you have some practical experience with the protocol, trying to read the Spec is probably not as daunting anymore.

Actors and Inboxes

Before looking at our first activities, we need to have a quick look at actors and inboxes.

Full mentions and actor IDs

If you have used Mastodon, you are probably familiar with full mentions. For my main account, the full mention is @crepels@mastodon.social, where mastodon.social is the instance, and crepels is the username on that instance. In the UI, that’s usually all you’ll ever need to deal with. But under the hood, the ActivityPub protocol doesn’t use full mentions at all. Instead, it uses actor IDs. (I think of actors as user accounts. The Spec has a different definition, but it is so general that it is almost useless.)

The actor ID for my main account is https://mastodon.social/users/crepels. In my case, there is a direct translation between the full mention and the actor ID, but that doesn’t have to be the case. They can look completely different. Since the UI uses full mentions but the protocol uses actor IDs, Mastodon needs a way to translate from one to the other. This is done using WebFinger, a standalone protocol independent of ActivityPub, defined in RFC 7033. I won’t go into the details of the protocol, but the usage in our case is simple. If we have a full mention and want to find out the corresponding actor ID, we execute a GET request against the WebFinger endpoint of the instance, passing the full mention as a query parameter. For example, to get the actor ID of my account, we can query the URL https://mastodon.social/.well-known/webfinger?resource=acct:crepels@mastodon.social, which returns

{
  "subject":"acct:crepels@mastodon.social",
  "aliases":[
    "https://mastodon.social/@crepels",
    "https://mastodon.social/users/crepels"
  ],
  "links":[
    {
      "rel":"http://webfinger.net/rel/profile-page",
      "type":"text/html",
      "href":"https://mastodon.social/@crepels"
    },
    {
      "rel":"self",
      "type":"application/activity+json",
      "href":"https://mastodon.social/users/crepels"
    },
    {
      "rel":"http://ostatus.org/schema/1.0/subscribe",
      "template":"https://mastodon.social/authorize_interaction?uri={uri}"
    }
  ]
}

Under the type application/activity+json we see the actor ID.

To see a case where the full mention and the actor ID look completely unrelated, query the WebFinger protocol for @seb@jambor.dev (using the URL https://jambor.dev/.well-known/webfinger?resource=acct:seb@jambor.dev). It returns

{
  "subject":"acct:seb@jambor.dev",
  "aliases":[
    "https://mastodon.social/@crepels",
    "https://mastodon.social/users/crepels"
  ],
  "links":[
    {
      "rel":"http://webfinger.net/rel/profile-page",
      "type":"text/html",
      "href":"https://mastodon.social/@crepels"
    },
    {
      "rel":"self",
      "type":"application/activity+json",
      "href":"https://mastodon.social/users/crepels"
    },
    {
      "rel":"http://ostatus.org/schema/1.0/subscribe",
      "template":"https://mastodon.social/authorize_interaction?uri={uri}"
    }
  ]
}

So @seb@jambor.dev also maps to the actor ID https://mastodon.social/users/crepels. (Some people set up aliases for their Mastodon accounts this way.)

Hint: On ActivityPub.Academy, you can type the full mention into the ActivityPub Explorer and it will be automatically translated into the corresponding WebFinger URL.

The inbox of an actor

The actor ID is a valid URI, and we can query it with a GET request. The response will differ depending on the Accept header that we specify. If we set it to text/html (or don’t specify any header at all), it will show my profile page. You can test it out by opening https://mastodon.social/users/crepels in your browser. But if we specify the header value application/ld+json; profile="https://www.w3.org/ns/activitystreams" or application/activity+json, it will return the ActivityStreams representation. (The first header value is the one that the spec says that all clients must use. Mastodon breaks with this rule and uses the second value instead. I guess for this reason, the spec says that every ActivityPub server should also support this header). For my actor ID it returns

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
      "toot": "http://joinmastodon.org/ns#",
      "featured": {
        "@id": "toot:featured",
        "@type": "@id"
      },
      "featuredTags": {
        "@id": "toot:featuredTags",
        "@type": "@id"
      },
      "alsoKnownAs": {
        "@id": "as:alsoKnownAs",
        "@type": "@id"
      },
      "movedTo": {
        "@id": "as:movedTo",
        "@type": "@id"
      },
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "discoverable": "toot:discoverable",
      "Device": "toot:Device",
      "Ed25519Signature": "toot:Ed25519Signature",
      "Ed25519Key": "toot:Ed25519Key",
      "Curve25519Key": "toot:Curve25519Key",
      "EncryptedMessage": "toot:EncryptedMessage",
      "publicKeyBase64": "toot:publicKeyBase64",
      "deviceId": "toot:deviceId",
      "claim": {
        "@type": "@id",
        "@id": "toot:claim"
      },
      "fingerprintKey": {
        "@type": "@id",
        "@id": "toot:fingerprintKey"
      },
      "identityKey": {
        "@type": "@id",
        "@id": "toot:identityKey"
      },
      "devices": {
        "@type": "@id",
        "@id": "toot:devices"
      },
      "messageFranking": "toot:messageFranking",
      "messageType": "toot:messageType",
      "cipherText": "toot:cipherText",
      "suspended": "toot:suspended",
      "focalPoint": {
        "@container": "@list",
        "@id": "toot:focalPoint"
      }
    }
  ],
  "id": "https://mastodon.social/users/crepels",
  "type": "Person",
  "following": "https://mastodon.social/users/crepels/following",
  "followers": "https://mastodon.social/users/crepels/followers",
  "inbox": "https://mastodon.social/users/crepels/inbox",
  "outbox": "https://mastodon.social/users/crepels/outbox",
  "featured": "https://mastodon.social/users/crepels/collections/featured",
  "featuredTags": "https://mastodon.social/users/crepels/collections/tags",
  "preferredUsername": "crepels",
  "name": "Sebastian Jambor",
  "summary": "<p>I created a systemd playground to help people learn systemd.</p>",
  "url": "https://mastodon.social/@crepels",
  "manuallyApprovesFollowers": false,
  "discoverable": true,
  "published": "2022-05-01T00:00:00Z",
  "devices": "https://mastodon.social/users/crepels/collections/devices",
  "publicKey": {
    "id": "https://mastodon.social/users/crepels#main-key",
    "owner": "https://mastodon.social/users/crepels",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzCg7ogX8utFGjeOs7EeX\nKLUixqzlyrCFobv+Pcqhmw8WrMYnm3jrbUWkjcOUHaepAFAgauns+Tr5XwmAsNkx\nYLqyVipartwf8/tdiHZAhHgOdvtfd3dXHDpZKyeQfOtdt2AxftMihg9wmCyupAJC\naLNbyx4S9Q+aDVkaaHiFvhTNWHNR4rf3OKvQixiopry0G9oCCkUxWs+z4RT8Rx+R\nOLteufRh6WOn7YgqeMcJ4hZ/DcQZD3JtdJfEwUDftEW3xkoI/T1IA2QYAr3m4xHJ\nhv3TONr6VD/Bx5RPUsBDvXOlN5hbadClOQURybuyUBe1D+Md6SYHSQ4Q/x0y6L9+\nhQIDAQAB\n-----END PUBLIC KEY-----\n"
  },
  "tag": [],
  "attachment": [
    {
      "type": "PropertyValue",
      "name": "Blog",
      "value": "<a href=\"https://seb.jambor.dev\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">seb.jambor.dev</span><span class=\"invisible\"></span></a>"
    },
    {
      "type": "PropertyValue",
      "name": "systemd by example",
      "value": "<a href=\"https://systemd-by-example.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">systemd-by-example.com/</span><span class=\"invisible\"></span></a>"
    }
  ],
  "endpoints": {
    "sharedInbox": "https://mastodon.social/inbox"
  },
  "icon": {
    "type": "Image",
    "mediaType": "image/png",
    "url": "https://files.mastodon.social/accounts/avatars/108/227/485/389/961/502/original/1213d525278ae01d.png"
  }
}

Hint: If you search for the actor ID (or any other ActivityPub URI) on the ActivityPub Explorer, the Accept header will be set automatically to application/ld+json; profile="https://www.w3.org/ns/activitystreams".

If you scroll past the @context field you’ll see useful information about my account. (The @context field turns the JSON response into JSON-LD. I won’t go into JSON-LD in this blog post and simply treat the responses as ordinary JSON.) The most important information for us are the inboxes. There are two of them. First there is the actors personal inbox (the inbox field, which is https://mastodon.social/users/crepels/inbox in my case), and second there is the shared inbox (the endpoints.sharedInbox field, which is https://mastodon.social/inbox for all actors on mastodon.social).

Inboxes are central to ActivityPub. When different ActivityPub servers talk to each other, they send Activities to inboxes, as we will see in a bit.

Another thing that we will come back to later is the publicKey field, which contains the public key for my actor. The private counterpart is stored on mastodon.social.

This is all information we need for now, and equipped with this we can look at our first activities.

Building a social graph

The ActivityPub protocol comes into play whenever you use your Mastodon account to interact with accounts on a different instance, for example, if you follow another account, post a direct message to someone, or send a post to all your followers. The instances communicate those actions by sending Activities to one another. On ActivityPub.Academy we can see those activities in real time in the Activity Log. We will create an account @alice@activitypub.academy and use it throughout this blog post. We will see all activities that are sent by @alice to actors on other instances, and all activities that @alice receives from actors on other instances.

The first activities we are taking a look at are generated when we follow other people. We will build a small social graph of four actors spread across three different instances. This will give us a lot of activities to investigate, and it will also be the basis for later examples.

The graph we want to create looks like this.

A diagram showing the social graph of four actors

This means that @alice@activitypub.academy follows @berta@techhub.social and vice versa. @blaine@techhub.social follows @alice, but @alice doesn’t follow @blaine. Lastly, @alice and @crepels@mastodon.social follow each other.

I encourage you to follow along on ActivityPub.Academy. You don’t have to create a social graph with multiple people, but at least follow for example your main Mastodon account.

Let’s start from @alice’s account and follow @berta. Clicking on the “Follow” button will trigger an exchange of Activities between activitypub.academy and techhub.social. On ActivityPub.Academy, we can see this exchange in the Activity Log, which looks like

What this means is that @alice sent an activity of type Follow to @berta’s inbox on techhub.social, and @berta immediately answers with an Accept activity, sent to @alice’s inbox. The actual activities are JSON objects; you can see them by clicking on show source.

I think this level of abstraction is most useful when trying to understand ActivityPub. We see the activities at a glance, and we see that it is a conversation between different Mastodon instances. We will use this level of abstraction throughout this blog post, and it’s exactly what you will see on ActivityPub.Academy. But I think it’s also important to understand what’s behind this abstraction, so let’s have a quick look at the technical details (a more complete look can be found in Eugen Rochko’s blog).

When @alice clicks the “Follow” button to follow @berta, activitypub.academy sends a POST request to @berta’s inbox endpoint https://techhub.social/users/berta/inbox with body

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://activitypub.academy/16606771-befe-483b-9e2c-0b8b85062373",
  "type": "Follow",
  "actor": "https://activitypub.academy/users/alice",
  "object": "https://techhub.social/users/berta"
}

This JSON object represents a Follow activity, and signifies that @alice (specified in the actor field) wants to follow @berta (specified in the object field). Since @berta’s account is configured to accept all follow requests, techhub.social immediately answers with a POST request to @alice’s inbox endpoint https://activitypub.academy/users/alice/inbox with body

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://techhub.social/users/berta#accepts/follows/946055",
  "type": "Accept",
  "actor": "https://techhub.social/users/berta",
  "object": {
    "id": "https://activitypub.academy/16606771-befe-483b-9e2c-0b8b85062373",
    "type": "Follow",
    "actor": "https://activitypub.academy/users/alice",
    "object": "https://techhub.social/users/berta"
  }
}

This is again an activity, this time of type Accept, signifying that @berta (the actor field) accepts the Follow activity (specified in the object) field.

The visualization above contains this information, but in a condensed form. The endpoint that the activity was sent to is shown in the Sent to field. The activity itself is reduced to its type and the object (since those are usually the most interesting properties). The object can be itself an activity (like in the Accept case above), resulting in a kind of recursive representation.

The last part from the visualization that we haven’t explained yet is the From field. You might guess that it comes from the actor field of the activity, but that’s not entirely correct. To explain where it actually comes from, we have to dig a little deeper.

Different Mastodon instances communicate with each other exclusively via ActivityPub. With Activities, an instance indicates that a user wants to follow another user (as we just saw), instances send messages from one user to users on other instances, and much more. Essentially, through Activities one instance tells another instance what a certain user did or intends to do. We can see above that the Activity necessary to follow another user is a simple JSON object. So what prevents me from POSTing a JSON object to mastodon.social, pretending that some random user wants to follow Eugen Rochko? Or worse, pretending to be Eugen and blocking all of his followers? The answer to this is that Mastodon requires all requests to be signed to verify the author of an activity. In order to do this, every actor has a public/private key pair that we saw above. When activitypub.academy sends the Follow activity to @berta’s inbox indicating that @alice wants to follow her, it signs the request using @alice’s private key and attaches a Signature header to the request that looks something like

Signature: keyId="https://activitypub.academy/users/alice#main-key",signature="...",...

The techhub.social instance will first check if the signature is correct, using @alice’s public key. (Notice that the keyId in the Signature header contains the actor ID, so if the instance has never seen @alice before, it can use the actor ID to fetch the public key.) If the signature is correct, the instance can be sure that the activity was created by @alice and processes it further; otherwise it throws an error.

So now we have the final part of the visualization. The From field in the visualization contains the actor from this Signature header. It is usually the same as the actor field of the activity, but there are some cases where they differ. (I’m planning to go into this more in a future blog post.)

Let’s finish this section by adding the rest of the links from the graph above, resulting in the following Activity Log.

We now have the social graph that we wanted to build and we will continue with some other ActivityPub features. But there’s a lot more to explore when it comes to the social graph. If you are following along on ActivityPub.Academy, there are a few things you could try out, like, what happens when you unfollow another actor? What happens when your account is configured to manually approve followers and you reject a follow request? What happens when you block a follower?

ActivityPub data

So far we have looked at the ActivityPub protocol – the exchange of activities. There is another important component to ActivityPub, which I like to call ActivityPub data. We have already seen one example of ActivityPub data: the actor. The actor ID is a well-defined URL, and if we query it with a GET request and the Accept header set to application/ld+json; profile="https://www.w3.org/ns/activitystreams", we get back the JSON object representing the actor that we saw above. But there is other data aside from actors. The actor object contains links to some examples, like my followers collection, available under https://mastodon.social/users/crepels/followers. If we GET this, again with the special Accept header, it returns

{
  "@context":"https://www.w3.org/ns/activitystreams",
  "id":"https://mastodon.social/users/crepels/followers",
  "type":"OrderedCollection",
  "totalItems":24,
  "first":"https://mastodon.social/users/crepels/followers?page=1"
}

The first entry points to the first page of the collection, which is again ActivityPub data. GETting this one shows

{
  "@context":"https://www.w3.org/ns/activitystreams",
  "id":"https://mastodon.social/users/crepels/followers?page=1",
  "type":"OrderedCollectionPage",
  "totalItems":24,
  "next":"https://mastodon.social/users/crepels/followers?page=2",
  "partOf":"https://mastodon.social/users/crepels/followers",
  "orderedItems":[
    "https://hachyderm.io/users/deomorxsy",
    "https://hachyderm.io/users/olepbr",
    "https://hachyderm.io/users/giulianopz",
    "https://mastodon.social/users/JamborJan",
    "https://mstdn.plus/users/jmuranga",
    "https://sfba.social/users/sesh",
    "https://fosstodon.org/users/Skel_0",
    "https://social.yakshed.org/users/bascht",
    "https://mastodon.social/users/tfe",
    "https://mastodon.social/users/mareyher",
    "https://bewegung.social/users/a_watch",
    "https://fosstodon.org/users/gait"
  ]
}

All of the URLs again point to ActivityPub data that can be explored further.

On ActivityPub.Academy, you can open the URLs in the ActivityPub Explorer. The shown JSON object is interactive, all URLs are clickable and will fetch the corresponding ActivityPub data. This allows for convenient navigation of the ActivityPub data.

Often, the exchange of Activities triggers changes in the ActivityPub data. For example, after @alice follows @berta, opening https://techhub.social/users/berta/followers?page=1 in the ActivityPub Explorer shows

{
  "@context":"https://www.w3.org/ns/activitystreams",
  "id":"https://techhub.social/users/berta/followers?page=1",
  "type":"OrderedCollectionPage",
  "totalItems":1,
  "partOf":"https://techhub.social/users/berta/followers",
  "orderedItems":[
    "https://activitypub.academy/users/alice"
  ]
}

So @alice was added to the list of @berta’s followers. Similarly, we would see @berta in @alice’s following collection.

Sending messages

The main function of Mastodon is to send messages. We’ll explore how this works on the protocol level in this section, looking at different types of messages.

There are different visibility options for messages. In the broadest visibility, your messages appear in all of your follower’s feeds. Furthermore, the message is publicly available: if you send a link of the message to someone, they should be able to open the link and see the message. On the other end of the spectrum are direct messages, which should only be visible by a single recipient. In ActivityPub, all of those messages are created using the same type of Activities. The only difference is that the Activities have to and cc fields (similar to email) which specify who should see those messages.

Public messages

In Mastodon, you can specify who should be allowed to see your messages by setting the privacy level of a post. By default, a post is visible for everyone, and it is delivered to all of your followers. So let’s start by creating a public “Hello World” post with @alice’s account. In the Activity Log, we will see the following.

Several interesting things are happening. First, we see that messages are created using the Create activity. Like the activities that we saw above, the Create activity has an object field, which in our case is an object of type Note, containing the message in HTML format.

There are two Create activities that are sent out by @alice, one going to the techhub.social instance, and the other to the mastodon.social instance. This makes sense, since @alice has followers on those two instances, so they must be notified of the new post. But the post is not sent to the followers’ inboxes directly. Instead, it is sent to the shared inboxes of the instances.

Remember that @alice has three followers: one on mastodon.social and two on techhub.social. Since @alice wants her post to be delivered to all of her followers, we could assume that this results in three Create activities being sent, one to each inbox of all followers. But this could become very inefficient, especially when @alice has a lot of followers. To combat this, the ActivityPub Spec allows the definition of a shared inbox (providing one is optional, but Mastodon always does it). Instead of sending the Create activity to all follower’s inboxes on an instance, we can simply send it to the shared inbox and rely on the instance to deliver it to our followers internally.

That’s what’s happening above. @alice has two followers on techhub.social, @berta and @blaine, but only one activity is sent to this instance. The question is now, how does techhub.social know which actors to show this message to? That’s where the to and cc fields of the activity come into play. If we take a look at the source of the activity, we’ll see the five fields that we saw for the activities above (@context, id, type, actor, and object), but also some additional fields, for instance to and cc. The to field contains the special collection https://www.w3.org/ns/activitystreams#Public. This means that the message is public and should be visible by everybody. (These posts are what shows up in your Federated timeline: it shows all messages that are sent to your instance and which are addressed to the public collection.) The cc field contains https://activitypub.academy/users/alice/followers, which means that the message should be shown in the Home timeline of all of @alice’s followers. Note that the techhub.social instance knows which of its users follow @alice, so it can deliver the message accordingly.

Unlisted messages

So far, we looked at public posts. What happens with the other privacy options?

Here is the result of an unlisted post.

If you look at the source, you’ll see that this time, the roles of the two address fields are reversed. The to field contains the followers collection, and the cc field contains the public collection. This means that the message is still sent to the Home timeline of all of @alice’s followers, and the message is still public, so everyone can see it (for example, by opening the ID of the note in a browser), but it no longer shows up in the Federated timeline.

The third privacy option is Followers only, where the to field still contains the followers collection, but the cc field is empty. As the name of the option suggests, the post is visible to followers only, it’s not publicly accessible. (I’ll omit the log here).

Direct messages

The strictest privacy level is the direct message, which is sent to mentioned people only. Here is an example, where we sent a direct message to @crepels@mastodon.social.

We see that it is only sent to mastodon.social. But interestingly, it is still sent to the shared inbox, even though the only recipient is @crepels@mastodon.social. This seems to be in violation of the ActivityPub Spec, which says that the shared inbox is to be used for messages to followers and for public posts only. I guess that this is done for simplicity.

Boosting messages

There are a lot of things we could experiment with when it comes to messages, but we will finish this section with boosting a message.

Let’s see what happens when @berta boosts the Hello World message from @alice.

Boosting happens via the Announce activity. This activity also has to and cc fields; it is sent to all followers of @berta, and additionally to @alice as the author of the original post. Note that the object field is just a reference to the original post. In our case, the activitypub.academy instance owns the post so it can show it to all of @berta’s followers on this instance (which is only @alice); but it can also happen that the instance doesn’t know about the post yet, in which case it has to fetch it to show it to its users. This fetching is not part of the ActivityPub protocol, meaning that it doesn’t happen via activities. But the post can be fetched from the ActivityPub data (try opening the URI in the ActivityPub Explorer).

I want to stop this section here. But again, if you are following along on the academy, there are a lot of things you could try out. For example, what happens when you add a content warning to a post? When you edit a post? When you delete a post? When you like a post? What happens when you reply to a post, and how it is reflected in the ActivityPub data? How are polls implemented in ActivityPub?

Moving accounts

The last topic that I want to investigate in this blog post is moving accounts. One of the features of Mastodon which distinguishes it from other social networks is that if you don’t like your instance, you can move to a different instance without losing your followers. But how does that work in practice? How do your followers know that you are now using a different account? Let’s try to find out.

We will move @alice@activitypub.academy to @alastair@activitypub.academy. We’ll assume that @alastair is already followed by @blaine, but neither by @berta nor by @crepels.

On ActivityPub.Academy, accounts are auto-generated and tied to your active session. To create more than one user, you can open the academy in an incognito window or in another browser.

Note that the source and the destination account are on the same instance. We do this so that we can see the activity log of both actors simultaneously. The same type of activities will be sent when the actors are on different instances.

To distinguish the two activity logs, we will display the log of @alice in light mode as before, and the log of @alastair in dark mode.

Before being allowed to move @alice’s account to @alastair, we have to prepare @alastair’s account by creating an account alias. @alastair has to say that he is also known as @alice. (This prevents anyone from moving their account to an account that doesn’t belong to them.) The alias can be created in Mastodon’s Preferences section. (The preferences section is not part of the Single Page App and will result in a page navigation; this will clear the Activity Log. If you are following along, make sure to open the preferences in a separate tab.) When the alias is created, we see the following activity in the log.

An Update activity is sent to all instances that have followers of @alastair; in our case, that’s just techhub.social. The object of the activity is a Person object, which contains the new JSON-LD presentation of the actor. You can see that there is now a alsoKnownAs entry which contains @alice’s actor ID.

Now we can switch to @alice’s account and initiate the account move, again in the Preferences section. In @alice’s log we see the following.

As with @alastair’s account, a Person update is sent to all instances that have followers of @alice’s. The new Person has a movedTo entry which points to @alastair’s actor. A separate Move activity is sent to the instances, indicating that @alice moved to @alastair. This is followed by two Undo activities from @berta and @crepels, reversing their earlier Follow activities. Curiously, there is no Undo activity sent by @blaine, and checking @blaine’s following collection shows that he is indeed still following @alice. It is not entirely clear to me whether this is a bug or intentional (you could argue that @blaine is already a follower of @alastair, so he doesn’t need to be moved), but it definitely was surprising to me.

On @alastair’s Activity Log we get

Both @berta and @crepels sent follow requests which were immediately accepted. @blaine didn’t send a follow request, since he was already following @alastair.

So that’s it. A Move activity is sent to all instances with followers. Those instances are then responsible for unfollowing @alice and following @alastair for all actors that were previously following @alice.

Conclusion

In this blog post, I showed a few examples of how to use the Activity Log and the Activity Explorer to understand ActivityPub. Of course, there is a lot more to explore, and if you are interested in understanding ActivityPub, I would urge you to do your own experiments. There is no better way to really understand a topic than to actually play around with it.

If you do use the academy, I would love to hear your feedback. Does it help you to understand things that you didn’t before? Is there anything that you think could be improved? Let me know on @crepels@mastodon.social.

—Written by Sebastian Jambor. Follow me on Mastodon @crepels@mastodon.social for updates on new blog posts.