Containers
An Introduction to Conversation Containers
A conversation is a collection of messages with a common context. The ActivityStreams specifications define both collections and contexts, but very little guidance is provided on how to use them effectively.
This document specifies an ActivityPub compatible model for managing conversations as Collection objects with a common context.
In ActivityPub messaging, an actor creates a note and delivers it to their followers. On the receiving side, the message is analysed, and if it is found to be acceptable, it is placed in the receiver's inbox - which is implemented as a collection.
The top level post of a conversation (often a Note object) is defined as an object without an inReplyto property.
Most existing ActivityPub compatible software adds a context element to this top level post object in order to associate it with a given conversation; and this context is typically added to all children of the conversation in order to indicate their association. Replies can nest to abitrary levels and form a conversation tree if objects within the same context set inReplyTo to a message other than the top-level post, for instance to another Note object within the same context which already has inReplyTo set to a different object.
The microblog ActivityPub conversation model, which is used by Mastodon (and some other ActivityPub software) is considered "unconstrained". In that model, members of the conversation are free to send their replies to any audience (typically but not limited to their own followers), and this may lead to fragmented conversations as each node of the conversation may be distributed to completely different audiences. The actor creating a reply is also generally unaware if the conversation creator accepted the comment into the original conversation thread and in turn published the comment to the original audience.
In the conversational model, the same basic steps occur, but respects the rights of the conversation creator, or the actor that created the initial "top level" post of a conversation thread. These conversations take place within a specific audience - and may be moderated. Conversations adopting this model of interaction are considered "constrained".
Creating a constrained conversation
A constrained conversation is implemented as a Collection. FEP-400e provides a model for working with collections. In that proposal, ActivityPub objects which are created as part of a collection are indicated by a
target
"stub" property containing- type -- (
Collection
orOrderedCollection
of activities) - id -- a URL where this collection can be found
- attributedTo -- the
id
of the collection owner
In a constrained conversation, conforming implementations will implement FEP-400e with some very minor additions:
- The collection contains complete activities, not simple objects.
- In a constrained conversation, the target->id and the context are identical. This provides easy identification.
- In a constrained conversation, replies SHOULD only be addressed to the target->attributedTo actor.
- When receiving a correctly signed
Add
activity, checking that the id of the target is a collection owned byactor
is slightly different from FEP-400e. This can be accomplished by fetching the Collection object and validating the attributedTo field.
Processing is otherwise identical. In the reference implementation of conversation containers by the streams repository, signed objects are often transmitted instead of just transmitting the ids, and these are signed using a combination of FEP-521a and FEP-8b32.
Example activities
Initial Create
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/data-integrity/v1",
"https://purl.archive.org/socialweb/webfinger",
{
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
}
],
"type": "Create",
"id": "https://streams.lndo.site/activity/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"published": "2024-03-05T18:28:26Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/red",
"url": "https://streams.lndo.site/activity/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"object": {
"type": "Note",
"id": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"published": "2024-03-05T18:28:26Z",
"attributedTo": "https://streams.lndo.site/channel/red",
"canReply": "https://streams.lndo.site/followers/red",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"content": "This is a post.",
"source": {
"content": "This is a post.",
"mediaType": "text/x-multicode"
},
"url": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"to": [
"https://streams.lndo.site/followers/red"
],
"cc": []
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"to": [
"https://streams.lndo.site/followers/red"
],
"cc": [],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:28:27Z",
"verificationMethod": "https://streams.lndo.site/channel/red#z6MkurfhRuTSaT1u6usPV7GKdbbTQWUQUtr3MA2ZSx8XSjAo",
"proofPurpose": "assertionMethod",
"proofValue": "z4fhQZC4pupyUL4E4z28B3dorBz5nPzYRr47or4LxvHHqjYKecoUQq7JMik35KTNMUBJJx5A54PJWiTxWXxJBxD4F"
}
}
Add to conversation
{
"@context": {
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
},
"type": "Add",
"id": "https://streams.lndo.site/activity/7fa1c190-27ee-4d7f-9c92-6945057a3009",
"published": "2024-03-05T18:28:26Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/red",
"url": "https://streams.lndo.site/activity/7fa1c190-27ee-4d7f-9c92-6945057a3009",
"object": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/data-integrity/v1",
"https://purl.archive.org/socialweb/webfinger",
{
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
}
],
"type": "Create",
"id": "https://streams.lndo.site/activity/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"published": "2024-03-05T18:28:26Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/red",
"url": "https://streams.lndo.site/activity/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"object": {
"type": "Note",
"id": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"published": "2024-03-05T18:28:26Z",
"attributedTo": "https://streams.lndo.site/channel/red",
"canReply": "https://streams.lndo.site/followers/red",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"content": "This is a post.",
"source": {
"content": "This is a post.",
"mediaType": "text/x-multicode"
},
"url": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"to": [
"https://streams.lndo.site/followers/red"
],
"cc": []
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"cc": [],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:28:26Z",
"verificationMethod": "https://streams.lndo.site/channel/red#z6MkurfhRuTSaT1u6usPV7GKdbbTQWUQUtr3MA2ZSx8XSjAo",
"proofPurpose": "assertionMethod",
"proofValue": "z5X6eXVx5ac1FpYczgc5oyjECZEQ5VHKjU5cUpPiRLRAAgHgFVZvemW9QmcDCoMk5Aa3hAYjAW9LZor4qmf9Sxm6J"
}
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"to": [],
"cc": [
"https://streams.lndo.site/followers/red",
"https://streams.lndo.site/channel/red"
],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:29:27Z",
"verificationMethod": "https://streams.lndo.site/channel/red#z6MkurfhRuTSaT1u6usPV7GKdbbTQWUQUtr3MA2ZSx8XSjAo",
"proofPurpose": "assertionMethod",
"proofValue": "z2cFyTkRvpuRFnpngm6jJst1gYChtqV66CvqXC6UcjJJM4V2w5y9D3CyCoyBUCAnahNNWcUA8V3BCqwtaygUKrVMh"
}
}
Create a reply
- target is optional
- reply is typically delivered to the actor indicated in target->attributedTo only.
- the conversation owner (target->attributedTo) then sends the resulting Add activity to the original recipients of the conversation.
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/data-integrity/v1",
"https://purl.archive.org/socialweb/webfinger",
{
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
}
],
"type": "Create",
"id": "https://streams.lndo.site/activity/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"published": "2024-03-05T18:35:36Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/blue",
"url": "https://streams.lndo.site/activity/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"object": {
"type": "Note",
"id": "https://streams.lndo.site/item/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"published": "2024-03-05T18:35:36Z",
"attributedTo": "https://streams.lndo.site/channel/blue",
"inReplyTo": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"content": "This is a comment.",
"source": {
"content": "This is a comment.",
"mediaType": "text/x-multicode"
},
"url": "https://streams.lndo.site/item/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"tag": [
{
"type": "Mention",
"href": "https://streams.lndo.site/channel/red",
"name": "@[email protected]"
}
],
"to": [
"https://streams.lndo.site/channel/red"
],
"cc": [
"https://streams.lndo.site/followers/red"
]
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"tag": [
{
"type": "Mention",
"href": "https://streams.lndo.site/channel/red",
"name": "@[email protected]"
}
],
"to": [
"https://streams.lndo.site/channel/red"
],
"cc": [
"https://streams.lndo.site/followers/red"
],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:35:37Z",
"verificationMethod": "https://streams.lndo.site/channel/blue#z6MkqKgnFq5bzNsrVvHcwBsg2dbzTUWsp2MfyZYCVSjzNEgy",
"proofPurpose": "assertionMethod",
"proofValue": "zAiLxL3Uii4a65XKfVZdz2Tg2x7EebAqetfmWgSa5xeMxoYJkzJSg7hHJPLaqbtDvp53H56peBnyrjYzYB2N8Zit"
}
}
Conversation owner Adds the reply to the conversation Collection
- If the post at the head of the conversation contains a Collection target with an id that matches the context attribute, only Add/Remove operations sent by target->attributedTo are accepted as replies.
- The entire contained Collection can be retrieved by fetching the context element
- receivers can validate the original activity and authorship by checking its proof.
{
"@context": {
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
},
"type": "Add",
"id": "https://streams.lndo.site/activity/04abe24f-6814-487e-a060-b0931e58e454",
"published": "2024-03-05T18:35:37Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/red",
"url": "https://streams.lndo.site/activity/04abe24f-6814-487e-a060-b0931e58e454",
"object": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/data-integrity/v1",
"https://purl.archive.org/socialweb/webfinger",
{
"nomad": "https://streams.lndo.site/apschema#",
"toot": "http://joinmastodon.org/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "nomad:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"canSearch": "nomad:canSearch",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"copiedTo": "nomad:copiedTo",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
}
],
"type": "Create",
"id": "https://streams.lndo.site/activity/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"published": "2024-03-05T18:35:36Z",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"actor": "https://streams.lndo.site/channel/blue",
"url": "https://streams.lndo.site/activity/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"object": {
"type": "Note",
"id": "https://streams.lndo.site/item/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"published": "2024-03-05T18:35:36Z",
"attributedTo": "https://streams.lndo.site/channel/blue",
"inReplyTo": "https://streams.lndo.site/item/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"context": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"content": "This is a comment.",
"source": {
"content": "This is a comment.",
"mediaType": "text/x-multicode"
},
"url": "https://streams.lndo.site/item/f6bfcb4e-49a1-4172-8d33-62fb4f498812",
"tag": [
{
"type": "Mention",
"href": "https://streams.lndo.site/channel/red",
"name": "@[email protected]"
}
],
"to": [
"https://streams.lndo.site/channel/red"
],
"cc": [
"https://streams.lndo.site/followers/red"
]
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"tag": [
{
"type": "Mention",
"href": "https://streams.lndo.site/channel/red",
"name": "@[email protected]"
}
],
"to": [
"https://streams.lndo.site/channel/red"
],
"cc": [
"https://streams.lndo.site/followers/red"
],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:35:37Z",
"verificationMethod": "https://streams.lndo.site/channel/blue#z6MkqKgnFq5bzNsrVvHcwBsg2dbzTUWsp2MfyZYCVSjzNEgy",
"proofPurpose": "assertionMethod",
"proofValue": "zAiLxL3Uii4a65XKfVZdz2Tg2x7EebAqetfmWgSa5xeMxoYJkzJSg7hHJPLaqbtDvp53H56peBnyrjYzYB2N8Zit"
}
},
"target": {
"id": "https://streams.lndo.site/conversation/ed4775f8-18ee-46a5-821e-b2ed2dc546e8",
"type": "Collection",
"attributedTo": "https://streams.lndo.site/channel/red"
},
"to": [],
"cc": [
"https://streams.lndo.site/followers/red",
"https://streams.lndo.site/channel/red"
],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2024-03-05T18:39:08Z",
"verificationMethod": "https://streams.lndo.site/channel/red#z6MkurfhRuTSaT1u6usPV7GKdbbTQWUQUtr3MA2ZSx8XSjAo",
"proofPurpose": "assertionMethod",
"proofValue": "z2ebC7DHjZ3js7dLXMbgN5v6s3PBFW6BFmrBtjDH83Bpvc4ceACqTo4PKP9ySE7dV3vfpvqz4xcgnPwF1GNV1TckC"
}
}