Activity Streams/Primer/Extensions

From W3C Wiki

The Activity Streams vocabulary covers many of the basic needs of the social web, but Activity Streams 2.0 is intended to be extended.

There are a few ways that AS2 encourages extensions:

  1. Adding properties to existing types
  2. Adding new Object types
  3. Adding new Activity types

Understanding extensibility

There are two main goals to extensibility:

  1. Representing activities, objects, and properties that aren't covered in the Core or Vocabulary specs.
  2. Supporting general-purpose Activity Streams processors for parsing, distribution, delivery, sorting, etc.

Activity Streams 2.0 extensions aren't self-explanatory or self-executing. It takes documentation and software to turn a pet-adoption schema into actual adoption of dogs and cats. However, ideally, the schema for pet adoptions could (and should) run over existing distribution infrastructure like ActivityPub.

Extended example

For this discussion, let's consider a domain not covered by AS2: playing asynchronous chess over the social web.

Some important activities in this domain:

  • Inviting someone to play a game.
  • Accepting an invitation.
  • Moving a piece.
  • Resigning the game.

Some important object types that come out of these activities:

  • A player
  • A kind of game (Chess, Polo, Tiddlywinks, ...)
  • A game instance (the game of chess that Chester played against Chessbot 3000 on 2023-03-30)
  • A chess piece
  • A square on the board
  • The game state

In the following examples, we'll start using in-line @context changes, and we'll talk about moving to a separate context document at the end.

Using existing types

The Activity Vocabulary covers a lot of ground. For the invitation and acceptance, for example, there are some clear mappings between our chess domain and the standard vocabulary.

To invite someone to play a game, we could use the Invite activity type.

{
   "@context": "https://www.w3.org/ns/activitystreams",
   "type": "Invite",
   "id": "https://social.example/activity/D7E7B576-E7E3-4775-8652-B4D8E1AC3191",
   "actor": {
      "type": "Person",
      "name": "Chester Q. Playerton",
      "id": "https://social.example/@cp"
   },
   "object": { 
      // ... The chess game type; see below
   },
   "target": {
      "type": "Application",
      "name": "Chessbot 3000",
      "id": "https://games.example/@chessbot"
   }
}

Here, a person is inviting a program to the game of chess. The app could accept:

{
   "@context": "https://www.w3.org/ns/activitystreams",
   "type": "Accept",
   "actor": {
      "type": "Application",
      "name": "Chessbot 3000",
      "id": "https://games.example/@chessbot"
   },
   "object": { 
      "type": "Invite",
      "id": "https://social.example/activity/D7E7B576-E7E3-4775-8652-B4D8E1AC3191",
   },
   "result": {
      // ... a chess game instance; see below
   }
}

Alternately, it could reject:

{
   "@context": "https://www.w3.org/ns/activitystreams",
   "type": "Reject",
   "actor": {
      "type": "Application",
      "name": "Chessbot 3000",
      "id": "https://games.example/@chessbot"
   },
   "object": { 
      "type": "Invite",
      "id": "https://social.example/activity/D7E7B576-E7E3-4775-8652-B4D8E1AC3191",
   },
   "context": {
       "type": "Note",
       "content": "Chessbot 3000 is a paid service."
   }
}

Adding properties

A chess player is usually a Person or an Application. Chess players often have a rating that shows their relative ability with the game.

In object-oriented programming languages, it's usually necessary to make a subclass in order to add properties to a class. This isn't necessary with Activity Streams 2.0. We can just add a rating property to objects with existing types. For example:

{
    "@context": [
       "https://www.w3.org/ns/activitystreams",
       { "chess": "https://chess.example/", 
         "xsd": "http://www.w3.org/2001/XMLSchema#",
         "rating": {
              "@id": "chess:rating",
              "@type": "xsd:nonNegativeInteger"
         }
       }
     ],
     "type": "Person",
     "name": "Chester Q. Playerton",
     "id": "https://social.example/@cp",
     "rating": 800
}

Adding new Object types

There are some object types we're discussing that really need new types. For example, a game (a set of rules), a game instance, a chess piece, a board state, a square on the board.

Here are some definitions of types with examples. Note that the extension types use fallback values for the type property, so that other processors can still work with them, even if they're not aware of the chess game schema. As a best practice, producers should include at least one type from the Activity Vocabulary, even if it's Object or Link.

Here's an Activity representing a move by Chester in turn 1.

{
    "@context": [
       "https://www.w3.org/ns/activitystreams",
       { "game": "https://game.example/",
         "chess": "https://chess.example/", 
         "Game": {
            "@type": "@id",
            "@id": "game:Game",
         },
         "GameInstance": {
            "@type": "@id",
            "@id": "game:GameInstance",
         },
         "GameInstanceState": {
            "@type": "@id",
            "@id": "game:GameInstanceState",
         },
         "ChessInstance": {
            "@type": "@id",
            "@id": "chess:ChessInstance",
         },
         "BoardState": {
            "@type": "@id",
            "@id": "chess:BoardState",
         },
         "Square": {
            "@type": "@id",
            "@id": "chess:Square",
         },
         "Pawn": {
            "@type": "@id",
            "@id": "chess:Pawn",
         }
       }
     ],
     "type": "Move",
     "id": "https://social.example/@cp/activity/1024",
     "actor": {
        "type": "Person",
        "name": "Chester Q. Playerton",
        "id": "https://social.example/@cp"
      },
      "context": {
           "type": ["BoardState", "GameInstanceState", "Object"],
           "id": "https://games.example/@chessbot/game/37/turn/1/white",
           "context": {
              "id": "https://games.example/@chessbot/game/37",
              "type": ["ChessInstance", "GameInstance", "Object"],
           }
      },    
      "object": {
           "type": ["Pawn", "Object"],
           "id": "https://chess.example/Pawn"
      },
      "origin": {
           "type": ["Square", "Place"],
           "name": "e2",
           "id": "https://chess.example/e2"
      },
      "target": {
           "type": ["Square", "Place"],
           "name": "e4",
           "id": "https://chess.example/e4"
      },
      "result": {
           "type": ["BoardState", "GameInstanceState", "Object"],
           "id": "https://games.example/@chessbot/game/37/turn/1/black",
           "context": {
              "id": "https://games.example/@chessbot/game/37",
              "type": ["ChessInstance", "GameInstance", "Object"],
           }
      }, 
}

It should be clear that the object of the Invite activity above would be the game of chess, and the result of the Accept activity would be a new ChessInstance.

TODO: cover more advanced moves like castling or promoting a pawn.

Adding new Activity types

One new Activity type is Resigning -- a process that will end the game.

{
    "@context": [
       "https://www.w3.org/ns/activitystreams",
       { "game": "https://game.example/",
         "chess": "https://chess.example/", 
         "GameInstance": {
            "@type": "@id",
            "@id": "game:GameInstance",
         },
         "GameInstanceState": {
            "@type": "@id",
            "@id": "game:GameInstanceState",
         },
         "ChessInstance": {
            "@type": "@id",
            "@id": "chess:ChessInstance",
         },
         "BoardState": {
            "@type": "@id",
            "@id": "chess:BoardState",
         },
         "Resign": {
            "@type": "@id",
            "@id": "chess:Resign",
         }
       }
     ],
     "type": ["Resign", "Leave"],
     "id": ""https://games.example/@chessbot/activities/30189",
     "actor": {
        "type": "Application",
        "name": "Chessbot 3000",
        "id": "https://games.example/@chessbot"
      },
      "object": {
         "id": "https://games.example/@chessbot/game/37",
         "type": ["ChessInstance", "GameInstance", "Object"],
      },
      "context": {
           "type": ["BoardState", "GameInstanceState", "Object"],
           "id": "https://games.example/@chessbot/game/37/turn/1/black",
           "context": {
              "id": "https://games.example/@chessbot/game/37",
              "type": ["ChessInstance", "GameInstance", "Object"],
           }
      } 
}

Make a complete context

A next step would be creating a complete context document and making it available at https://chess.example/. That way, you can just refer to it in your Activity Streams documents.

Note that we're pre-supposing a game schema that's already been fleshed out elsewhere.

{
    "@context": [
       "https://www.w3.org/ns/activitystreams",
       { "game": "https://game.example/" }
       { "chess": "https://chess.example/" },
     "type": ["chess:Resign", "Leave"],
     "id": ""https://games.example/@chessbot/activities/30189",
     "actor": {
        "type": "Application",
        "name": "Chessbot 3000",
        "id": "https://games.example/@chessbot"
      },
      "object": {
         "id": "https://games.example/@chessbot/game/37",
         "type": ["chess:ChessInstance", "game:GameInstance", "Object"],
      },
      "context": {
           "type": ["chess:BoardState", "game:GameInstanceState", "Object"],
           "id": "https://games.example/@chessbot/game/37/turn/1/black",
           "context": {
              "id": "https://games.example/@chessbot/game/37",
              "type": ["chess:ChessInstance", "game:GameInstance", "Object"],
           }
      } 
}

Get consensus

When building software for interoperability, it's a good idea to get consensus from others in the developer community.

Publishing the context on the SWICG mailing list, proposing it for the agenda for an upcoming meeting, or otherwise circulating the proposal is a good idea.

Implement the schema

After a round or two of comments, it's a good idea to implement the schema. One important thing is to get at least two independent implementations, so you can make sure you're not building in any assumptions.

Make revisions

After the implementation, there may need to be revisions. You should try as hard as humanly possible to make them backwards compatible -- software networks in the Internet upgrade slowly, and causing any undue incompatibilities will reduce the usage of the protocol entirely. About the only time you should make breaking changes is when there is a critical security, privacy, or accessibility problem.

Add the extension to the standard namespace

A widely-used extension can get added to the main namespace. See https://www.w3.org/ns/activitystreams#extensions for examples and process.