Groups are a shared context for communications organized around bounded membership in a set of group members. Membership in a group is distinct from following the group; being a follower entails potentially receiving future activities published by an actor, but being a member entails a social role within the group independently of whether activities are received. This document explores ways to create and manage groups within the ActivityPub ecosystem.
In a social setting, activities can be bound to various contexts. These contexts can be things like:
Popular implementations of ActivityPub (and popular social networking and social media services) have unfortunately historically neglected support for binding resources to context, in a problem known as context collapse. In particular, the social ecosystem widely lacks primitives for posts having authorship and audience bound to a consistent but dynamic collective group.
In social psychology, groups are established based on a shared purpose or identity that mediates group activities. For example, users hosted on the same network or service do not necessarily share any common identity. You could describe a "collection of all users on this service", but this is not necessarily the same as a "group". The quality that defines a "group" is its entitativity, or the perception of being a group (from [[[understanding-social-groups]]]). It is only when the Group becomes a "real" "entity" (or is "reified") that we can consider it a Group (and not simply a Collection of actors). We can describe properties of the group separately from properties of its members, and separately from properties of the collection.
Similarly, membership in a group entails its own properties within the group context, separately from the properties of the individual who is a group member. For example, you might have permission to perform certain actions in one group, but not in another. It would be imprecise to say that the individual holds those privileges; rather, the member holds those privileges within the group context, and that membership is a social role performed by the individual. Outside of the group context, membership considerations do not apply to the individual.
The term "Group" is used in other ways that may or may not align with the use of the word "Group" in this document.
The [[[FOAF]]] is one of the earliest vocabularies to define a concept of a "Group", alongside other social concepts. In particular, relations like "member" and "membershipClass" can be used to describe who is a member of the group. FOAF defines "Group" like so:
The Group class represents a collection of individual agents (and may itself play the role of a Agent, ie. something that can perform actions).
This concept is intentionally quite broad, covering informal and ad-hoc groups, long-lived communities, organizational groups within a workplace, etc. [...]
The [[[RFC6350]]] defines a KIND of "group" like so:
a vCard representing a group of persons or entities. The group's member entities can be other vCards or other types of entities, such as email addresses or web sites. A group vCard will usually contain MEMBER properties to specify the members of the group, but it is not required to. A group vCard without MEMBER properties can be considered an abstract grouping, or one whose members are known empirically (perhaps "IETF Participants" or "Republican U.S. Senators").
All properties in a group vCard apply to the group as a whole, and not to any particular MEMBER. For example, an EMAIL property might specify the address of a mailing list associated with the group, and an IMPP property might refer to a group chat room.
The concept of membership, while optional, is one of the defining features of a group according to vCard. It is possible to describe membership in a group using both the vocabulary of vCard and the vocabulary defined in this document.
The [[[activitystreams-vocabulary]]] defines a Group only in the most general terms:
Represents a formal or informal collective of Actors.
The groups described by this document can be said to be instances of https://www.w3.org/ns/activitystreams#Group, but they carry additional considerations. Activity Streams 2.0 does not define a concept of membership within a "Group".
Note that the membership concepts defined in this document can be used with other types, and the protocols for managing membership depend on the presence of properties rather than the presence of types.
[[[activitypub]]] defines an actor system where activities can be published to outboxes and pushed to addressed audiences as notifications. Primarily, ActivityPub defines a way to manage "followers" via the "Follow" activity, which one can later "Accept" or "Reject":
The follow activity generally is a request to see the objects an actor creates. This makes the Followers collection an appropriate default target for delivery of notifications.
However, binding all social contexts to the concept of "followers" limits the range of social contexts that people can participate in. Being a follower does not carry any inherent social considerations beyond a simple desire to see more activities from a certain actor. Also, the concept of members is distinct from the concept of followers; it is possible to be a member without receiving activities intended for followers.
[[[FEP-1b12]]] uses the Activity Streams 2.0 "Group" term directly, but assumes that any or all "Group" objects necessarily adhere to the protocol requirements laid out in the FEP. In FEP-1b12, a "Group" receives activities in its inbox, and publishes activities to its ActivityPub followers where the type is Announce and the object of the Announce is the received activity (instead of forwarding from inbox as defined in ActivityPub).
The groups described in this document are not constrained in which activities they produce. It is possible to produce Announce activities in accordance with the aforementioned FEP, but this is not a requirement of this document.
[[[RFC9420]]] and [[[RFC9750]]] define a notion of a "group" as a shared cryptographic state:
A client that is part of a group is a member of that group. As groups change membership and group or member properties, they advance from one epoch to another and the cryptographic state of the group evolves.
This document's notion of groups similarly allows for binding state to the group context, but without a strong requirement for cryptography or encryption.
Some platforms implement "group" functionality, although it may not be called a "group".
A popular conception of "groups" is as a sort of forum. However, while the Forums task force deals with the organization of posts into threads and/or forums, the Groups task force deals with the organization of actors into groups which have members and roles. Thus, the work of these two task forces is complementary.
Since MLS is designed around a concept of "groups", alignment between this document's model of groups and the MLS model of groups can allow implementation of the group architecture via an MLS group's application data, and opening the possibility of both authenticated and encrypted groups.
A group can be described like any other resource. The following terms from the ActivityStreams context (`https://www.w3.org/ns/activitystreams`) can be broadly useful:
TODO: any missing properties?
TODO: describe in more detail
A `members` collection contains `Member` records for each group member.
TODO: needs discussion
For convenience, group managers can define roles to create subgroupings of group members
A group SHOULD publish information about which interactions it will accept, and from whom, by carrying an interaction policy in the `interactionPolicy` property of its actor object. Each entry names an interaction and states who the group will accept it from. The policy is published as a hint: it SHOULD describe what the group accepts, while deciding stays with the group actor itself (see ). An interaction is any action a policy names, whether it adds content to the group, acts on content already there, or acts on the group itself.
Interaction policies are already in use on individual posts rather than on groups, described in [[[gotosocial-interaction-controls]]], used by [[[FEP-044f]]] to obtain consent for quoting, and implemented on that basis by [[[mastodon-activitypub]]]. What this section adds is the group as a subject; the shape is unchanged.
Each key of `interactionPolicy` names an interaction, and MUST map to an object with the properties `automaticApproval` and `manualApproval`. Each property MUST hold ActivityPub identifiers of actors or collections of actors, or the value `as:Public`. Values are identifiers rather than names (e.g. `https://group.example/g/cooks/followers` instead of `followers`). Full IRIs SHOULD be used, since many consumers compare values as plain strings without JSON-LD processing.
{
"type": "Group",
"id": "https://group.example/g/cooks",
"followers": "https://group.example/g/cooks/followers",
"attributedTo": "https://group.example/g/cooks/moderators",
"interactionPolicy": {
"canJoin": {
"automaticApproval": ["https://group.example/g/cooks/followers"],
"manualApproval": ["https://www.w3.org/ns/activitystreams#Public"]
}
}
}
An interaction that is absent or `null` is unconstrained by this policy.
Absence is deliberately not a denial. Publishing a policy is best practice rather than a requirement, so a group that publishes none, or omits an interaction from one, keeps federating exactly as groups do today.
A group states that an interaction is not allowed by anyone by naming nobody but itself: `automaticApproval` holding at most the group's own actor, and `manualApproval` empty or absent. This is not the same as leaving the interaction out. An entry naming nobody says the group enforces the interaction and permits no one else, while an absent entry says nothing at all.
{
"interactionPolicy": {
"canJoin": {
"automaticApproval": ["https://group.example/g/cooks"],
"manualApproval": []
}
}
}
This reads the lists as complete, which an open-world reading of the data would not: nothing else being named does not, in RDF terms, mean no one else is permitted. The closed reading is deliberate: a policy is the group's own statement of its own rules, so what it lists is meant as the whole list rather than inferred from absence.
A policy names actors and collections, so a group needs no vocabulary of roles to express one. A group that wants a grouping such as "safety team" or "trusted posters" publishes that collection and names it in the entries it should govern. A consumer can then check whether an actor is in the collection without knowing what the group means by the name.
A policy on the group's actor object governs interactions with the group itself and with the objects it has announced or authored. A policy on an individual object governs interactions with that object, and MUST take precedence over the group's where both name the same interaction.
{
"type": "Group",
"id": "https://group.example/g/cooks",
"interactionPolicy": {
"canReply": {
"automaticApproval": ["https://group.example/g/cooks/members"]
}
}
}
{
"type": "Note",
"id": "https://group.example/g/cooks/notes/rules",
"attributedTo": "https://group.example/g/cooks",
"interactionPolicy": {
"canReply": {
"automaticApproval": ["https://group.example/g/cooks/moderators"]
}
}
}
An object that merely names a group in its `audience` asserts only its own author's claim, which does not by itself bring the object under the group's policy. A consumer MUST NOT treat such a claim as evidence that the object belongs to the group.
A policy governs who may act, not who may see. Naming an actor in one MUST NOT be read as granting it visibility of the group or of any object within the group, and an actor that cannot see an object MUST NOT be permitted to interact with it. Visibility remains determined by `to`, `cc` and `audience`.
{
"type": "Note",
"id": "https://alice.example/notes/1",
"attributedTo": "https://alice.example",
"audience": "https://group.example/g/cooks",
"interactionPolicy": {
"canReply": {
"automaticApproval": ["https://group.example/g/cooks/members"]
}
}
}
The interactions themselves, and their names, are not defined here. See issue #26.
Interaction names are IRIs, so a specification or an implementation MAY define its own without coordination, provided it does so in a namespace it controls.
{
"interactionPolicy": {
"canJoin": {
"automaticApproval": ["https://www.w3.org/ns/activitystreams#Public"]
},
"https://software.example/ns#canPin": {
"automaticApproval": ["https://group.example/g/cooks/moderators"]
}
}
}
A group SHOULD serve an interaction policy with its actor object that reflects what it actually enforces. The policy it serves can be the rules its own server evaluates when an activity arrives, or a description of rules it holds separately.
Including an interaction in its policy indicates two things: that the group supports interaction policies, and that it enforces that interaction.
A group SHOULD therefore include every interaction it enforces, including those whose value does not differ from the default.
Silence is ambiguous in a way that matters: a group stating nothing might place no restrictions, or might run software that has not implemented interaction policies. The two call for opposite handling, since there is no use in waiting on a decision from a server that will never make one. Writing out a permission, even at its default value, settles it.
To anyone other than the group publishing it, a policy is a declaration of intent. A client that understands one SHOULD use it to present only the actions a user is likely to be permitted to perform, and an actor SHOULD use it to decide whether an attempt is worth making.
An actor interacting with a remote group MUST NOT treat a permissive policy as a guarantee that the interaction will be accepted, and SHOULD NOT read an absent interaction as a denial.
A consumer that does not recognise an interaction SHOULD continue to apply the interactions it does recognise, rather than disregarding the policy as a whole.
Where a group publishes no policy at all, a consumer cannot tell whether it places no restrictions or does not support interaction policies.
Where a group publishes a policy that omits a given interaction, the group supports interaction policies but does not describe that interaction as restricted.
In either case an actor can still attempt the interaction, as it would with any group that publishes no policy. Since the group has advertised no support for it, a consumer MUST NOT rely on it being recognised or accepted, and SHOULD NOT present it as having taken effect within the group until the group acts on it.
An actor attempts an interaction by sending the activity to the group, usually by delivering it to the group's inbox. There is no separate step of asking first: the activity is the request, in either case.
Whilst interaction policies on posts outside any group have a separate step (because there is no group to redistribute: the interacting actor distributes its own interaction, so it must obtain approval beforehand and attach it), a group instead distributes the interaction itself, on the interacting actor's behalf and under its own authority, which both performs the interaction and evidences that it was permitted. It also saves the round trips, since a group can reject an interaction simply by not distributing it.
What the policy tells the actor's client is how to describe the state while it waits. Presenting the interaction as pending is always correct, in either case, and a client MAY do only that. Where the actor is listed in `automaticApproval` a client MAY instead present it as having taken effect; where the actor is listed in `manualApproval` it SHOULD NOT, since a decision is pending and may never come.
Either presentation is provisional. The actual state follows from what the group does: an `Accept`, or the group acting on the interaction and publishing the result to its outbox.
This is how a `Follow` of an actor with `manuallyApprovesFollowers` already behaves. The same activity is sent either way, and the difference the user sees is "Following" rather than "Requested".
{
"type": "Join",
"id": "https://alice.example/activities/1",
"actor": "https://alice.example",
"object": "https://group.example/g/cooks",
"to": "https://group.example/g/cooks"
}
A group server MUST check each activity delivered to its inbox against its own rules, and MUST NOT rely on a sender having consulted or followed its policy.
A group SHOULD accept an interaction from an actor listed in `automaticApproval`, MAY hold one from an actor listed in `manualApproval` pending a decision, and SHOULD NOT accept one from an actor listed in neither.
An interaction takes effect within the group only once the group acts on it, by announcing it, or by acting on what it affects and publishing the outcome to its outbox. That action is also what evidences permission: a group that redistributes an interaction thereby asserts that it permitted it, so a separate approval object is not needed.
{
"type": "Announce",
"id": "https://group.example/g/cooks/activities/3",
"actor": "https://group.example/g/cooks",
"object": {
"type": "Note",
"id": "https://alice.example/notes/1",
"attributedTo": "https://alice.example",
"audience": "https://group.example/g/cooks"
},
"to": ["https://group.example/g/cooks/followers"]
}
Because a group can have more than one actor able to decide on its behalf, and those actors can be on other servers, a policy MAY also state who can accept/reject interactions. Where it does, a group server may reject any decision from an actor it does not name.
{
"interactionPolicy": {
"canJoin": {
"manualApproval": ["https://www.w3.org/ns/activitystreams#Public"]
},
"canAcceptJoin": {
"automaticApproval": ["https://group.example/g/cooks/greeters"]
}
}
}
{
"type": "Accept",
"id": "https://bob.example/activities/7",
"actor": "https://bob.example",
"object": "https://alice.example/activities/1",
"to": "https://group.example/g/cooks"
}
{
"type": "Accept",
"id": "https://group.example/g/cooks/activities/8",
"actor": "https://group.example/g/cooks",
"object": "https://alice.example/activities/1",
"to": "https://alice.example"
}
TODO: needs discussion
`context` or something similar?
TODO: needs discussion
TODO: needs discussion
https://swicg.github.io/groups/#Memberhttps://swicg.github.io/groups/#membershttps://swicg.github.io/groups/#membershipsFor information about the group:
For voluntarily managing one's own membership:
For managing other people's memberships in a group you are authorized to manage:
ActivityPub defines a flow for Follow, Accept, Reject, and Undo activities with the following states:
And the following state transitions:
This design from ActivityPub has the following shortcomings and issues:
It can seem appealing to mirror the Follow activity flow and its side effects, but with the Join activity instead of the Follow activity -- perhaps assuming that familiarity with the existing Follow flow will make it easier to implement a parallel Join flow. However, doing so inherits all of the aforementioned issues and would lead to a repeat of the implementation errors faced in practice.
To address the shortcomings of the mirrored flow described above, we need a design with the following properties:
The simplest way to create a membership record is to rely on something like HTTP PUT at an indexed location:
PUT /members/alice.example HTTP/1.1
Content-Type: application/activity+json
{
"type": "_:Member",
"_:user": {"id": "https://alice.example"},
"name": "arisu"
}
Alternatively, HTTP POST can be used instead, to let the members collection choose the resulting Location:
POST /members/ HTTP/1.1
Content-Type: application/activity+json
{
"type": "_:Member",
"_:user": {"id": "https://alice.example"},
"name": "arisu"
}
The response might show that the membership is pending:
HTTP/1.1 201 Created
Location: https://group.example/members/01KSQFYKVAGWH0HW09QMTD2VC9
Content-Type: application/activity+json
{
"type": "_:Member",
"_:user": {"id": "https://alice.example"},
"_:pending": true,
"name": "arisu",
}
In this case, the member and the user are not the same -- the member wraps the user. Similarly, metadata about your membership within a group is not metadata of the actor. We can use membership records/entities to achieve this separation.
In MLS, clients are added to groups via Proposal/Commit, and then separately receive a Welcome message that contains the keying material needed to actually join the group. This "add before join" pattern means that the Join does not exist before the client has actually joined the group.
Per MLS Architecture Section 3.7 "Users, Clients, and Groups":
Note that until a client has been added to the group and contributed to the group secret in a manner verifiable by other members of the group, other members cannot assume that the client is a member of the group; for instance, the newly added member might not have received the Welcome message or been unable to decrypt it for some reason.
We could have the new member confirm their membership with a Join activity (which gets republished to the group's outbox), instead of using the Join activity to manage the membership itself.