flak rss random

what happens when you activity post

You’re out there posting on your federated status federator, and people are reading your posts, and you’re reading their posts, but how exactly does it happen? What’s talking to what? (Equally applicable to tooting, but we don’t use that word in my house.)

This post is about ActivityPub. It doesn’t assume too much familiarity with the spec, although it helps if you’ve heard the name before. It also assumes some familiarity with web servers and HTTP in general. It’s an attempt to provide The missing fediverse documentation.

intro

Here are two introductory articles from Mastodon. How to implement a basic ActivityPub server and How to make friends and verify requests outline the basics. I’ll repeat some of that info.

Much of the data requests that will occur in this example will be cached by servers, but I’ll assume a system without caching, so we’re going to make every request possible, though I’ll indicate what’s likely to be cached. Also, going to start by assuming you have some followers.

post

You write a new post. This goes into a textarea in the web interface, or whatever client you’re using. This example contains a mention and a link.

Just read a great blog post by @tedu@honk.tedunangst.com.

https://flak.tedunangst.com/post/what-happens-when-you-honk

You click submit and now the machines are in control.

At this point, most software will turn the mention and URL into HTML links.

The next thing your server will do is build an audience for this post. Based on whether it’s public, or followers only, etc. and who else is mentioned.

We need to resolve the mentions. This is done via a protocol called webfinger. Your server sends a request to https://honk.tedunangst.com/.well-known/webfinger?resource=acct:tedu@honk.tedunangst.com. This returns some JSON data which basically says, yeah, that’s a person, and their URL is https://honk.tedunangst.com/u/tedu. So we’ll add that person to the recipients. (Very likely cached.)

{
  "aliases": [
    "https://honk.tedunangst.com/u/tedu"
  ],
  "links": [
    {
      "href": "https://honk.tedunangst.com/u/tedu",
      "rel": "self",
      "type": "application/activity+json"
    }
  ],
  "subject": "acct:tedu@honk.tedunangst.com"
}

addressing

Next we’re going to iterate over all your followers. Every follower has a URL like https://example.com/users/handle. For every follower, we’re going to fetch that URL to retrieve the actor JSON object, which tells us their inbox. This is very probably going to be https://example.com/users/handle/inbox. If the post is limited in visibility (followers only, direct message, etc.) that’s the inbox we’ll use. (Very likely cached.)

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "followers": "https://honk.tedunangst.com/u/tedu/followers",
  "following": "https://honk.tedunangst.com/u/tedu/following",
  "icon": {
    "mediaType": "image/png",
    "type": "icon",
    "url": "https://honk.tedunangst.com/a?a=https%3A%2F%2Fhonk.tedunangst.com%2Fu%2Ftedu"
  },
  "id": "https://honk.tedunangst.com/u/tedu",
  "inbox": "https://honk.tedunangst.com/u/tedu/inbox",
  "name": "tedu",
  "outbox": "https://honk.tedunangst.com/u/tedu/outbox",
  "sharedInbox": "https://honk.tedunangst.com/inbox",
  "preferredUsername": "tedu",
  "publicKey": {
    "id": "https://honk.tedunangst.com/u/tedu#key",
    "owner": "https://honk.tedunangst.com/u/tedu",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA593GZ9TYrvWgMaMKQ6k6\ngkItUapUgNnNXzU9J63GRtYZ7CE/Zi39Kgpsxu77hHBj34vwjr1Oc9AMrVDIMfu9\nEirW1RWxPvrjThBU56VgkpkAXVsieaffJo80BA00QzV4x69Jgat6OT7ox/HMvMxR\nyZ6CXNCPKQALYqQF6v1fX1kO9lhIA+mPd0JN/qMKvZfd1NXABEk9nORUneH7Audt\nIHNdJzKMHC6wPSQWC7SmXT0/nq6o5mR2SgvwTI/JUx6T5r8NDrwSaqB69e+EMJqR\nxKOh9N4A1ba/AQOQZbO/YkFyYY2VE4HWbvS9XpYL74yT9D6Fp4cUovJiXC+ziam0\nNwIDAQAB\n-----END PUBLIC KEY-----\n"
  },
  "summary": "honk honcho. i do what i can.",
  "type": "Person",
  "url": "https://honk.tedunangst.com/u/tedu"
}

If the post is public, however, we’ll use the sharedInbox property. This might be https://example.com/inbox. Note the absence of username. Many users on the same instance will have the same sharedInbox, so we will deduplicate the list. In the end, we should have a list of URL endpoints.

delivery

We stick the post and the list of recipients in some sort of outbound queue. Then we deliver to everybody. Every request is signed using HTTP Signatures. We POST our message to the inbox endpoint.

Sending a message in ActivityPub usually means a Note wrapped in a Create.

    {
      "actor": "https://honk.tedunangst.com/u/tedu",
      "id": "https://honk.tedunangst.com/u/tedu/honk/CjmfH7xVRr6H3W3y3T",
      "object": {
        "attributedTo": "https://honk.tedunangst.com/u/tedu",
        "content": "<blockquote>The TRV of <code>TemplateCharacter::\\NotEscapeSequence</code> is the sequence consisting of the code unit 0x005C (REVERSE SOLIDUS) followed by the code units of TRV of <code>NotEscapeSequence</code>.</blockquote><p>",
        "context": "data:,electrichonkytonk-qTRKz1t2M1DWRJ67h3",
        "conversation": "data:,electrichonkytonk-qTRKz1t2M1DWRJ67h3",
        "id": "https://honk.tedunangst.com/u/tedu/h/CjmfH7xVRr6H3W3y3T",
        "published": "2019-07-02T18:27:43Z",
        "summary": "",
        "to": "https://www.w3.org/ns/activitystreams#Public",
        "type": "Note",
        "url": "https://honk.tedunangst.com/u/tedu/h/CjmfH7xVRr6H3W3y3T"
      },
      "published": "2019-07-02T18:27:43Z",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public",
        "https://honk.tedunangst.com/u/tedu/followers"
      ],
      "type": "Create"
    }

reception

When the remote server receives a POST to its inbox, it checks the signature on the HTTP request. The signature indicates a keyID, like https://honk.tedunangst.com/u/tedu#key. The remote server then fetches that URL and retrieves a JSON, and extracts the publicKey and publicKeyPem data. Then it verifies the signature. (Very likely cached.) (Also note that the keyname is just a URL fragment appended to the existing actor URL. That’s how we know who the key belongs to.)

If the inbox belongs to a specific user, it saves the message for them. If the inbox is the shared inbox, it might put the message in the federated timeline, etc., but there’s also a bit more work to do. The server needs to find a way to identify who follows you and make sure your post appears in their timeline.

While processing the message, the receiving server is likely to make a number of requests. If there are any links in the post, it will prefetch them to provide link previews. If there are any attachments (images, etc.) it will fetch them and cache them locally. (Server dependent.)

At this point, the receiving server may notice that this post is in reply to another post, via the inReplyTo field. If the receiving server doesn’t have that post, it will fetch it to complete the thread. And continue fetching upthread posts until it reaches the top. So this is a second way that posts propagate. In addition to the servers we directly send our message to, any server receiving a reply to it will fetch a copy.

A fetched Note won’t be wrapped in the create.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "attributedTo": "https://honk.tedunangst.com/u/tedu",
  "content": "<blockquote>The TRV of <code>TemplateCharacter::\\NotEscapeSequence</code> is the sequence consisting of the code unit 0x005C (REVERSE SOLIDUS) followed by the code units of TRV of <code>NotEscapeSequence</code>.</blockquote><p>",
  "context": "data:,electrichonkytonk-qTRKz1t2M1DWRJ67h3",
  "conversation": "data:,electrichonkytonk-qTRKz1t2M1DWRJ67h3",
  "id": "https://honk.tedunangst.com/u/tedu/h/CjmfH7xVRr6H3W3y3T",
  "published": "2019-07-02T18:27:43Z",
  "summary": "",
  "to": "https://www.w3.org/ns/activitystreams#Public",
  "type": "Note",
  "url": "https://honk.tedunangst.com/u/tedu/h/CjmfH7xVRr6H3W3y3T"
}

Finally, the post is shown to receiving users.

sharing

Clicking share performs many of the same steps, however the message is usually simpler. This is the message that would be delivered to followers after sharing.

    {
      "actor": "https://honk.tedunangst.com/u/tedu",
      "cc": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "context": "tag:botsin.space,2019-07-02:objectId=17297234:objectType=Conversation",
      "id": "https://honk.tedunangst.com/u/tedu/bonk/102372944890029780",
      "object": "https://botsin.space/users/hackaday/statuses/102372944890029780",
      "published": "2019-07-02T18:38:20Z",
      "to": [
        "https://botsin.space/users/hackaday",
        "https://honk.tedunangst.com/u/tedu/followers"
      ],
      "type": "Announce"
    }

outline

1. Submit post to server.

2. Web finger requests to resolve mentions. (cached)

3. ActivityPub inbox lookups for each recipient. (cached)

4. Post to inbox for each recipient.

5. Receiver does key lookup back to origin to verify signature. (cached)

6. Receiver fetches attached media and link previews. (optional, but likely)

7. Receiver fetches upstream posts. (optional)

8. User sees post.

Posted 02 Jul 2019 21:24 by tedu Updated: 02 Jul 2019 21:24
Tagged: activitypub web