This FEP defines a way to identify the conversation thread of an object with Activity Streams 2.0.
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.
These are some user stories for threading in conversations.
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.inReplyTo chain and replies collections can be slow and inefficient, especially if the thread is deep or has many objects.inReplyTo and replies, and possibly requires fetching every single replies collection in the tree.replies collection is not sufficient, because it only contains direct replies to the object, not the full conversation tree.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"
}
}
The context defines two properties.
threadThe 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.
rootThe 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.
This covers recommended behavior for processors that implement the thread property.
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.
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.
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.
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.
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.
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"
}
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.
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"
]
}
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"
]
}
}
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"
}
}
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"
}
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."
}
}
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."
}
}
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.
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.
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.