fep

FEP-76ea: Conversation Threads

Summary

This FEP defines a way to identify the conversation thread of an object with Activity Streams 2.0.

Motivation

Threaded conversations are a common data structure for social software. This is defined as a tree with the original post at its root, replies to that post as child nodes, all replies to those replies as their children, and so on recursively.

Some social software restricts the depth of the thread, while others allow for unlimited depth.

Identifying the thread that an AS2 object is part of allows for the construction of a conversation view of the thread.

It is possible with Activity Streams 2.0 to construct a conversation thread by following the inReplyTo property of an object until the original post is found, and then expanding the replies property of the original post recursively. With ActivityPub, however, this can require a lot of different HTTPS requests to different servers, which can be slow and inefficient.

This FEP defines an extension property, thread, that can be used to identify the conversation thread of an object.

ActivityPub is the primary use case for Activity Streams 2.0, but not the only one. Where specific processing requirements of ActivityPub implementations are made, they are specifically noted. General processing hints for other use cases are also provided.

User stories

These are some user stories for threading in conversations.

  1. As an ActivityPub developer, I want to be able to determine if two objects are part of the same conversation thread, so that I can display them together in a conversation view. This is possible to do by walking up the inReplyTo chain and following the replies collections, but it can be slow and inefficient. Comparing a thread identifier found in each object can be much faster.
  2. As an ActivityPub developer, I want to get the full conversation thread of an object efficiently, so I can show the object in context. Again, using the inReplyTo chain and replies collections can be slow and inefficient, especially if the thread is deep or has many objects.
  3. As an ActivityPub developer, I want to get the most-recently-added objects in a conversation thread, so I can keep my view of the thread up-to-date. This is hard to do with inReplyTo and replies, and possibly requires fetching every single replies collection in the tree.
  4. As an ActivityPub developer, I want to be able to identify the original post of a conversation thread, so I can show the thread in context. This content object is usually treated differently by clients, so it’s useful to be able to identify it.
  5. As a commenter, I want my reply to a content object to be part of the same thread, so I am participating in a larger context.
  6. As the original poster, I want to be able to curate the conversation thread, so I can remove spam, off-topic, or abusive content from the thread. Maintaining a curated thread is an important service that the original poster provides. Curating the replies collection is not sufficient, because it only contains direct replies to the object, not the full conversation tree.
  7. As a user, I want to comment on or about a content object without being part of the conversation thread, so I can discuss it in my own social sphere. Not all continued conversation should be part of the original reply tree.
  8. As a user, I want to branch a reply into its own conversation thread, so I can have a separate conversation about a sub-topic. This is common in forums and other threaded discussion software.
  9. As a user, I want to graft part of a conversation thread into another thread, so I can consolidate related conversations. This is common in forums and other threaded discussion software.

Context

The context URL for this FEP is https://purl.archive.org/socialweb/thread.

The context is as follows:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "thr": "https://purl.archive.org/socialweb/thread#",
  "thread": {
    "@id": "thr:thread",
    "@type": "@id"
  },
  "root": {
    "@id": "thr:root",
    "@type": "@id"
  }
}

Terms

The context defines two properties.

thread

The thread property is an OrderedCollection that contains all of the objects in the conversation thread. The collection is ordered in reverse chronological order, with the most recent object first.

The thread collection does not directly represent the tree structure of the conversation thread; it is a flat list of objects. The tree structure can be reconstructed by following the inReplyTo and/or replies properties of each object in the collection.

The thread property extends the context property from the Activity Vocabulary.

The thread property does not replace the replies property of an object. replies contains the possibly curated collection of direct replies to the object; thread contains the full conversation tree, up- and down-thread.

root

The root property is an Object that is the original post of the conversation thread. The root property is usually the last (earliest) object in the thread collection.

This property gives an easy way for a consumer to find the root post of the thread without having to search the orderedItems collection, navigate through multiple OrderedCollectionPage pages, or traverse the inReplyTo properties of the objects in the collection.

Note that thread and root are partially inverse properties. The thread property of the root property of a collection SHOULD contain the id of the thread collection. However, the root property of the thread property of an object MAY not contain the object’s id, because the object is in the thread, but is not the root.

Behavioral specification

This covers recommended behavior for processors that implement the thread property.

Original posts

When a publisher creates a new content object that is not a reply to any others, it should include a new, unique collection as its thread property. The collection should contain only the new object. The thread collection should be addressed to all the same addressees as the original object.

Replies in the thread

When a publisher is creating a new content object with an inReplyTo property, the publisher SHOULD use the thread property of the object being replied to as the thread property of the new object. The addressees of the new object should include the creator of the original post, identified by the attributedTo property of the original post or the attributedTo property of the thread collection.

Replies can be created to multiple other objects; the inReplyTo property can be an array. The thread property can also be an array, with more or fewer values than the inReplyTo. Each thread property should correspond to the thread property of an object in the inReplyTo array.

Branching

To branch a content object into its own conversation thread, the publisher should create an Announce activity that includes the new object as the object property. The Announce activity should have a new, unique thread property. The Announce activity can include a content property.

Grafting

To graft a content object into a different thread than the ones it is already part of, the publisher should create an Announce activity that includes the new object as the object property. The Announce activity should have the thread property of the new thread, and an inReplyTo property that matches one of the objects in the thread. The Announce activity can include a content property.

Thread maintenance

As with the replies property, the processor implementing the original post of a thread SHOULD maintain the thread collection by adding new objects to the collection as they are received.

In ActivityPub, this could be done when the processor receives an object with an inReplyTo property that matches an object in the thread collection.

To facilitate collection synchronization, the processor SHOULD distribute an Add activity to the audience of the original object with the new object as the object property and the thread as the target property.

However, private replies “down-thread” may not be addressed to the author of the original post and may not be available to the processor for the original post.

The processor implementing the original post MAY curate the thread collection by filtering objects from the collection. This could be done to remove spam, off-topic, or abusive content from the thread.

In ActivityPub, if an object is removed from the thread, he processor SHOULD distribute a Remove activity to the audience of the original object with the new object as the object property and the thread as the target property.

The tree structure of the thread should be maintained; every object in the thread collection, except the root, should have an inReplyTo property that matches the id of another object in the collection. If the processor removes an object from the collection, it SHOULD remove all objects that are in reply to that object, and their replies, and so on.

The replies property of objects in the thread collection MAY be maintained by other processors. Curation of the replies collections or of the thread collection may mean that objects may be omitted from one collection or the other. However, the replies collection of the original post SHOULD be a subset of the thread collection.

Examples

Example 1

An example of a Note object with a thread property:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/note/123",
  "type": "Note",
  "attributedTo": "https://example.com/user/1",
  "to": [
    "https://remote.example/user/17",
    "https://remote.example/user/17/followers"
  ],
  "content": "I concur!",
  "thread": "https://remote.example/thread/117",
  "inReplyTo": "https://remote.example/note/117"
}

Example 2

An example of an Image object with a thread property. The Image is a root or original post with no inReplyTo property:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/image/123",
  "type": "Image",
  "name": "A photo of a cat",
  "attributedTo": "https://example.com/user/1",
  "to": "https://example.com/user/1/followers",
  "url": {
    "type": "Link",
    "mediaType": "image/jpeg",
    "href": "https://example.com/image/123.jpg"
  },
  "replies": "https://example.com/replies/123",
  "thread": {
    "id": "https://example.com/thread/123",
    "to": "https://example.com/user/1/followers",
    "type": "OrderedCollection",
    "totalItems": 4,
    "orderedItems": [
      {
        "id": "https://fourth.example/note/721",
        "attributedTo": "https://fourth.example/user/4",
        "to": [
          "https://example.com/user/1",
          "https://example.com/user/1/followers",
          "https://other.example/user/2"
        ],
        "inReplyTo": "https://other.example/note/338"
      },
      {
        "id": "https://third.example/note/992",
        "attributedTo": "https://third.example/user/3",
        "to": "https://example.com/user/1",
        "inReplyTo": "https://example.com/image/123"
      },
      {
        "id": "https://other.example/note/338",
        "attributedTo": "https://other.example/user/2",
        "to": [
          "https://example.com/user/1",
          "https://example.com/user/1/followers"
        ],
        "inReplyTo": "https://example.com/image/123"
      },
      "https://example.com/image/123"
    ]
  }
}

Note that not all objects in the thread collection need to be addressed to the same audience. The audience of the thread collection is the audience of the original post.

Example 3

This is a Note object that is a reply to two different objects, and thus is part of two different threads.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/note/789",
  "attributedTo": "https://example.com/user/1",
  "to": "as:Public",
  "content": "These are both good points.",
  "inReplyTo": [
    "https://remote.example/note/57",
    "https://other.example/note/456"
  ],
  "thread": [
    "https://remote.example/thread/57",
    "https://other.example/thread/456"
  ]
}

Example 4

Objects in a thread that have been deleted by their author can be represented in the thread collection with a Tombstone object.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/note/345",
  "attributedTo": "https://example.com/user/1",
  "to": "as:Public",
  "content": "Activity Streams 2.0 is awesome!",
  "replies": "https://example.com/replies/345",
  "thread": {
    "id": "https://example.com/thread/345",
    "to": "as:Public",
    "type": "OrderedCollection",
    "orderedItems": [
      {
        "id": "https://third.example/note/567",
      },
      {
        "type": "Tombstone",
        "id": "https://remote.example/note/456",
        "inReplyTo": "https://example.com/note/345",
        "deleted": "2024-10-03T00:00:00Z"
      },
      "https://example.com/note/345"
    ]
  }
}

Example 5

The thread collection can be paged, as with other collections.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/note/678",
  "attributedTo": "https://example.com/user/1",
  "to": "as:Public",
  "content": "Is Wario A Libertarian?",
  "replies": "https://example.com/replies/678",
  "thread": {
    "id": "https://example.com/thread/678",
    "to": "as:Public",
    "type": "OrderedCollection",
    "totalItems": 244780,
    "first": "https://example.com/thread/678/page/12239",
    "last": "https://example.com/thread/678/page/1"
  }
}

Example 6

The root property can be used to identify the original post of a thread.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/thread/654",
  "type": "OrderedCollection",
  "totalItems": 457,
  "first": "https://example.com/thread/654/page/23",
  "last": "https://example.com/thread/654/page/1",
  "root": "https://example.com/note/654"
}

Example 7

To branch an object to a new conversation, an Announce activity is used.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/announce/123",
  "to": "as:Public",
  "type": "Announce",
  "actor": "https://example.com/user/1",
  "thread": "https://example.com/thread/123",
  "content": "I think this note is important and I want to start a separate discussion about it.",
  "object": {
    "id": "https://example.com/note/456",
    "type": "Note",
    "attributedTo": "https://example.org/user/2",
    "thread": "https://example.net/thread/789",
    "inReplyTo": "https://example.net/note/foo",
    "to": "as:Public",
    "content": "Trains are great."
  }
}

Example 8

To graft an object to an existing conversation, an Announce activity is used.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/thread"
  ],
  "id": "https://example.com/announce/456",
  "to": "as:Public",
  "type": "Announce",
  "actor": "https://example.com/user/1",
  "thread": "https://social.example/thread/222",
  "inReplyTo": "https://social.example/note/888",
  "content": "This comment about trains from another thread seems relevant here.",
  "object": {
    "id": "https://example.com/note/456",
    "type": "Note",
    "attributedTo": "https://example.org/user/2",
    "thread": "https://example.net/thread/789",
    "inReplyTo": "https://example.net/note/foo",
    "to": "as:Public",
    "content": "Trains are great."
  }
}

Security Considerations

Not all objects in the thread collection may be addressed to the same audience. Representations of the collection SHOULD NOT include the content property or other sensitive information from objects in the collection that are not addressed to the recipient of the representation.

In ActivityPub, the orderedItems property of the thread collection MAY be filtered for the recipient of the representation.

Previous work

The ostatus:conversation property is used in Mastodon and elsewhere to identify the thread of an object, but it is not necessarily dereferenceable.

Some implementations of ActivityPub use the context property to represent the thread of an object. This FEP provides a more specific property, which frees up the “intentionally vague” context property for other uses. It also avoids the confusing clash with the @context property of JSON-LD.

References

CC0 1.0 Universal (CC0 1.0) Public Domain Dedication

To the extent possible under law, the authors of this Fediverse Enhancement Proposal have waived all copyright and related or neighboring rights to this work.