Skip to content

Figuring out Mastodon#

by Reno Muijsenberg

Did you know that a single protocol, ActivityPub, is the backbone of the entire Fediverse? This powerful tool allows data to flow freely between Mastodon servers, fostering a truly interconnected social web. Today, we'll explore ActivityPub and show you how to use it to retrieve your Mastodon data.

In this article I will try to get a better understanding of the ActivityPub protocol by 'reverse engineering' and figuring out what data we can retrieve from the Social Edu Mastodon servers.

Key ActivityPub Endpoints#

After having delved into the world of the ActivityPub. There are a few endpoints that we are going to explore.

Variable syntax

For the remainder of this article I will use the syntax {value} as variables, where value is replaced with the expected information.

WebFinger#

This endpoint utilizes the WebFinger protocol to discover the ActivityPub actor associated with a specific username and domain combination.

  • Endpoint: {base_url}/.well-know/webfinger?resource=acct:{username}@{domain}

User#

This endpoint retrieves information about a particular user on the ActivityPub server.

  • Endpoint: {base_url}/users/{username}

Outbox#

The outbox endpoint acts as the repository for ActivityStreams objects created by the user.

  • Endpoint: {base_url}/users/{username}/outbox

Inbox#

The inbox endpoint serves as the designated location for receiving ActivityStreams objects directed towards the user.

  • Endpoint: {base_url}/users/{username}/inbox

Retrieving Information#

In this section we will try and retrieve or post data to the Key ActivityPub Endpoints specified in the previous section.

WebFinger#

First we will start by making a HTTP-GET request to the WebFinger endpoint as this should contain the endpoint link to our user account. This endpoint can simply be opened in the browser to view the information.

Input:

Endpoint https://social.edu.nl/.well-known/webfinger?resource=acct:Reno@social.edu.nl
Method GET
Client Brave Browser
Headers -

Output:

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

User#

The returned JSON response in previous section contains links to the user's Mastodon profile. First we will try to get the information via the browser.

Input:

Endpoint https://social.edu.nl/users/Reno
Method GET
Client Brave Browser
Headers -

Output:

  • Social.edu Mastodon profile page.

While visiting these links redirects us to the users profile, it does not provided the desired information regarding the users account, such as the endpoints to the inbox, outbox etc.

The next step to try is to I will try is to make a request to the provided endpoints using the Postman client, in this client we will make a HTTP-GET request to:

Input:

Endpoint https://social.edu.nl/users/Reno
Method GET
Client Postman
Headers -

Output:

  • Source code of Social.edu Mastodon profile page.

The next step I will try is to change the Accept header of the request to application/activity+json. To change this header, navigate to the Headers tab in the Postman client and add a new header. - Key: Accept - Value: application/activity+json

Input:

Endpoint https://social.edu.nl/users/Reno
Method GET
Client Postman
Headers Accept: application/activity+json

Output:

{
    "@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",
            "memorial": "toot:memorial",
            "indexable": "toot:indexable"
        }
    ],
    "id": "https://social.edu.nl/users/Reno",
    "type": "Person",
    "following": "https://social.edu.nl/users/Reno/following",
    "followers": "https://social.edu.nl/users/Reno/followers",
    "inbox": "https://social.edu.nl/users/Reno/inbox",
    "outbox": "https://social.edu.nl/users/Reno/outbox",
    "featured":"https://social.edu.nl/users/Reno/collections/featured",
    "featuredTags":"https://social.edu.nl/users/Reno/collections/tags",
    "preferredUsername": "Reno",
    "name": "Muijsenberg,Reno R.F.",
    "summary": "",
    "url": "https://social.edu.nl/@Reno",
    "manuallyApprovesFollowers": false,
    "discoverable": false,
    "indexable": false,
    "published": "2024-03-07T00:00:00Z",
    "memorial": false,
    "devices": "https://social.edu.nl/users/Reno/collections/devices",
    "publicKey": {
        "id": "https://social.edu.nl/users/Reno#main-key",
        "owner": "https://social.edu.nl/users/Reno",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzA4Ho30x+6tudnHyEvWp\n8E5uWgDMU6yYQ5BFxNhWZ8+I1BJ/Uedt7NTtzouo0NaLj8SAgGZsFLeg3emx9fLx\nJ2QzxUiIiHGwvKlLFZzTPYxYo9p1AsAYQblO9aPLcwD8XTTdyjCa+1UXl+fQSiU6\nEIrfBx6L6m6JmCTAGSks7MVuZd0csj9wJbyVn/gEzSQ2zVteghC3HMJ6dHNbOBZG\nILjO13Xbr09oGX0Y53fkgeEW4wwrO3a83NHTmBlJVFsA3Jo5b0Vyc26h6ZL39br1\nQE2yyw3AQguXmLEoobYaf1XQeMlkUSArnajF70XnGNQZzyrqACjUOWltOPfPpJm2\nEQIDAQAB\n-----END PUBLIC KEY-----\n"
    },
    "tag": [],
    "attachment": [],
    "endpoints": {
        "sharedInbox": "https://social.edu.nl/inbox"
    }
}

In this big JSON there are a couple of things to note down, first of all are the two endpoints for the inbox and outbox.

  • Inbox: https://social.edu.nl/users/Reno/inbox
  • Outbox: https://social.edu.nl/users/Reno/outbox

Then there is an object that contains the information of the public keys, used to sign requests.

  • Id: https://social.edu.nl/users/Reno#main-key
  • Owner: https://social.edu.nl/users/Reno
  • PublicKeyPem: -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A{rest of key}\nEQIDAQAB\n-----END PUBLIC KEY-----\n

Outbox#

Now that we retrieved the inbox and outbox from the user, we will try and read the outbox using the browser, as this is the most simple way to GET the information.

Input:

Endpoint https://social.edu.nl/users/Reno/outbox
Method GET
Client Brave Browser
Headers -

Output:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://social.edu.nl/users/Reno/outbox",
  "type": "OrderedCollection",
  "totalItems": 0,
  "first": "https://social.edu.nl/users/Reno/outbox?page=true",
  "last": "https://social.edu.nl/users/Reno/outbox?min_id=0&page=true"
}

As we can see this JSON returns the data belonging the my outbox, the data is being structured with an OrderedCollection, as should be the case stated in the ActivityPub specs.

ActivityPub Specs

The outbox MUST be an OrderedCollection.

The strange thing about this is that it the totalItems is 0, because I did send and receive a friend requests. So, it properly is partially protected by some kind of authentication.

Note

Even after syncing the Mastodon cookies (_mastodon_session, _session_id, SimpleSAML and SimpleSAMLAuthToken) with my local Postman. Its still only showed an empty outbox.

Inbox#

The last thing to try and do, is to POST something to our own inbox. To do this I will use the Postman client, and use a body from the original ActivityPub specs but only change the from and to, to my own account.

Input:

Endpoint https://social.edu.nl/users/Reno/inbox
Method POST
Client Postman
Headers Content-type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
Body
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "type": "Note",
    "to": [
        "https://social.edu.nl/users/Reno"
    ],
    "attributedTo": "https://social.edu.nl/users/Reno",
    "content": "Say, did you finish reading that book I lent you?"
}

Output:

{
    "error": "Request not signed"
}

Update

After reading the Mastodon documentation, I figured out that to POST something to someone's inbox endpoint, we need to sign the request.

Conclusion#

Yet to come...