extends Node signal session_started signal progress_changed signal session_completed(summary: Dictionary) signal session_failed(message: String) const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_URL := "https://us3q.ranaze.com/api/study-sessions" var api_url := DEFAULT_URL var request: HTTPRequest var active_session_id := "" var progress: Dictionary = {} var summary: Dictionary = {} var pending_action := "" var pending_launch: Dictionary = {} func _ready() -> void: api_url = EndpointConfig.resolve(DEFAULT_URL, "/api/study-sessions") request = HTTPRequest.new() request.timeout = 25.0 request.request_completed.connect(_on_completed) add_child(request) AuthState.auth_changed.connect(_on_auth_changed) func start(goal_type: String, target_value: int, mode: String, step3_system: String = "", launch: Dictionary = {}) -> void: if not AuthState.can_send_authenticated_request(): session_failed.emit("Login is required to start a study session.") return pending_launch = launch.duplicate(true) summary = {} _send("create", HTTPClient.METHOD_POST, "", { "clientSessionId": _new_uuid(), "goalType": goal_type, "targetValue": target_value, "mode": mode, "step3System": step3_system }) func finish_early() -> void: if active_session_id.is_empty(): return _send("finish", HTTPClient.METHOD_POST, "/%s/finish" % active_session_id.uri_encode()) func check_completion() -> void: if active_session_id.is_empty(): return _send("check", HTTPClient.METHOD_POST, "/%s/check" % active_session_id.uri_encode()) func apply_response(parsed: Dictionary, defer_completion: bool = false) -> void: var next_progress: Variant = parsed.get("studySessionProgress", null) if typeof(next_progress) == TYPE_DICTIONARY: progress = next_progress active_session_id = str(progress.get("id", active_session_id)) progress_changed.emit() var next_summary: Variant = parsed.get("sessionSummary", null) if typeof(next_summary) == TYPE_DICTIONARY and not next_summary.is_empty(): summary = next_summary active_session_id = "" if not defer_completion: session_completed.emit(summary) func emit_deferred_summary() -> void: if not summary.is_empty(): session_completed.emit(summary) func is_active() -> bool: return not active_session_id.is_empty() func seconds_remaining() -> int: return int(progress.get("secondsRemaining", 0)) func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void: if request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return pending_action = action var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token]) var body := "" if not payload.is_empty(): headers.append("Content-Type: application/json") body = JSON.stringify(payload) var error := request.request(api_url + suffix, headers, method, body) if error != OK: pending_action = "" session_failed.emit("The study session could not be started.") func _on_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var action := pending_action pending_action = "" if response_code == 401: AuthState.handle_unauthorized(AuthState.access_token) return if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: session_failed.emit("The study session request failed.") return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: session_failed.emit("The study session response was invalid.") return var next_progress: Dictionary = parsed.get("progress", {}) progress = next_progress active_session_id = str(progress.get("id", "")) if str(progress.get("state", "")) == "active" else "" var next_summary: Variant = parsed.get("summary", null) if typeof(next_summary) == TYPE_DICTIONARY: summary = next_summary progress_changed.emit() if action == "create": session_started.emit() elif not summary.is_empty(): session_completed.emit(summary) func _on_auth_changed() -> void: if not AuthState.is_logged_in: active_session_id = "" progress = {} summary = {} func _new_uuid() -> String: var bytes := Crypto.new().generate_random_bytes(16) bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 var value := bytes.hex_encode() return "%s-%s-%s-%s-%s" % [value.substr(0, 8), value.substr(8, 4), value.substr(12, 4), value.substr(16, 4), value.substr(20, 12)]