diff --git a/.api-template/core/ApiConfig.handlebars b/.api-template/core/ApiConfig.handlebars new file mode 100644 index 0000000..483f5d1 --- /dev/null +++ b/.api-template/core/ApiConfig.handlebars @@ -0,0 +1,169 @@ +extends {{>partials/api_config_parent_class}} +class_name {{>partials/api_config_class_name}} + +{{>partials/disclaimer_autogenerated}} + +# Configuration options for Api endpoints +# ======================================= +# +# Helps share configuration customizations across Apis: +# - host, port & scheme +# - extra headers (low priority, high priority) +# - transport layer security options (TLS certificates) +# - log level +# +# You probably want to make an instance of this class with your own values, +# and feed it to each Api's constructor, before calling the Api's methods. +# +# Since it is a Resource, you may use `ResourceSaver.save()` and `preload()` +# to save it and load it from file, for convenience. +# + + +# These are constant, immutable default values. Best not edit them. +# To set different values at runtime, use the @export'ed properties below. +const BEE_DEFAULT_HOST := "{{#if host}}{{{host}}}{{else}}localhost{{/if}}" +const BEE_DEFAULT_PORT_HTTP := 80 +const BEE_DEFAULT_PORT_HTTPS := 443 +const BEE_DEFAULT_POLLING_INTERVAL_MS := 333 # milliseconds + + +# Configuration also handles logging because it's convenient. +enum LogLevel { + SILENT, + ERROR, + WARNING, + INFO, + DEBUG, +} + + +## Log level to configure verbosity. +@export var log_level := LogLevel.WARNING + +{{!-- +# Not sure if this should hold the HTTPClient instance or not. Not for now. +# Godot recommends using a single client for all requests, so it perhaps should. + +# Godot's HTTP Client we are using. +# If none was set (by you), we'll lazily make one. +var bee_client: HTTPClient: + set(value): + bee_client = value + get: + if not bee_client: + bee_client = HTTPClient.new() + return bee_client +--}} + +## The host to connect to, with or without the protocol scheme. +## We toggle TLS accordingly to the provided scheme, if any. +@export var host := BEE_DEFAULT_HOST: + set(value): + if value.begins_with("https://"): + tls_enabled = true + value = value.substr(8) # "https://".length() == 8 + elif value.begins_with("http://"): + tls_enabled = false + value = value.substr(7) # "http://".length() == 7 + host = value + + +## Port through which the connection will be established. +## NOTE: changing the host may change the port as well if the scheme was provided, see above. +@export var port := BEE_DEFAULT_PORT_HTTP + + +## Headers used as base for all requests made by Api instances using this config. +## Those are the lowest priority headers, and are merged with custom headers provided in the bee_request() method call +## as well as the headers override below, to compute the final, actually sent headers. +@export var headers_base := { + # Stigmergy: everyone does what is left to do (like ants do) + "User-Agent": "Stigmergiac/1.0 (Godot)", + # For my mental health's sake, only JSON is supported for now + "Accept": "application/json", + "Content-Type": "application/json", +} + + +## High-priority headers, they will always override other headers coming from the base above or the method call. +@export var headers_override := {} + + +## Duration of sleep between poll() calls. +@export var polling_interval_ms := BEE_DEFAULT_POLLING_INTERVAL_MS # milliseconds + + +## Enable the Transport Security Layer (packet encryption, HTTPS) +@export var tls_enabled := false: + set(value): + tls_enabled = value + # port = BEE_DEFAULT_PORT_HTTPS if tls_enabled else BEE_DEFAULT_PORT_HTTP + + +## You should preload your *.crt file (the whole chain) in here if you want TLS. +## I usually concatenate my /etc/ssl/certs/ca-certificates.crt and webserver certs here. +## Remember to add the *.crt file to the exports, if necessary. +@export var trusted_chain: X509Certificate # only used if tls is enabled +@export var common_name_override := "" # for TLSOptions + + +## Dynamic accessor using the TLS properties above, but you may inject your own +## for example if you need to use TLSOptions.client_unsafe. Best not @export this. +var tls_options: TLSOptions: + set(value): + tls_options = value + get: + if not tls_enabled: + return null + if not tls_options: + tls_options = TLSOptions.client(trusted_chain, common_name_override) + return tls_options + + +func log_error(message: String): + if self.log_level >= LogLevel.ERROR: + push_error(message) + +func log_warning(message: String): + if self.log_level >= LogLevel.WARNING: + push_warning(message) + +func log_info(message: String): + if self.log_level >= LogLevel.INFO: + print(message) + +func log_debug(message: String): + if self.log_level >= LogLevel.DEBUG: + print(message) + + +{{#each authMethods}} +# Authentication method `{{name}}`. +{{#if isBasicBearer }} +# Basic Bearer Authentication `{{bearerFormat}}` +func set_security_{{name}}(value: String): + self.headers_base["Authorization"] = "Bearer %s" % value + + +{{else if isApiKey }} +# Api Key Authentication `{{keyParamName}}` +func set_security_{{name}}(value: String): + {{#if isKeyInHeader }} + self.headers_base["{{keyParamName}}"] = value + {{else if isKeyInQuery }} + # Implementing this should be straightforward + log_error("Api Key in Query is not supported at the moment. (contribs welcome)") + {{else if isKeyInCookie }} + log_error("Api Key in Cookie is not supported at the moment. (contribs welcome)") + {{else }} + log_error("Unrecognized Api Key format (contribs welcome).") + {{/if}} + + +{{else}} +# → Skipped: not implemented in the gdscript templates. (contribs are welcome) + + +{{/if}} +{{/each}} diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 57917e1..89ca1db 100755 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -7,8 +7,25 @@ on: - 'v*' jobs: + generate: + runs-on: stable + container: + image: git.euph.dev/actions/runner-java-21:1300 + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - name: "Generate" + run: just generate + - name: Upload API Client as Artifact + uses: "https://git.euph.dev/actions/upload-artifact@v3" + with: + name: api-client + path: scripts/api-client/* + build: runs-on: stable + needs: + - generate container: image: git.euph.dev/actions/runner-redot-4.3:latest strategy: @@ -35,6 +52,11 @@ jobs: steps: - name: "Checkout" uses: "https://git.euph.dev/actions/checkout@v3" + - name: Download artifact from previous job + uses: "https://git.euph.dev/actions/download-artifact@v3" + with: + name: api-client + path: scripts/api-client - name: Build Binary run: | if [ "${{ matrix.build-dir }}" != "" ]; then diff --git a/.gitignore b/.gitignore index e2dce33..042fdcd 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # Redot 4+ specific ignores .godot/ /android/ +openapitools.json +scripts/api-client/ diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..2ccbb91 --- /dev/null +++ b/Justfile @@ -0,0 +1,20 @@ +_choose: + just --choose + +lint: + -gdlint . + -gdformat -d . + +api_version := "v0.0.0-rc.4" +api_location := "scripts/api-client" +generate: + #!/bin/sh + API_URL="https://git.euph.dev/TowerDefence/Server/releases/download/{{ api_version }}/api.yml" + rm -rf {{ api_location }} + mkdir -p {{ api_location }} + npx --yes @openapitools/openapi-generator-cli \ + generate \ + -g gdscript \ + -i "$API_URL" \ + -o {{ api_location }} \ + -t .api-template diff --git a/config/api_config.tres b/config/api_config.tres new file mode 100644 index 0000000..318c29c --- /dev/null +++ b/config/api_config.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="ApiConfig" load_steps=2 format=3 uid="uid://cdixdbu3sqgjn"] + +[ext_resource type="Script" path="res://scripts/api-client/core/ApiConfig.gd" id="1_0p6qs"] + +[resource] +script = ExtResource("1_0p6qs") +log_level = 2 +host = "localhost" +port = 8080 +headers_base = { +"Accept": "application/json", +"Content-Type": "application/json", +"User-Agent": "Stigmergiac/1.0 (Godot)" +} +headers_override = {} +polling_interval_ms = 333 +tls_enabled = false +common_name_override = "" diff --git a/gdformatrc b/gdformatrc index 235fd70..cadddef 100644 --- a/gdformatrc +++ b/gdformatrc @@ -1,4 +1,5 @@ excluded_directories: !!set .git: null addons: null + api-client: null diff --git a/gdlintrc b/gdlintrc index a140f15..2dfe0ce 100644 --- a/gdlintrc +++ b/gdlintrc @@ -25,6 +25,7 @@ enum-name: ([A-Z][a-z0-9]*)+ excluded_directories: !!set .git: null addons: null + api-client: null expression-not-assigned: null function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* function-arguments-number: 10 diff --git a/scenes/login.tscn b/scenes/login.tscn index 35c7d0d..88fe867 100644 --- a/scenes/login.tscn +++ b/scenes/login.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=3 format=3 uid="uid://dtaaw31x3n22f"] +[gd_scene load_steps=4 format=3 uid="uid://dtaaw31x3n22f"] -[ext_resource type="Script" path="res://scripts/login/login.gd" id="1_12w35"] +[ext_resource type="Script" path="res://scripts/ui/login.gd" id="1_12w35"] +[ext_resource type="Resource" uid="uid://cdixdbu3sqgjn" path="res://config/api_config.tres" id="2_60hb8"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_d0bbp"] @@ -74,12 +75,12 @@ scroll_active = false layout_mode = 2 secret = true -[node name="Button" type="Button" parent="Panel/VBoxContainer" node_paths=PackedStringArray("username_field", "password_field", "http_request")] +[node name="Button" type="Button" parent="Panel/VBoxContainer" node_paths=PackedStringArray("username_field", "password_field")] layout_mode = 2 text = "Login" script = ExtResource("1_12w35") username_field = NodePath("../InputContainer/UsernameContainer/UsernameInput") password_field = NodePath("../InputContainer/PasswordContainer/PasswordInput") -http_request = NodePath("HTTPRequest") +api_config = ExtResource("2_60hb8") [node name="HTTPRequest" type="HTTPRequest" parent="Panel/VBoxContainer/Button"] diff --git a/scripts/login/login.gd b/scripts/login/login.gd deleted file mode 100644 index 8d053e1..0000000 --- a/scripts/login/login.gd +++ /dev/null @@ -1,39 +0,0 @@ -extends Button - -@export var username_field: LineEdit -@export var password_field: LineEdit -@export var http_request: HTTPRequest -var url: String = "http://localhost:8080/api/v1/player/login" -var headers = ["Content-Type: application/json"] - - -func _ready() -> void: - if not username_field: - push_error("No Username Field set") - return - if not password_field: - push_error("No Password Field set") - return - if not http_request: - push_error("No Http Request set") - return - connect("pressed", login) - - -func login() -> void: - var login_data = { - "username": username_field.text, - "password": password_field.text, - } - var json = JSON.stringify(login_data) - http_request.connect("request_completed", on_login_response) - http_request.request(url, headers, HTTPClient.METHOD_POST, json) - - -func on_login_response( - _result: int, - _response_code: int, - _headers: PackedStringArray, - body: PackedByteArray, -) -> void: - print(body.get_string_from_utf8()) diff --git a/scripts/ui/login.gd b/scripts/ui/login.gd new file mode 100644 index 0000000..81ab01b --- /dev/null +++ b/scripts/ui/login.gd @@ -0,0 +1,37 @@ +extends Button + +@export var username_field: LineEdit +@export var password_field: LineEdit +@export var api_config: ApiConfig +var api: ServerApi + + +func _ready() -> void: + if not username_field: + push_error("No Username Field set") + return + if not password_field: + push_error("No Password Field set") + return + if not api_config: + push_error("No API Configuration provided") + return + api = ServerApi.new(api_config) + connect("pressed", login) + username_field.connect("pressed", login) + password_field.connect("pressed", login) + + +func login() -> void: + var login_data = PlayerLoginData.new() + login_data.username = username_field.text + login_data.password = password_field.text + api.player_login(login_data, on_login_response, error) + + +func on_login_response(a: ApiResponse) -> void: + print(a.body) + + +func error(b: ApiError) -> void: + print(b.response_code)