This repository contains the Kotlin backend, which acts as a middle man between the dart recognition python server (https://github.com/DartCaller/darts-recognition) and the user-facing frontend (https://github.com/DartCaller/web).
- π¦Tech Stack
- π» Running Locally
- π Testing
- π Authentication
- π API Endpoints
- ποΈ DB Schema
- π License
- Language: Kotlin
- Framework: Ktor
- Database: PostgreSQL
- ORM Framework: Exposed
- Testing: JUnit5 & the Ktor testing tools
Kotlin runs on JVM. Hence it is necessary to use JDK 8 for your local Kotlin development.
After that, you can build the project with
# run tests and build
$ ./gradlew build
# build without tests
$ ./gradlew build -x test
# serve with hot reload at localhost:3000
$ DATABASE_URL={DB_URL} ./gradlew run
where DATABASE_URL should follow the following format:
DATABASE_URL=postgres://{user}:{password}@{hostname}:{port}/{database-name}
# run tests
$ ./gradlew test
Currently, this repository is equipped with the Ktor testing tools including JUnit5. To quote the official Ktor documentation on their testing tools
Ktor has a special kind engine TestEngine, that doesn't create a web server, doesn't bind to sockets and doesn't do any real HTTP requests. Instead, it hooks directly into internal mechanisms and processes ApplicationCall directly. This allows for fast test execution at the expense of maybe missing some HTTP processing details. It's perfectly capable of testing application logic, but be sure to set up integration tests as well.
You can find the tests in https://github.com/DartCaller/api/tree/main/test. The tests are part of the CI / CD pipeline and are run on each push to the repository, as can be seen here https://github.com/DartCaller/api/actions.
Most of the routes on this server require authentication, which I implemented with Auth0 and JWT. In the paragraph below about API Endpoint, every endpoint that does require authentication has a π emoji at the end of the headline. Every other endpoint can be called un-authenticated.
You can currently authenticate through the frontend https://github.com/DartCaller/web using username and password. Or you can also authenticate, machine to machine like I do within https://github.com/DartCaller/darts-recognition, but here you would have to know the client_secret.
The authentication of HTTP requests is then pretty straightforward. All that is required is the access_token
issued by the authentication client that I created on Auth0 and then to pass that token as a Bearer Token in an HTTP request header (link to a little HowTo).
And that's it. You can now access protected HTTP endpoints.
Since WebSockets don't have an inbuilt authentication mechanism, I created my own little rule set for that, and it works like this.
Everybody can open a WebSocket connection to my WebSocket Endpoint under /ws
. But every WS event that you'll send to the server will be interpreted as an authentication event until you successfully passed authentication. After that, you are free to send any below specified WS Event
This is the endpoint that is used by the dart recognition python backend https://github.com/DartCaller/darts-recognition to submit scores that were detected on the dartboard. Each dart recognition hardware setup has a so-called boardID
used to specify on which board this score has been thrown. If the frontend passes the same boardID
during game creation, this backend knows that any submitted scores using this boardID
will be added to the Dart Game with the same boardID
.
boardID
, but for the moment, I have hardcoded the same boardID=proto
(short for prototype) for every game. This is why this backend currently doesn't support multiple parallel running games since one submitted dart score using the boardID=proto
would be published to all active Dart Games.
All that is needed to call this route is the boardID
as a query parameter, and in the request body as plain text, the dart score in the below-specified format.
Method | Query Param | Example Request Body | Body Type |
---|---|---|---|
POST | boardID | D14 |
plaintext |
The DartScore Format that is used in all three DartCaller Applications is pretty simple:
One DartScore equals one thrown dart. You have a leading identifier that specifies if the dart hit a single (S
), double (D
), or triple field (T
)
After that, we have the field that has been hit.
So T20
would mean the triple 20 fields resulting in a thrown score of 60 while S25
means the single bull and D25
means bulls-eye (the two small rings right at the center of the dartboard scoring 25 and 50 points, respectively).
For a more detailed explanation with graphic and more examples, please follow this link https://github.com/DartCaller/MainReadMe#score
This route is used by the fronend to change a player's past score. Just specify the playerID and the new score he should have, and his last thrown score will be set to this.
Method | Query Param | Example Request Body | Body Type |
---|---|---|---|
POST | gameID | { playerID: "{playerID}", scoreString: "D14S20T10" } |
plaintext |
This is the domain where the WebSocket for the frontend is served. The below-mentioned endpoints are all WebSocket events that require a certain WebSocket event payload to be sent to the backend after the frontend has successfully created a WebSocket connection via this endpoint.
# Minimal event payload
{ "type": "{WsEventType}" }
All WebSocket event payloads need to be a JSON object with a type
property specifying the WebSocket event type which the frontend wishes to execute.
The rest of the payload that is required is different for every WS event and specified below.
{ "type": "Authenticate", "accessToken": "{access_token}" }
This WS event will be the first one you'll need to send. After you authenticate yourself with this event, you can send any other WS event specified below.
{ "type": "CreateGame", "players": ["Alice", "Bob", "Cedric"], "gameMode": "301" }
This WS event will create a new dart game with the specified players and the given game mode. Currently, the valid gameModes
are 301
& 501
, determining the leg starting number.
After successfully creating the new dart game, the WebSocket client will receive updates on the game's current state in the form of the network game state specified below.
{
"gameID": "123",
"legFinished": false,
"playerNames": {
"exampleUUID1": "Alice",
"exampleUUID2": "Bob",
},
"currentPlayer": "exampleUUID1",
"scores": {
"exampleUUID1": ["501", "T20D10S5"],
"exampleUUID2": ["501", "T20D10S5"]
},
"playerOrder": ["exampleUUID1", "exampleUUID2"],
"playerFinishedOrder": ["exampleUUID2"],
"currentRoundIndex": 1
}
The scores
key follows the Dart Score Format. While the very first element in each player's score list is just the starting number of the round, every element after that has between 1 and 3 dart scores directly together depending on how many darts the player has thrown in that round. After a player has completed his turn, the round scorestring string should contain three occurrences of the Dart Score Format. If you see a string with less than three occurrences, it means the player is still throwing his last darts.
{ "type": "JoinGame", "gameID": "{GameUUID}" }
When the specified gameID is found within the current active games, the client will be added as a subscriber to the game and will receive the newest network game state of the specified dart game from now on.
{ "type": "NextLeg", "gameID": "{GameUUID}" }
When the specified gameID is found within the current active games, and all players have finished the current game, the next game will be started. In the next game, the player who finished last in the last round will start, and the winner of the previous round will go last.
Distributed under the GNU GPLv3 License. See LICENSE for more information.