groups

Groups

This is a task force of the Social Web Incubator Community Group (SWICG) to explore the use of groups in ActivityPub.

This page should be available at https://swicg.github.io/groups/.

Leads

The task force leads are Evan Prodromou and a.

Timeline

Here are some things that have happened with this task force.

User stories

User stories for this task force are collected and tracked in the GitHub issue tracker. Each user story issue has the user story issue tag.

What is a group?

The English word “group” has many different meanings in different contexts. In social networks, it has two usual meanings:

This task force is focused on the second meaning of “group”. The user stories above define behaviours that are expected of a group in this sense.

Group representation

Groups are represented by an ActivityPub object with object type Group.

Important properties of a group include:

There are other properties mentioned in the user stories that have reasonable implementations as group. These properties can be included in JSON-LD documents using the https://swicg.github.io/groups/0.1.0.jsonld context.

The “owner” relationship can be represented with the attributedTo property. The owner of the group is the actor that created the group. The owner can be a member of the group, but does not have to be.

Question: Should the Group be an ActivityPub actor, including an inbox and outbox? Actors can receive activities and act on them directly. Some of the implementation options for posting to groups require the group to be an actor that can receive and then redistribute activities.

An example group:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://swicg.github.io/groups/0.1.0.jsonld"
  ],
  "id": "https://example.com/groups/world-group",
  "type": "Group",
  "name": "World Group",
  "summary": "A group of people who want to talk about the world.",
  "image": {
    "type": "Link",
    "href": "https://example.com/images/world-group.jpg"
  },
  "icon": {
    "type": "Link",
    "href": "https://example.com/images/world-group-icon.jpg"
  },
  "members": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/members"
  },
  "admins": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/admins"
  },
  "collections": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/collections"
  },
  "pendingActivities": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/pendingActivities"
  },
  "pendingMembers": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/pendingMembers"
  },
  "attributedTo": {
    "id": "https://example.com/actors/3456789012",
    "type": "Person",
    "name": "Evan Prodromou"
  }
}

Protocol

The following sections describe the protocol for groups.

Posting content to a group

To post content to a group, the actor sends an ActivityPub Create activity to the group. The activity must include the object property, which is the content to be posted. The content can be any ActivityPub object, such as an Image, Note, or Video.

An example activity posted to a group (Post privately to a group)

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Create",
  "object": {
    "id": "https://example.com/notes/2345678901",
    "type": "Note",
    "content": "Hello, world!"
  },
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  }
}

The to property of the activity is a Group actor that represents the group.

The Group actor is responsible for redistributing the activity to all the members of the group.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/groups/world-group/activities/1234567890",
  "type": "Announce",
  "actor": "https://example.com/groups/world-group",
  "to": "https://example.com/groups/world-group/members",
  "object": {
    "id": "https://example.com/activities/1234567890",
    "actor": "https://example.com/actors/3456789012",
    "type": "Create",
    "object": {
      "id": "https://example.com/notes/2345678901",
      "type": "Note",
      "content": "Hello, world!"
    },
    "to": {
      "type": "Group",
      "id": "https://example.com/groups/world-group",
      "name": "World Group"
    }
  }
}

The Group actor creates an Announce activity to redistribute the activity to all the members of the group. The to property of the activity is the OrderedCollection that represents the members of the group.

Question: Do the permissions of the Note extend to all actors in the members collection, or just the group actor? Should the posting member also include the members collection in addressing?

To post content to a group publicly (Post publicly to a group), the same flow occurs, but the to property of the original activity includes the Public collection.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/3141592653",
  "actor": "https://example.com/actors/27182818284",
  "type": "Create",
  "object": {
    "id": "https://example.com/notes/141592653",
    "type": "Note",
    "content": "Hello, world and public!"
  },
  "to": [
    {
      "type": "Group",
      "id": "https://example.com/groups/world-group",
      "name": "World Group"
    },
    "as:Public"
  ]
}

The Group actor creates an Announce activity to redistribute the activity to all the members of the group and the public.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/groups/world-group/activities/3141592653",
  "type": "Announce",
  "actor": "https://example.com/groups/world-group",
  "to": [
    {
      "type": "Group",
      "id": "https://example.com/groups/world-group",
      "name": "World Group"
    },
    "as:Public"
  ],
  "object": {
    "id": "https://example.com/activities/3141592653",
    "actor": "https://example.com/actors/27182818284",
    "type": "Create",
    "object": {
      "id": "https://example.com/notes/141592653",
      "type": "Note",
      "content": "Hello, world and public!"
    },
    "to": [
      {
        "type": "Group",
        "id": "https://example.com/groups/world-group",
        "name": "World Group"
      },
      "as:Public"
    ]
  }
}

The pendingActivities collection holds activities that have been posted to the group and not yet redistributed. This allows a moderation step, where admins can review the content of posts for topic or tone (Moderate group posts).

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/groups/world-group/queue",
  "type": "OrderedCollection",
  "attributedTo": "https://example.com/actors/3456789012",
  "to": "https://example.com/groups/world-group/admins",
  "totalItems": 1,
  "items": [
     "https://example.com/activities/1234567890"
  ]
}

An admin can view the queue and approve or reject activities before they are redistributed to the members of the group. To accept, they send an Accept activity to the group.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/groups/world-group/admins/3456789012",
  "type": "Accept",
  "object": "https://example.com/activities/1234567890",
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  },
  "target": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  }
}

To reject, they send a Reject activity to the group.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/groups/world-group/admins/3456789012",
  "type": "Reject",
  "object": "https://example.com/activities/1234567890",
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  },
  "target": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  }
}

🚪 Alternative The above design requires the Group to be an actor that can receive activities. Unlike with most ActivityPub structures, the addressing of an activity or object is not directly related to its authorization grants.

An alternative design would have the Create activity addressed the group members collection directly.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Create",
  "object": {
    "id": "https://example.com/notes/2345678901",
    "type": "Note",
    "content": "Hello, world!"
  },
  "to": {
    "type": "OrderedCollection",
    "id": "https://example.com/groups/world-group/members",
    "name": "World Group members"
  }
}

(Public posts would add as:Public as an addressee, as above.) In this alternative, the addressing properties of the activity and the Note match the authorization grant, namely, to all members of the group. In this design, however, the actor’s server would be responsible for delivering the activity to all members. The activity would not be delivered to the group actor first. This prevents the admins or owner from moderating the content before it is delivered to the group members (Moderate group posts).

Question: Are all activities sent to the group shared with all members? What about activities that are not content focused, such as Like or Question? What about activities that are part of this protocol, such as Join or Leave?

Membership

To get the list of members of a group (Get list of members), an actor fetches the members collection of the group. This will return an OrderedCollection object, with information about the members of the group.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/groups/world-group/members",
  "type": "OrderedCollection",
  "totalItems": 2,
  "items": [
    {
      "id": "https://example.com/actors/3456789012",
      "type": "Person",
      "name": "Evan Prodromou"
    },
    {
      "id": "https://example.com/actors/27182818284",
      "type": "Person",
      "name": "a"
    }
  ]
}

This collection could be filtered by the authorization of the client, so that only members of the group, or even a more limited set of people, can see the full list.

To join a group (Join a group), an actor sends a Join activity to the group. The activity must include the object property, which is the group to join.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Join",
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  },
  "object": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  }
}

The owner and/or admins of the group can accept the request (Accept a Join request). To accept, they send an Accept activity to the group, with the Join activity as the object property.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actor/25418",
  "type": "Accept",
  "object": {
    "id": "https://example.com/activities/1234567890",
    "type": "Join"
  },
  "to": [
    "https://example.com/groups/world-group",
    "https://example.com/actors/3456789012"
  ]
}

To reject (Reject a Join request), an admin sends a Reject activity to the group.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actor/25418",
  "type": "Reject",
  "object": {
    "id": "https://example.com/activities/1234567890",
    "type": "Join"
  },
  "to": [
    "https://example.com/groups/world-group",
    "https://example.com/actors/3456789012"
  ]
}

To leave a group (Leave a group), an actor sends a Leave activity to the group. The activity must include the object property, which is the group to leave.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Leave",
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  },
  "object": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  }
}

🚪 Alternative The above design requires the Group to be an actor that can receive activities. An alternative design would have the actor send the join request to the group’s owner and/or admins.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Join",
  "object": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  },
  "to": [
    {
      "id": "https://example.com/actors/3456789012",
      "type": "Person",
      "name": "Evan Prodromou"
    },
    {
      "id": "https://example.com/groups/world-group/admins",
      "type": "OrderedCollection",
      "name": "World Group admins"
    }
  ]
}

🚪 Alternative The above design for accepting or rejecting Join requests could also use the format for Accept and Reject where a collection is the target, accepting (or rejecting) the joiner into the members collection.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/13337",
  "type": "Accept",
  "object": {
    "actor": "https://example.com/actors/3456789012",
    "type": "Person"
  },
  "target": {
    "id": "https://example.com/groups/world-group/members",
    "type": "OrderedCollection",
    "name": "World Group members"
  },
  "to": [
    {
      "id": "https://example.com/groups/world-group",
      "type": "Group",
      "name": "World Group"
    }
  ]
}

To invite someone to a group (Invite to a group), an actor sends an Invite activity to actor being invited. The sending actor must have authorization to invite other actors.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Invite",
  "object": {
    "id": "https://example.com/groups/world-group",
    "type": "Group",
    "name": "World Group"
  },
  "target": {
    "type": "Person",
    "id": "https://example.com/actors/27182818284",
    "name": "Non-member"
  },
  "to": {
    "type": "Person",
    "id": "https://example.com/actors/27182818284",
    "name": "Non-member"
  },
  "cc": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  }
}

Question: Should other addressees be included in the cc property? For example, the admins, the owner, or the members?

Question: Is it necessary to specify which actors can invite others to the group, or leave it up to the implementation?

To expel someone from a group (Expel from a group), an actor sends an Remove activity for the actor being expelled. The sending actor must have authorization to expel other actors.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/groups/world-group/admins/3456789012",
  "type": "Remove",
  "object": {
    "id": "https://example.com/actors/27182818284",
    "type": "Person",
    "name": "Removed member"
  },
  "target": {
    "type": "OrderedCollection",
    "id":  "https://example.com/groups/world-group/members",
    "name": "World Group members"
  },
  "to": [
    {
      "type": "Group",
      "id": "https://example.com/groups/world-group",
      "name": "World Group"
    },
    {
      "id": "https://example.com/actors/27182818284",
      "type": "Person",
      "name": "Removed member"
    }
  ]
}

🚪 Alternative Other options exist for removing a member, such as using a Reject with their original Join activity, or using a Block activity.

Question: Who has authorization to expel a member? The owner, the admins, members? Protocol-defined, or implementation dependent?

Group lifecycle

The lifecycle of a group object – creation, update, and deletion – is similar to the lifecycle of other ActivityPub objects. The group object is created, updated, and deleted using the same activities as other objects.

To create a group (Create a group), an actor sends a Create activity with the group as the object property. As with other Create activities, the addressing of the activity is the authorization list for visibility for the created object.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Create",
  "object": {
    "type": "Group",
    "name": "World Group"
  },
  "to": "as:Public"
}

Question: This will create a group on the server of the actor. Dedicated group servers may use other methods to create a group. One option is to log in to the group server as an API client to the user’s account server, and then post a Create activity for the group with the id already set, to indicate that the creation is already complete.

To read a group’s information (Get group info), an actor fetches the group object using an HTTP GET request to the object id property. This will return a JSON-LD document with the group information, such as the object shown in “Group representation” above.

To update a group (Change group info), an actor sends an Update activity with the group as the object property. The activity must include the object property, which is the group to update.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Update",
  "object": {
    "id": "https://example.com/groups/world-group",
    "summary": "This is a new group description."
  },
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  }
}

Question: Again, the user’s account server and the Group server may be different. Is this properly handled by this configuration?

To delete a group (Close a group), an actor sends a Delete activity with the group as the object property. The activity must include the object property, which is the group to delete.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/activities/1234567890",
  "actor": "https://example.com/actors/3456789012",
  "type": "Delete",
  "object": {
    "id": "https://example.com/groups/world-group"
  },
  "to": {
    "type": "Group",
    "id": "https://example.com/groups/world-group",
    "name": "World Group"
  }
}

Question: Does the Delete activity need to have any boundaries, or should this be left up to implementations?

Collections

TBD

Admins

TBD

Alternative designs

TBD

Open issues

TBD

Assumed structure

TBD

Group as actor

TBD

Owner

TBD