# Client facing game API Clients connecting to this service should open a websocket connection to the `/ws` path. After a WebSocket connection is established the client needs to establish a session before issuing any other commands. ``` $ websocat wss://game.gs.stg.matchday.com/ws ``` ## Structure All inbound and outbound have the following structure: ``` ├── Namespace │ └── Message │ └── field ``` For example, if we have a message `Message1` with two fields `field1` and `field1` in namespace `Namespace1`, its JSON representation would be: ```json { "Namespace1": { "Message1": { "field1": "value1", "field2": "value2" } } } ``` If a message has no fields, it is written as: ```json { "Namespace1": "Message1" } ``` or if it has only one value, then ```json { "Namespace1": { "Message1": "value" } } ``` All messages may be sent in batches, thus the protocol expects the following message format for incoming messages (even when sending only one message at a time): ```json { "messages": [ - message1, - message2, - messageN ] } ``` All messages received in a batch are going to be executed by the server. The order of their execution is not specified and most often executed in parallel. So if you have two messages where one depends on the other being executed first, then you should send them in two separate batches. At the moment the server returns an array of `messages` as well but does not batch responses and each of the input messages in the request that has a response will have a separate response message with an array of `messages` with exactly one element. This is most likely going to change in the future, but for client-side code it shouldn't matter. Just process all `messages` in all responses as if you were processing individually. ## Messages ``` ├── Control │ └── AuthenticateWithUsernameAndPassword │ └── game_id: u64 │ └── email: String │ └── password: String │ └── locale: Option<String> │ └── AuthenticateWithJWT │ └── game_id: u64 │ └── token: String │ └── locale: Option<String> │ └── AuthenticateAsGuest │ └── game_id: u64 │ └── locale: Option<String> │ └── AuthenticateWithContextData │ └── game_id: u64 │ └── context: String │ └── locale: Option<String> │ └── CompleteGuestSignup │ └── game_id: u64 │ └── email: u64 │ └── name: u64 ├── Player │ └── CurrentProfile │ └── GetProfile(id: u64) │ └── ListCards │ └── ListFriends │ └── GetClansMembership │ └── GetGlobalScores │ └── SetUserData │ └── key: String │ └── value: String │ └── GetUserData(key: String) │ └── AddFriend(id: u64) │ └── RemoveFriend(id: u64) │ └── FavoriteFriend(id: u64) │ └── UnfavoriteFriend(id: u64) │ └── BlockFriend(id: u64) │ └── UnblockFriend(id: u64) ├── Cards │ └── CardInfo(id: u64) ├── Clan │ └── Create │ └── name: String │ └── autoapprove: bool │ └── Join(id: u64) │ └── Leave(id: u64) │ └── GetClanInfo(id: u64) │ └── AddAdmin │ └── clan_id: u64 │ └── player_id: u64 │ └── RemoveAdmin │ └── clan_id: u64 │ └── player_id: u64 │ └── InviteMember │ └── clan_id: u64 │ └── player_id: u64 │ └── ApproveMember │ └── clan_id: u64 │ └── player_id: u64 │ └── RejectMember │ └── clan_id: u64 │ └── player_id: u64 │ └── TransferOwnership │ └── clan_id: u64 │ └── player_id: u64 │ └── SetAutoapprove │ └── clan_id: u64 │ └── autoapprove: bool │ └── GetClanLeaders(id: u64) │ └── IncrementScores │ └── clan_id: u64 │ └── scores: [u64 array] ├── Prediction │ └── SetPredictionForMatch │ └── tournament_id: u64 │ └── match_id: u64 │ └── teams: [String array] │ └── prediction: [u64 array] │ └── quiz_goal_score: u64 │ └── GetPredictionForMatch(id: u64) │ └── GetCommunityPredictionsForMatch: [u64 array] ├── Tournament │ └── GetTournament(id: u64) │ └── GetMatch: [u64 array] │ └── GetMatchDetails: [u64 array] ├── Admin │ └── CreateTournament │ └── tournament_id: u64 │ └── match_id: Option<MatchMessage> │ └── details: Option<MatchMessage> │ └── UpdateTournament │ └── tournament_id: u64 │ └── match_id: Option<MatchMessage> │ └── details: Option<MatchMessage> │ └── CreateMatch │ └── tournament_id: u64 │ └── match_id: Option<MatchMessage> │ └── details: Option<MatchMessage> │ └── UpdateMatch │ └── tournament_id: u64 │ └── match_id: Option<MatchMessage> │ └── details: Option<MatchMessage> │ └── DumpTournament(id: u64) ``` ## Establishing a session To establish a session, the player needs to authenticate and provide the Game ID that its connecting to. Currently, two authentication methods are supported: ### 1. Username and Password In this authentication method, the client sends user credentials in the following format: ```json { "messages": [ { "Control": { "AuthenticateWithPlainEmailAndPassword": { "game_id": 1, "email": "karim+test2@matchday.com", "password": "P@$$word9", "locale": "es" } } } ] } ``` ### 2. JWT Token After acquiring a token from Platform API ```json { "messages": [ { "Control": { "AuthenticateWithJWT": { "game_id": 1, "token": "eyJhbGciOi...pq4UID4SA" } } } ] } ``` Upon successfull authentication, the server will respond with: ```json { "messages": [ { "Control": { "Session": { "groups": [] } } } ] } ``` on failure, the session object is null: ```json { "messages": [ { "Control": "AuthFailed" } ] } ``` ## Query any user profile by id on an open session: ```json { "messages": [ { "Player": { "GetProfile": 2024 } } ] } ``` should return ```json { "messages": [ { "Player": { "Profile": { "id": 2024, "username": "test_NBjf6Yh3Xa" } } } ] } ``` ## Get a player's ID ```json { "messages": [ { "Player": "GetId" } ] } ``` Returns: ``` { "messages": [ { "Player": { "Id": 123 } } ] } ``` ## Player's global score ### Retreiving global score This returns the global score for the current game that has been accumulated since the profile creation: ```json { "messages": [ { "Player": "GetGlobalScores" } ] } ``` Should return ```json { "messages": [ { "Player": { "GlobalScores": [ 3, 6, 9 ] } } ] } ``` ## Incrementing player's score Will increment the global scores and all scores in clans that the player is a member of ```json { "messages": [ { "Player": { "IncrementScores": [1, 2, 3] } } ] } ``` Where each row is `[player_id, [score_component_1, score_component_1, ..., score_component_N]]` ## Increment Named scores In most cases, we want to increment a named score. The name refers to the `leaderboard_kind`, `"RFQ"`, refers to Rapid Fire Quiz `"PQ"` ```json { "messages": [ { "Player": { "IncrementNamedScores": { "scores": [3, 1, 1], "leaderboard_kind": "RFQ" } } } ] } ``` ## Game-specific User Data Store new entry: ```json { "messages": [ { "Player": { "SetUserData": { "key": "test1", "value": "value1" } } } ] } ``` Get existing entry: ```json { "messages": [ { "Player": { "GetUserData": "test1" } } ] } ``` should return ```json { "messages": [ { "Player": { "UserData": { "key": "test1", "value": "value1" } } } ] } ``` If the key does not exist, returned value will be `null`. to delete an entry: ```json { "messages": [ { "Player": { "SetUserData": { "key": "test1", "value": null } } } ] } ``` ## Getting player's list of cards After establishing a sessions, use the following message to get a list of all cards owned by the logged in player: ```json { "messages": [ { "Player": "ListCards" } ] } ``` You should get a response like: ```json { { "messages": [ { "Player": { "CardsList": [ { "id": 1010, "card": { "id": 3590750, "name": "Ola Toivonen", "description": "Ola Toivonen | Season 2022/23 | Common", "edition": "Season 2022/23", "rarity": "common", "total_issued": 0, "season": "2022/23", "version": 1, "metadata_version": 1, "price": "0", "sale_status": "unsold", "is_active": true, "created_on": "2023-02-20T19:42:04.559Z", "source": { "baseUrl": "https://matchday-nfts-assets.matchday.com", "hash": "4f20d89f732623e7a5e7453a71ca963f35fc84f63c522bdd8b34d496b84823f0" }, "serial_number": "0/0", "player": { "position": "forward", "first_name": "Ola", "last_name": "Toivonen", "birth_date": null, "country": "Sweden", "league_country": "Sweden", "tier": { "attack": "S", "defense": "C" } } } }, ...more cards ] } } ] } ``` ## Cards Query any card by ID: ```json { "messages": [ { "Card": { "CardInfo": 1863014 } } ] } ``` response ```json { "messages": [ { "Card": { "CardInfo": { "id": 1863014, "name": "Gonzalo Montiel", "description": "Gonzalo Montiel | Season 2022/23 | Limited", "edition": "Season 2022/23", "rarity": "limited", "total_issued": 10000, "season": "2022/23", "version": 1, "metadata_version": 1, "price": "0", "sale_status": "unsold", "is_active": true, "created_on": "2023-02-11T00:24:00.847Z", "source": { "baseUrl": "https://matchday-nfts-assets.matchday.com", "hash": "cb1d495d749c077689990ac46593da736b67e72f969b771d249092f176f44f18" }, "serial_number": "7022/10000", "player": { "position": "defender", "first_name": "Gonzalo", "last_name": "Montiel", "birth_date": null, "country": "Argentina", "league_country": "Spain", "tier": { "attack": "B", "defense": "S+" } } } } } ] } ``` ## Friends Send those messages on an open session. Each message will return the current state of player friends after applying the requested operation, it should look like this: ```json { "messages": [ { "Player": { "Friends": { "friends": { "4": "username1", "10": "username2" }, "favorites": [], "blocks": [ 3 ] } } } ] } ``` ### Available messages ```json { "messages": [ { "Player": "ListFriends" } ] } ``` ```json { "messages": [ { "Player": { "AddFriend": 11 } } ] } ``` ```json { "messages": [ { "Player": { "RemoveFriend": 11 } } ] } ``` ```json { "messages": [ { "Player": { "FavoriteFriend": 3 } } ] } ``` ```json { "messages": [ { "Player": { "UnfavoriteFriend": 3 } } ] } ``` ```json { "messages": [ { "Player": { "BlockFriend": 3 } } ] } ``` ```json { "messages": [ { "Player": { "UnblockFriend": 3 } } ] } ``` ## Clans On an open session: ### Get all player clans Retreived all clans that the current player is a member of: ```json { "messages": [ { "Player": "GetClansMembership" } ] } ``` will return something like: ```json { "messages": [ { "Player": { "ClansMembership": { "joined": [ 10347234725074961581, 16909040649212794278 ], "pending": [ 3829305525187636792 ], "invites": [ [719930552518763661, 99272772] ] } } } ] } ``` `joined` and `pending` are lists of `clan_id`s. `invites` is a tuple of `[clan_id, inviter_player_id]`. ### Query any clan ```json { "messages": [ { "Clan": { "GetClanInfo": 16909040649212794278 } } ] } ``` returns something like ```json { "messages": [ { "Clan": { "ClanInfo": { "clan_id": 16909040649212794278, "name": "ClanName", "autoapprove": true, "owner": 7, "admins": [8, 9], "members": [10, 11], "pending": [12, 13], "invited": [14] } } } ] } ``` or ```json { "messages": [ { "Clan": { "Error": "Object with id 16909040649212794278 not found" } } ] } ``` ### Creating clans this request ```json { "messages": [ { "Clan": { "Create": { "name": "ClanName", "autoapprove": true } } } ] } ``` will return something like ```json { "messages": [ { "Clan": { "Created": { "id": 16909040649212794278, "name": "ClanName", "owner": 7 } } } ] } ``` ### Clan membership Those commands will add or leave the currently authenticated player to a clan. If the clan has `autoapprove` set to false, then te current player will be added to the pending plyers list on that clan until an admin or an owner approves their membership. ```json { "messages": [ { "Clan": { "Join": 16909040649212794278 } } ] } ``` ```json { "messages": [ { "Clan": { "Lave": 16909040649212794278 } } ] } ``` ### Clan Administration (must be performed by the clan owner): Add/remove admins: ```json { "messages": [ { "Clan": { "AddAdmin": { "clan_id": 16909040649212794278, "player_id": 71208937091238 } } } ] } ``` ```json { "messages": [ { "Clan": { "RemoveAdmin": { "clan_id": 16909040649212794278, "player_id": 71208937091238 } } } ] } ``` When a an admin is removed, they remain members of the clan, but without admin privilages. #### Enable or disable autoapproval. When enabled, any user joining a clan must be approved by an admin or the owner, otherwise any player is allowed to join the clan. ```json { "messages": [ { "Clan": { "SetAutoApprove": { "clan_id": 16909040649212794278, "autoapprove": true } } } ] } ``` Transfer ownership from the current owner to another player: ```json { "messages": [ { "Clan": { "TransferOwnership": { "clan_id": 16909040649212794278, "player_id": 8827 } } } ] } ``` ### Clan administration (can be performed by the owner or any admin): Approve a member that is pending membership. Joined a clan that has `autoapprove` set to `false`: ```json { "messages": [ { "Clan": { "ApproveMember": { "clan_id": 16909040649212794278, "player_id": 8827 } } } ] } ``` Reject a member pending membership or remove an existing member from a clan: ```json { "messages": [ { "Clan": { "RejectMember": { "clan_id": 16909040649212794278, "player_id": 8827 } } } ] } ``` ### Clan Scores and Leaderboards Getting a sorted list of clan players and their scores: ```json { "messages": [ { "Clan": { "GetClanLeaders": 9766582022667716893 } } ] } ``` Will return something like: ```json { "messages": [ { "Clan": { "ClanLeaders": { "clan_id": 9766582022667716893, "columns": [ "correct_answers", "wrong_answers", "time" ], "rows": [ [123, [10, 3, 100]] ] } } } ] } ``` ### Member invitation flow 1. Clan owner invokes `Clan::InviteMember` for the target member. 2. Target member then sees that invite when invoking `Player::GetClansMembership` in the `invites` field. Invites are tuples of `(clan_id, inviter_player_id)`. 3. Target member then either `Clan::Join` on that clan to accept the invite and join the clan or `Clan::Leave` on that invite to reject it. 4. Clan membership limits still apply. Regardless of invitations a player cannot be a member of more than 5 clans. ## Deferred Signup / Onboarding flow: 1. If there is no JWT token in browser's storage then invoke: ```json { "messages": [ { "Control": { "AuthenticateAsGuest": { "game_id": 3 } } } ] } ``` Then when you get user's name and email invoke: ```json { "messages": [ { "Control": { "CompleteGuestSignup": { "game_id": 3, "email": "karim@matchday.com", "name": "bobobobob" } } } ] } ``` That will end your session and disconnect the websocket. It will send an email with the context token. Once you get the context token invoke: ```json { "messages": [ { "Control": { "AuthenticateWithContextData": { "game_id": 3, "context": "sBuW8djaV9Loqu...wfaflk" } } } ] } ``` it will start a new session and also additionally give you a JWT token that you can reuse in future sessions: ```jsons { "messages": [ { "Control": { "Session": { "groups": [], "jwt": { "access": "eyJhbGciOiJFUz..._ZPmaujxCtcuJocDBMYD7COUGEUkvAiw", "refresh": "eyJhbGciOiJFU...OXdaRmHv6_mbGbckGUK05jXrFg" } } } } ] } ``` ## Updating the Tournament Service as Admin In order to test out the Tournament service messages must be sent from the Admin service. - CreateTournament ```json { "messages": [ { "Admin": { "CreateTournament": { "id": 42, "name": "Andreas - Test" } } } ] } ``` - CreateMatch ```json { "messages": [ { "Admin": { "CreateMatch": { "tournament_id": 42, "match_id": 111, "details": { "date": "06/27/23", "time": "08:54:31", "status": "Scheduled", "teams": [ "MatchDayKingdom", "WorldsEnd" ], "current_score": [ 0, 0 ], "has_result": false } } } } ] } ``` - UpdateMatch Here, we update the `status` field to "Finised". The `status` field must use a variant of the enum ```rust pub enum MatchStatus { #[default] Scheduled, InProgress, Finished, Deleted, } ``` ```json { "messages": [ { "Admin": { "UpdateMatch": { "tournament_id": 42, "match_id": 111, "details": { "date": "06/27/23", "time": "08:54:31", "status": "Finished", "teams": [ "MatchDayKingdom", "WorldsEnd" ], "current_score": [ 0, 0 ], "has_result": false } } } } ] } ``` - GetMatchDetails ``` { "messages": [ { "Tournament": { "GetMatchDetails": { "tournament_id": 42, "match_id": 111 } } } ] } ``` Response: ```json { "messages": [ { "Tournament": { "Match": { "tournament_id": 42, "match_id": 111, "details": { "status": "Finished", "date": "06/27/23", "time": "08:54:31", "teams": [ "MatchDayKingdom", "WorldsEnd" ], "current_score": [ 0, 0 ], "has_result": false, "final_score": null } } } } ] } ``` ## Predictions - SetPredictionForMatch ```json { "messages": [ { "Prediction": { "SetPredictionForMatch": { "correlated_ids": { "tournament_id": 42, "match_id": 111 }, "prediction": [ 6, 8 ], "quiz_goal_score": 68 } } } ] } ``` - GetPredictionForMatch ```json { "messages": [ { "Prediction": { "GetPredictionForMatch": { "tournament_id": 42, "match_id": 111 } } } ] } ``` Response: ```json { "messages": [ { "Prediction": { "Prediction": { "id": 12916372887883332635, "tournament_id": 100, "match_id": 42, "player_id": 123, "teams": null, "prediction": [ 1, 6 ] } } } ] } ``` - GetCommunityPredictionsForMatch ```json { "messages": [ { "Prediction": { "GetCommunityPredictionsForMatch": { "tournament_id": 42, "match_id": 115 } } } ] } ``` Response: ```json { "messages": [ { "Prediction": { "CommunityPredictions": { "tournament_id": 42, "match_id": 115, "predictions": [ 0, 0, 0 ] } } } ] } ``` The `predictions` array in the response represents total wins, draws and losses voted by the community. ## Tournament - GetTournament ```json { "messages": [ { "Tournament": { "GetTournament": 1 } } ] } ``` Response ``` { "messages": [ { "Tournament": { "MatchIds": [ 1, 2 ] } } ] } ``` - Dump Tournament ```json { "messages": [ { "Admin": { "DumpTournament": 42 } } ] } ``` - GetMatchDetails ```json { "messages": [ { "Tournament": { "GetMatchDetails": { "tournament_id": 42, "match_id": 114 } } } ] } ``` Response: ```json { "messages": [ { "Tournament": { "Match": { "tournament_id": 42, "match_id": 114, "details": { "status": "Scheduled", "date": "21/07/23", "time": "08:54:31", "teams": [ "MatchDayKingdom", "WorldsEnd" ], "current_score": [ 0, 0 ], "has_result": false, "final_score": null } } } } ] } ```