Jump to content

ActivityPub/Primer/Replies

From W3C Wiki

Every ActivityStreams object can have a replies property. This is a collection of objects that are responses to the object with the property; usually because they have the original object as their inReplyTo property.

The replies property is the way that ActivityPub models comments, commenting, and conversations.

Any Object type can have a replies property. However, the property is most common and most applicable on content types like:

  • Note
  • Article
  • Image
  • Video
  • Audio

Another use of replies is for some kinds of Activity objects. For example, if Alice posts a Travel activity, Bob might comment, "I hope you have fun!" The reply is to the activity, not to Alice, nor to either of the places she travelled between.

replies is unusual but possible for actors or collections.

Creating replies

Suppose Alice creates this note (we'll call this "the original post" in following text):

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://social.example/user/alice/note/15",
  "type": "Note",
  "attributedTo": "https://social.example/user/alice",
  "to": "https://social.example/user/alice/followers",
  "content": "Hello, followers!",
  "replies": {
     "id":  "https://social.example/user/alice/note/15/replies",
     "type": "OrderedCollection",
     "totalItems": 0
  }
}

The Note is only available to Alice and her followers. The replies property is expanded inline here for clarity. When first posted, the note has no replies.

Bob replies to Alice:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://other.example/user/bob/note/23",
  "type": "Note",
  "attributedTo": "https://other.example/user/bob",
  "to": [
    "https://social.example/user/alice/followers",
    "https://social.example/user/alice"
  ],
  "inReplyTo": "https://social.example/user/alice/note/15",
  "content": "Hello, back, Alice!"
}

After Bob's reply, Alice's post might look like this to Alice and her followers:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://social.example/user/alice/note/15",
  "type": "Note",
  "attributedTo": "https://social.example/user/alice",
  "to": "https://social.example/user/alice/followers",
  "content": "Hello, followers!",
  "replies": {
     "id":  "https://social.example/user/alice/note/15/replies",
     "type": "OrderedCollection",
     "totalItems": 1,
     "items": [
       {
         "id": "https://other.example/user/bob/note/23",
         "type": "Note",
         "attributedTo": "https://other.example/user/bob",
         "to": [
            "https://social.example/user/alice/followers",
            "https://social.example/user/alice"
         ],
         "content": "Hello, back, Alice!"
       }
     ]
  }
}

There may be some delay in the update; Alice's server may give Alice a chance to screen replies for unfriendly responses. The server might even do automated screening of blocked authors or spammy content.

Fetching replies

To continue the example, let's consider that Carol and Dave also send replies to Alice:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://third.example/user/carol/note/4",
  "type": "Note",
  "attributedTo": "https://third.example/user/carol",
  "to": [
    "https://social.example/user/alice/followers",
    "https://social.example/user/alice"
  ],
  "inReplyTo": "https://social.example/user/alice/note/15",
  "content": "Nice to see you!"
}

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://fourth.example/user/dave/note/8",
  "type": "Note",
  "attributedTo": https://fourth.example/user/dave",
  "to": [
    "https://social.example/user/alice"
  ],
  "inReplyTo": "https://social.example/user/alice/note/15",
  "content": "We should talk soon about work."
}

Carol, like Bob, has replied to both Alice and Alice's followers. Dave, however, has only replied to Alice -- it's a more private response, only for her eyes.

In general, replies should always address the author of the original post. They can include the full audience of the original post, or a more restricted subset.

Addressing a broader group, such as the as:Public collection, or one's own followers, is frowned upon, and may result in replies being automatically rejected by servers. An exception is for addressing individual actors to bring them into the conversation.

After Carol and Dave's replies, when Alice fetches the replies collection, it might look like this:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id":  "https://social.example/user/alice/note/15/replies",
  "type": "OrderedCollection",
  "attributedTo": "https://social.example/user/alice",
  "to": [
     "https://social.example/user/alice/followers"
  ],
  "totalItems": 3,
  "items": [
     "https://other.example/user/bob/note/23",
     "https://third.example/user/carol/note/4",
     "https://fourth.example/user/dave/note/8"
  ]
}

Here, we have switched over to a more compact representation of the replies, with only the id property of each reply shown.

Note that the replies collection has the same author and addressees as the original post, and thus the same access control.

Alice has access to all the replies, since they are all addressed to her explicitly. But if Bob fetches the replies collection, he might get this result instead:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id":  "https://social.example/user/alice/note/15/replies",
  "attributedTo": "https://social.example/user/alice",
  "to": [
     "https://social.example/user/alice/followers"
  ],
  "type": "OrderedCollection",
  "totalItems": 3,
  "items": [
     "https://other.example/user/bob/note/23",
     "https://third.example/user/carol/note/4"
  ]
}

Bob's and Carol's notes are addressed to Alice's followers, and because Bob is one of Alice's followers, and because the server at social.example knows that, he can see both of those notes listed. But Dave's note is addressed only to Alice, and Bob is not allowed to read it. ActivityPub servers usually filter out collection members that they know the client cannot read, and the replies collection is no exception. So, the id of Dave's reply has been filtered out. However, the totalReplies property still shows 3 replies.

To properly display the full conversation -- or, at least the part he has access to -- Bob's client will need to get the full metadata and content of Carol's note. The canonical version is available at the url https://third.example/user/carol/note/4. Bob's ActivityPub API client can use the proxyUrl endpoint on his server other.example to fetch Carol's note from third.example, so that the HTTP Signatures authentication correctly identifies him.

There remains a wrinkle, however. Per the standard authorization model of ActivityPub, only the author, Carol, and the addressees, Alice and Alice's followers, are allowed to read the note. Since Bob is one of Alice's followers, he should be able to read the note. The collection of Alice's followers is stored canonically on social.example, though. In order to check if Bob is a member of that collection, third.example will need to fetch the full collection of Alice's followers from that remote server. It may have thousands of members and many pages of results. The membership data is very cacheable, but there's a hurdle here for third.example.

Also noteworthy is that some servers protect the collection of followers so that no remote servers can read it. In this case, there is no easy way for Carol's server to confirm that Bob is one of Alice's followers, and it will refuse access to the note to Bob.

One way to avoid this issue is for the replies collection on social.example to include an expanded copy of Carol's note, with the metadata needed by most clients to display it (content, published, attributedTo, and so on).

But the authorization process may be more complicated. Carol may have personally blocked Bob, in which case the request will fail. Or moderators of third.example may have blocked the other.example server, in which case the request will also fail.

Despite the challenges of fetching remote content objects, showing only IDs in the replies collection remains the best way to preserve user safety.

Notifying addressees of replies

How can Bob's client have the most up-to-date list of comments if new notes are being added all the time?

Repeatedly fetching the replies collection from social.example every time Bob looks at Alice's note is inefficient -- especially if nothing has been added. It's better if Alice's server pushes those changes to all addressees of the original post when they arrive, rather than depending on polling to keep things up to date.

There are two ways to do this. The first is inbox forwarding. In this case, when Alice's server social.example receives a reply, it shares it directly with local addressees and with members of any addressed local collection. This only happens under the following conditions:

  • The reply addresses a collection on the server, like a list of followers, or a curated list of contacts maintained by the user.
  • The reply has an inReplyTo property that matches an object on the server.

When Carol's server delivers the Create activity for her note to Alice's server, these conditions are met, and Alice's server will in turn deliver it to the servers for all of Alice's followers.

Note that Alice's server will not deliver to non-local addressees that aren't members of the local collection; Carol's server is responsible for that.

There are some downsides to this. First, as mentioned above, some of those followers may be blocked by Carol, and the full content of her Note should not be delivered to them. Alice's server does not have access to information about Carol's blocked users.

Another downside is that it's difficult for Bob's server to validate the inbox forwarding. Usually, servers should not accept activities hosted on one server to be delivered by another server, and authenticated by a different user. Teasing out that this is OK because of inbox forwarding is more difficult for Bob's server.

A different technique is for Alice's server to use an explicit activity, from Alice's account, to show that Carol's note has been added to the replies collection. The Accept or Announce activity could work here, but the Add activity is becoming a de facto standard for this kind of notification:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://social.example/user/alice/add/42",
  "type": "Add",
  "actor": "https://social.example/user/alice",
  "to": [
    "https://social.example/user/alice/followers"
  ],
  "object": "https://third.example/user/carol/note/4",
  "target": "https://social.example/user/alice/note/15/replies"
}

Here, the activity comes from Alice and is delivered by Alice's server with Alice's authentication, so Bob's server will not have to make any exceptions for it. Add is a standard activity that's part of ActivityPub specifically for managing collections like the replies collection. Finally, Carol's note is only referred to by ID; her server still has a chance to perform extended authorization checks when Bob's client requests the note (probably through proxyUrl).