2019-03-30
Last year I started playing around with the Solid platform, introduced by Tim Berners-Lee. In order to get more familiar with the different concepts and technologies used, I created a proof-of-concept app: a browser chess game.
The main concept of the Solid platform is the use of Solid PODs, which provide personal online data storage. The idea of Solid is that a user has one or more PODs to store their data and different apps are able to interact with these PODs, but these apps can only read and write the data to which the user has granted them access. So, instead of having to store all data of all users on a single server per app, users now store and control (!) their own data, leading to a decentralized approach. The focus of this blog post is on the interaction between the app and the Solid PODs.
First, we list the different features of the chess app. Second, we elaborate on the three high-level components of the app. Third, we list the different steps that are taken when certain actions are done by a player. Finally, we discuss how the data generated by this app can be used by other apps.
The app provides the following features:
The app consists of three high-level components: the GUI, the chess game engine, and the interaction with the POD. The GUI uses the chessboard.js library, which offers the chessboard and the interaction of the players with the board, such as the moving of the pieces. It does not provide a chess engine, i.e., it will not check whether the moves are valid or not and keep track of which player's turn it is. To accomplish this we make use of the chess.js library. Although it provides the required information to play a chess game, the data is not presented as Linked Data, which is preferred by the Solid platform. Linked Data is a method of publishing structured data so that it can be interlinked and become more useful through semantic queries. Therefore, I created a "wrapper" library around chess.js called semantic-chess that outputs RDF, a technology used to materialize Linked Data, which can be used during the interaction of the app with the POD. The focus of this blog post is on this interaction.
When the app is launched, one of the first actions a player does is logging in. For this we use the solid-auth-client library. It keeps track of the session and provides a fetch method that is used whenever we want to interact with the POD. Whenever a player is logged in the fetch method is able to store and read data from PODs to which the logged-in player has access.
In the GUI there is a classic log-in button.
When clicked, a pop-up appears that allows selecting your identify provider
to which you want to authenticate (see Figure 1).
You can provide your own HTML file to render the pop-up or
you can use the default one provided by the library.
After successfully logging in,
the pop-up is closed and the focus is back on the app.
The solid-auth-client
library has tracked the fact that a player has logged in and
now a session object is available.
This session object contains, for example, the Web ID of the logged in player.
A Web ID is an HTTP URI that denotes an agent on an HTTP-based network.
In line with the Linked Data principles, when a Web ID is de-referenced,
it resolves to a profile document that describes its referent, i.e., the player.
Furthermore, the fetch method provided by the library will now act on behalf of the player.
When a player starts a new game he needs to select an opponent, the app needs to store the game data on the player's POD, and invite the opponent.
With this action, the player selects an opponent for a new game. In Figure 2, we describe the corresponding steps between the player, the player's app, the player's POD, and the PODs of his friends. The steps are:
The app does a GET to the player's Web ID. If the Web ID of the player is
https://player.solid.community/profile/card#me
copy success
then the corresponding curl
command is
curl https://player.solid.community/profile/card#me
copy success
The player's POD returns RDF describing the player. An example of such RDF is
@prefix : <https://player.solid.community/profile/card#>. @prefix inbox: <https://player.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix c0: <https://opponent.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows c0:me; foaf:name "Player".
copy success
The app queries the RDF to determine the player's friends,
via the predicate foaf:knows
.
An example of SPARQL query using this predicate is
PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX player: <https://player.solid.community/profile/card#> SELECT ?friend WHERE { player:me foaf:knows ?friend. }
copy success
For every friend the following three steps are taken.
The app does a GET to the friend's Web ID. If the Web ID of the player is
https://opponent.solid.community/profile/card#me
copy success
then the corresponding curl
command is
curl https://opponent.solid.community/profile/card#me
copy success
The friend's POD returns RDF describing the friend. This RDF is similar to the RDF of step 2. An example of such RDF is
@prefix : <https://opponent.solid.community/profile/card#>. @prefix inbox: <https://opponent.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix player: <https://player.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows player:me; foaf:name "Opponent".
copy success
The app queries the RDF to determine the friend's name,
via the predicate foaf:name
.
An example of SPARQL query using this predicate
for an friend with the Web ID
https://opponent.solid.community/profile/card#me
copy success
is
PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX opponent: <https://opponent.solid.community/profile/card#> SELECT ?name WHERE { opponent:me foaf:name ?name. }
copy success
The app shows a list of friends to the player.
The player selects the desired friend as his opponent.
Remarks
foaf
, is one of several ontologies,
such as Schema.org and vCard,
that can be used to describe the basic information of a person.
At the time of writing, when creating a default profile with Solid mostly FOAF is used.
However, bear in mind that other Solid PODs might use (a combination of) other ontologies.With this action, the game data of a new game is stored on the player's POD. In Figure 3, we describe the corresponding steps between the player, the player's app, and the player's POD. The steps are:
The player starts the game.
The player's app generates RDF that describing this specific instance of a chess game. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. @prefix stor: <http://example.org/storage/>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. :game a chess:ChessGame; stor:storeIn :; chess:providesAgentRole :jo8deywv, :jo8deyww; chess:starts :jo8deywv; schema:name "Test game". :jo8deywv a chess:WhitePlayerRole; chess:performedBy player:me. :jo8deyww a chess:BlackPlayerRole; chess:performedBy opponent:me.
copy success
It includes
The player's app does a PATCH with a SPARQL UPDATE query to the player's POD with the RDF. A PATCH is used because the file where we want to store the RDF, chosen by the player, might already contain data, which we do not want to overwrite. An example of such a PATCH is
PREFIX : <https://player.solid.community/public/chess.ttl#> PREFIX chess: <http://purl.org/NET/rdfchess/ontology/> PREFIX stor: <http://example.org/storage/> PREFIX schema: <http://schema.org/> PREFIX player: <https://player.solid.community/profile/card#> PREFIX opponent: <https://opponent.solid.community/profile/card#> INSERT DATA { :game a chess:ChessGame; stor:storeIn <>; chess:providesAgentRole :jo8deywv, :jo8deyww; chess:starts :jo8deywv; schema:name "Test game". :jo8deywv a chess:WhitePlayerRole; chess:performedBy player:me. :jo8deyww a chess:BlackPlayerRole; chess:performedBy opponent:me. }
copy success
With this action, an invitation is sent to the opponent to join the newly created game. In Figure 4, we describe the corresponding steps between the player's app and the opponent's POD. The steps are:
The player's app generates RDF describing the invitation. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. :invitation a schema:InviteAction>; schema:event :game; schema:agent player:me; schema:recipient opponent:me.
copy success
The player's app does PATCH with a SPARQL UPDATE query to store the invitation on the player's POD. An example of such a PATCH is
PREFIX schema: http://schema.org/> PREFIX : <https://player.solid.community/public/chess.ttl#> PREFIX player: <https://player.solid.community/profile/card#> PREFIX opponent: <https://opponent.solid.community/profile/card#> INSERT DATA { :invitation a schema:InviteAction; schema:event :game; schema:agent player:me; schema:recipient opponent:me. }
copy success
The player's app generates RDF for a notification of the invitation. An RDF example of such an notification is
@prefix schema: <http://schema.org/> . @prefix : <https://player.solid.community/public/chess.ttl#>. :invitation a schema:InviteAction .
copy success
The player's app does a GET to the Web ID of the opponent. If the Web ID of the player is
https://player.solid.community/profile/card#me
copy success
then the corresponding curl
command is
curl https://player.solid.community/profile/card#me
copy success
The opponent's POD returns RDF describing the opponent. An example of such RDF is
@prefix : <https://opponent.solid.community/profile/card#>. @prefix inbox: <https://opponent.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix c0: <https://player.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows c0:me; foaf:name "Opponent".
copy success
The player's app queries the RDF to determine the opponent's inbox
via the predicate ldp:inbox
.
An example of a SPARQL query using this predicate is
PREFIX ldp: <http://www.w3.org/ns/ldp#> SELECT ?inbox WHERE { <https://opponent.solid.community/profile/card#> ldp:inbox ?inbox. }
copy success
The player's app does a POST to the inbox with the notification of the invitation. When we do a POST to an inbox a new file, containing the notification, is created in the inbox.
With this action, the opponent joins the game for which he received an invitation. In Figure 5, we describe the corresponding steps between the player, the opponent, the player's app, the player's POD, the opponent's app, and the opponent's POD. The steps are:
The opponent's app does a GET to the Web ID of the the logged-in user, which in this case is the opponent.
The opponent's POD returns RDF describing the opponent. An example of such RDF is
@prefix : <https://opponent.solid.community/profile/card#>. @prefix inbox: <https://opponent.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix player: <https://player.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows player:me; foaf:name "Opponent".
copy success
The opponent's app queries the RDF to determine the opponent's inbox
via the predicate ldp:inbox
.
An example of a SPARQL query using this predicate is
PREFIX ldp: <http://www.w3.org/ns/ldp#> PREFIX opponent: <https://opponent.solid.community/profile/card#> SELECT ?inbox WHERE { opponent:me ldp:inbox ?inbox. }
copy success
The opponent's app does a GET to the inbox, which contains links that identify the different notifications.
The opponent's POD returns RDF describing the inbox. An example of such RDF is
@prefix inbox: <https://opponent.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix terms: <http://purl.org/dc/terms/>. @prefix XML: <http://www.w3.org/2001/XMLSchema#>. @prefix st: <http://www.w3.org/ns/posix/stat#>. inbox: a ldp:BasicContainer, ldp:Container; terms:modified "2019-01-10T15:00:26Z"^^XML:dateTime; ldp:contains inbox:765a0510-14e8-11e9-a29e-5d8e3e616ac9, n0:; st:mtime 1547132426.207; st:size 4096. inbox:765a0510-14e8-11e9-a29e-5d8e3e616ac9 a ldp:Resource; terms:modified "2019-01-10T15:00:26Z"^^XML:dateTime; st:mtime 1547132426.207; st:size 1369.
copy success
The opponent's app queries the RDF to determine the notifications, i.e., their links,
via the class ldp:Resource
.
An example of a SPARQL query using this predicate is
PREFIX ldp: <http://www.w3.org/ns/ldp#> SELECT ?notification WHERE { ?notification a ldp:Resource . }
copy success
The opponent's app iterates over all notifications in the inbox.
The opponent's app does a GET to the link of the notification.
The opponent's POD returns RDF describing the notification. An example of such RDF is
@prefix schema: <http://schema.org/> . @prefix : <https://player.solid.community/public/chess.ttl#>. :invitation a schema:InviteAction .
copy success
The app queries the RDF to determine if the notification contains an invitation,
via the class schema:InviteAction
.
An example of a corresponding SPARQL query is
PREFIX schema: <http://schema.org/> SELECT ?invitation WHERE { ?invitation a schema:InviteAction . }
copy success
If the notification contains an invitation, the opponent's app does a GET to the link of the invitation.
The player's POD returns RDF describing the invitation. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. :invitation a schema:InviteAction; schema:event :game; schema:agent player:me; schema:recipient opponent:me.
copy success
The opponent's app queries the RDF to determine the link of the game and the Web ID of the opponent. A corresponding SPARQL query is
PREFIX : <https://player.solid.community/public/chess.ttl#> PREFIX schema: <http://schema.org/> SELECT ?game ?opponent WHERE { :invitation schema:event ?game; schema:agent ?opponent. }
copy success
The opponent's app shows the invitation to the opponent.
The opponent accepts the invitation.
The opponent's app generates RDF describing the response. An example of such RDF is
@prefix response: <https://opponent.solid.community/public/chess.ttl#response>. @prefix invitation: <https://player.solid.community/public/chess.ttl#invitation>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. response: a schema:RsvpAction; schema:rsvpResponse schema:RsvpResponseYes; schema:agent opponent:me; schema:recipient player:me. invitation: schema:result response:.
copy success
The opponent's app does PATCH with a SPARQL UPDATE query to store the response on the opponent's POD. An example of such a PATCH is
PREFIX response: <https://opponent.solid.community/public/chess.ttl#response> PREFIX invitation: <https://player.solid.community/public/chess.ttl#invitation> PREFIX schema: <http://schema.org/> PREFIX player: <https://player.solid.community/profile/card#> PREFIX opponent: <https://opponent.solid.community/profile/card#> INSERT DATA { response: a schema:RsvpAction; schema:rsvpResponse schema:RsvpResponseYes; schema:agent opponent:me; schema:recipient player:me. invitation: schema:result response:. }
copy success
The player's app generates RDF for a notification of the response. An RDF example of such an notification is
@prefix response: <https://opponent.solid.community/public/chess.ttl#response>. @prefix schema: <http://schema.org/>. response: a schema:RsvpAction.
copy success
The opponent's app does a PATCH to the opponent's POD to store the relevant game data. This is mostly data to know later that the opponent is participating in the game. Details about the game are not stored on the opponent's POD, because they are stored on the player's POD and are retrievable by doing a GET to the link of the game.
The opponent's app does a GET to the Web ID of the player. This Web ID is available through the invitation.
The player's POD returns RDF describing the player. An example of such RDF is
@prefix : <https://player.solid.community/profile/card#>. @prefix inbox: <https://player.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix opponent: <https://opponent.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows opponent:me; foaf:name "Player".
copy success
The app queries the RDF to determine the player's inbox via the predicate ldp:inbox
.
The app does a POST to the player's inbox with notification of the response.
The player's app does a GET to the notification. For clarity we skip the iteration over the different notifications, because this is the same as earlier.
The player's POD returns RDF describing the notification. An example of such RDF is
@prefix response: <https://opponent.solid.community/public/chess.ttl#response>. @prefix schema: <http://schema.org/>. response: a schema:RsvpAction.
copy success
The player's app queries the RDF to determine if the notification contains a response,
via the class schema:RsvpAction
.
If the notification contains a response, the app does a GET to the link of the response.
The opponent's POD returns RDF describing the response. An example of such RDF is
@prefix response: <https://opponent.solid.community/public/chess.ttl#response>. @prefix invitation: <https://player.solid.community/public/chess.ttl#invitation>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. response: a schema:RsvpAction; schema:rsvpResponse schema:RsvpResponseYes; schema:agent opponent:me; schema:recipient player:me. invitation: schema:result response:.
copy success
The player's app queries the response to determine the corresponding invitation, from which the corresponding game can be determined. An example of a corresponding SPARQL query is
PREFIX schema: <http://schema.org/> SELECT ?invitation WHERE { response: schema:result ?invitation. }
copy success
The player's app shows the response to the player.
With this action, the player does a new move, which is shown to the opponent. In Figure 6, we describe the corresponding steps between the player, the player's app, the player's POD, the opponent's app, and the opponent's POD. The steps are:
The player does a move.
The player's app generates RDF that describes this move. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix schema: <http://schema.org/>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. @prefix opponent: <https://opponent.solid.community/public/chess.ttl#>. :game chess:hasHalfMove :move2. :move2 a chess:HalfMove; schema:subEvent :game; chess:hasSANRecord "e3"^^xsd:string; chess:resultingPosition "rnbqkbnr/pppp1ppp/4p3/8/8/4P3/PPPP1PPP/RNBQKBNR w KQkq -". opponent:move1 chess:nextHalfMove :move2.
copy success
where
https://player.solid.community/public/chess.ttl#move2
copy success
is the link of the new move and
https://opponent.solid.community/public/chess.ttl#move1
copy success
is the link of the previous move.
The player's app does a PATCH to the player's POD to store the move.
The player's app generates a notification for the move. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix opponent: <https://opponent.solid.community/public/chess.ttl#>. opponent:move1 chess:nextHalfMove :move2.
copy success
The player's app does a POST to the opponent's POD with this notification.
The opponent's app does a GET to the link of the notification.
The opponent's POD returns RDF describing the move. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix opponent: <https://opponent.solid.community/public/chess.ttl#>. opponent:move1 chess:nextHalfMove :move2.
copy success
The opponent's app queries the RDF to determine if the notification contains a move.
The opponent's app does a GET to the link of the move.
The player's POD returns RDF describing the move. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix schema: <http://schema.org/>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. :move2 a chess:HalfMove; schema:subEvent :game; chess:hasSANRecord "e3"^^xsd:string; chess:resultingPosition "rnbqkbnr/pppp1ppp/4p3/8/8/4P3/PPPP1PPP/RNBQKBNR w KQkq -".
copy success
The opponent's app queries the RDF to determine the details of the move. A corresponding SPARQL query is
PREFIX : <https://player.solid.community/public/chess.ttl#> PREFIX schema: <http://schema.org/> PREFIX chess: <http://purl.org/NET/rdfchess/ontology/> SELECT ?game ?san WHERE { :move2 schema:subEvent ?game; chess:hasSANRecord ?san. }
copy success
The opponent's app shows the new move to the opponent.
The opponent's app stores the link of the new move on the opponent's POD. This is done to reconstruct the chess game based on the sequence of individual moves.
With this action, the player continues a game that he started before. Chess does not have to be played real time, i.e., it is possible that a player does a move in the morning, but that the opponent does the next move only in the evening. Therefore, players should be able to continue a game at any point in time. In Figure 7, we describe the corresponding steps between the player, the player's app, the player's POD, and the opponent's POD. The steps are:
The player's app does a GET to the player's Web ID.
The player's POD returns RDF describing the player. An example of such RDF is
@prefix : <https://player.solid.community/profile/card#>. @prefix inbox: <https://player.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix c0: <https://opponent.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows c0:me; foaf:name "Player".
copy success
The player's app queries the RDF to determine the chess games in which the player participates. The app iterates over all games.
The player's app does a GET to the link of each game.
The player's POD returns RDF describing the game. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. @prefix stor: <http://example.org/storage/>. @prefix schema: <http://schema.org/>. @prefix player: <https://player.solid.community/profile/card#>. @prefix opponent: <https://opponent.solid.community/profile/card#>. :game a chess:ChessGame; stor:storeIn :; chess:providesAgentRole :jo8deywv, :jo8deyww; chess:starts :jo8deywv; schema:name "Test game". :jo8deywv a chess:WhitePlayerRole; chess:performedBy player:me. :jo8deyww a chess:BlackPlayerRole; chess:performedBy opponent:me.
copy success
The player's app queries the RDF to determine the name of the game and the opponent's Web ID. An example of a corresponding SPARQL query is
PREFIX : <https://player.solid.community/public/chess.ttl#> PREFIX chess: <http://purl.org/NET/rdfchess/ontology/> PREFIX schema: <http://schema.org> PREFIX player: <https://player.solid.community/profile/card#> SELECT ?name ?opponent WHERE { :game schema:name ?name; chess:providesAgentRole ?role. ?role chess:performedBy ?opponent. MINUS {?role chess:performedBy player:me} }
copy success
The player's app does a GET to the opponent's Web ID.
The opponent's POD returns RDF describing the opponent. An example of such RDF is
@prefix : <https://opponent.solid.community/profile/card#>. @prefix inbox: <https://opponent.solid.community/inbox/>. @prefix ldp: <http://www.w3.org/ns/ldp#>. @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix player: <https://player.solid.community/profile/card#>. :me ldp:inbox inbox:; foaf:knows player:me; foaf:name "Opponent".
copy success
The player's app queries the RDF to determine the name of the player via the predicate foaf:name
.
An example of SPARQL query using this predicate
for an friend with the Web ID
https://opponent.solid.community/profile/card#me
copy success
is
PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX opponent: <https://opponent.solid.community/profile/card#> SELECT ?name WHERE { opponent:me foaf:name ?name. }
copy success
The player selects the game he wants to continue.
The player's app iterates over all moves of the selected game.
If the move is from the player then a GET to the link of the move by the player's app goes to the player's POD.
The player's POD returns RDF describing the move. An example of such RDF is
@prefix : <https://player.solid.community/public/chess.ttl#>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. @prefix schema: <http://schema.org>. :move2 a chess:HalfMove; schema:subEvent :game; chess:hasSANRecord "e3"^^xsd:string; chess:resultingPosition "rnbqkbnr/pppp1ppp/4p3/8/8/4P3/PPPP1PPP/RNBQKBNR w KQkq -".
copy success
If the move is from the opponent then a GET to the link of the move by the player's app goes to the opponent's POD.
The opponent's POD returns RDF describing the move. An example of such RDF is
@prefix : <https://opponent.solid.community/public/chess.ttl#>. @prefix player: <https://opponent.solid.community/public/chess.ttl#>. @prefix chess: <http://purl.org/NET/rdfchess/ontology/>. @prefix schema: <http://schema.org>. :move1 a chess:HalfMove; schema:subEvent :game; chess:hasSANRecord "e6"^^xsd:string; chess:resultingPosition "rnbqkbnr/pppp1ppp/4p3/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -"; chess:nextHalfMove player:move2.
copy success
The player's app adds the move to the instance of the game.
The player's app shows the game to the player.
The data generated by this app is not tied to this specific app, because the data is materialized as RDF and follows the Linked Data principles. Furthermore, it is stored in the PODs of the players, which the players themselves control. As a result, the data can be used by other apps, completely independent of the app described in this blog post. Examples of such other apps are:
If you have any questions or remarks, don’t hesitate to contact me via email or via Twitter.