extends Node ## HTTPClient Service ## ## Singleton service for all HTTP communication with the Flask backend. ## Handles authentication, JSON parsing, error handling, and provides ## a convenient interface for making API requests. ## ## Usage: ## HTTPClient.http_get("/api/v1/characters", _on_characters_loaded) ## HTTPClient.http_post("/api/v1/auth/login", {"email": "...", "password": "..."}, _on_login_complete) # API Configuration const API_TIMEOUT := 30.0 # Seconds # HTTP Methods enum Method { GET, POST, PUT, DELETE, PATCH } # Internal state var _auth_token: String = "" var _session_cookie: String = "" # Session cookie for authentication #var _request_queue: Array[Dictionary] = [] var _active_requests: Dictionary = {} # request_id -> HTTPRequest node func _ready() -> void: print("[HTTPClient] Service initialized. API Base URL: %s" % Settings.get_api_url()) ## Make a GET request ## ## @param endpoint: API endpoint (e.g., "/api/v1/characters") ## @param callback: Function to call with APIResponse when complete ## @param error_callback: Optional function to call on error func http_get(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void: _make_request(Method.GET, endpoint, {}, callback, error_callback) ## Make a POST request ## ## @param endpoint: API endpoint ## @param data: Dictionary to send as JSON body ## @param callback: Function to call with APIResponse when complete ## @param error_callback: Optional function to call on error func http_post(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void: _make_request(Method.POST, endpoint, data, callback, error_callback) ## Make a PUT request ## ## @param endpoint: API endpoint ## @param data: Dictionary to send as JSON body ## @param callback: Function to call with APIResponse when complete ## @param error_callback: Optional function to call on error func http_put(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void: _make_request(Method.PUT, endpoint, data, callback, error_callback) ## Make a DELETE request ## ## @param endpoint: API endpoint ## @param callback: Function to call with APIResponse when complete ## @param error_callback: Optional function to call on error func http_delete(endpoint: String, callback: Callable, error_callback: Callable = Callable()) -> void: _make_request(Method.DELETE, endpoint, {}, callback, error_callback) ## Make a PATCH request ## ## @param endpoint: API endpoint ## @param data: Dictionary to send as JSON body ## @param callback: Function to call with APIResponse when complete ## @param error_callback: Optional function to call on error func http_patch(endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable = Callable()) -> void: _make_request(Method.PATCH, endpoint, data, callback, error_callback) ## Set the authentication token for subsequent requests ## ## @param token: JWT token from login/registration func set_auth_token(token: String) -> void: _auth_token = token print("[HTTPClient] Auth token set") ## Clear the authentication token (logout) func clear_auth_token() -> void: _auth_token = "" print("[HTTPClient] Auth token cleared") ## Get current auth token func get_auth_token() -> String: return _auth_token ## Set the session cookie for subsequent requests ## ## @param cookie: Session cookie value (e.g., "coc_session=xxx") func set_session_cookie(cookie: String) -> void: _session_cookie = cookie print("[HTTPClient] Session cookie set") ## Clear the session cookie (logout) func clear_session_cookie() -> void: _session_cookie = "" print("[HTTPClient] Session cookie cleared") ## Get current session cookie func get_session_cookie() -> String: return _session_cookie ## Check if authenticated func is_authenticated() -> bool: return not _auth_token.is_empty() or not _session_cookie.is_empty() ## Internal: Make HTTP request func _make_request( method: Method, endpoint: String, data: Dictionary, callback: Callable, error_callback: Callable ) -> void: # Create HTTPRequest node var http_request := HTTPRequest.new() add_child(http_request) # Generate request ID for tracking var request_id := "%s_%d" % [endpoint.get_file(), Time.get_ticks_msec()] _active_requests[request_id] = http_request # Build full URL var url := Settings.get_api_url() + endpoint # Build headers var headers := _build_headers() # Connect completion signal http_request.request_completed.connect( _on_request_completed.bind(request_id, callback, error_callback) ) # Set timeout http_request.timeout = API_TIMEOUT # Make request based on method var method_int := _method_to_int(method) var body := "" if method in [Method.POST, Method.PUT, Method.PATCH]: body = JSON.stringify(data) print("[HTTPClient] %s %s" % [_method_to_string(method), url]) if not body.is_empty(): print("[HTTPClient] Body: %s" % body) var error := http_request.request(url, headers, method_int, body) if error != OK: push_error("[HTTPClient] Failed to initiate request: %s" % error) _cleanup_request(request_id) if error_callback.is_valid(): error_callback.call(_create_error_response("Failed to initiate request", 0)) ## Internal: Build request headers func _build_headers() -> PackedStringArray: var headers := PackedStringArray([ "Content-Type: application/json", "Accept: application/json" ]) # Add auth token if present (for JWT auth) if not _auth_token.is_empty(): headers.append("Authorization: Bearer %s" % _auth_token) # Add session cookie if present (for cookie-based auth) if not _session_cookie.is_empty(): headers.append("Cookie: %s" % _session_cookie) return headers ## Internal: Handle request completion func _on_request_completed( result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, request_id: String, callback: Callable, error_callback: Callable ) -> void: print("[HTTPClient] Request completed: %s (status=%d)" % [request_id, response_code]) # Extract Set-Cookie header if present _extract_session_cookie(headers) # Parse response body var body_string := body.get_string_from_utf8() # Handle network errors if result != HTTPRequest.RESULT_SUCCESS: push_error("[HTTPClient] Network error: %s" % _result_to_string(result)) _cleanup_request(request_id) if error_callback.is_valid(): error_callback.call(_create_error_response(_result_to_string(result), response_code)) return # Parse JSON response var json := JSON.new() var parse_error := json.parse(body_string) if parse_error != OK: push_error("[HTTPClient] Failed to parse JSON: %s" % body_string) _cleanup_request(request_id) if error_callback.is_valid(): error_callback.call(_create_error_response("Failed to parse JSON response", response_code)) return # Create APIResponse var api_response: APIResponse = APIResponse.new(json.data) api_response.raw_response = body_string # Check for errors if api_response.has_error(): print("[HTTPClient] API error: %s" % api_response.get_error_message()) if error_callback.is_valid(): error_callback.call(api_response) elif callback.is_valid(): # Call regular callback even with error if no error callback callback.call(api_response) else: print("[HTTPClient] Success: %s" % request_id) if callback.is_valid(): callback.call(api_response) _cleanup_request(request_id) ## Internal: Extract session cookie from response headers func _extract_session_cookie(headers: PackedStringArray) -> void: for header in headers: # Look for Set-Cookie header if header.begins_with("Set-Cookie:") or header.begins_with("set-cookie:"): # Extract cookie value var cookie_string := header.substr(11).strip_edges() # Remove "Set-Cookie:" # Look for coc_session cookie if cookie_string.begins_with("coc_session="): # Extract just the cookie name=value part (before semicolon) var cookie_parts := cookie_string.split(";") if cookie_parts.size() > 0: _session_cookie = cookie_parts[0].strip_edges() print("[HTTPClient] Session cookie extracted: %s" % _session_cookie) return ## Internal: Cleanup request resources func _cleanup_request(request_id: String) -> void: if _active_requests.has(request_id): var http_request: HTTPRequest = _active_requests[request_id] http_request.queue_free() _active_requests.erase(request_id) ## Internal: Create error response func _create_error_response(message: String, status_code: int) -> APIResponse: var error_data := { "app": "Code of Conquest", "version": "0.1.0", "status": status_code if status_code > 0 else 500, "timestamp": Time.get_datetime_string_from_system(), "result": null, "error": { "message": message, "code": "NETWORK_ERROR" }, "meta": {} } return APIResponse.new(error_data) ## Internal: Convert Method enum to HTTPClient constant func _method_to_int(method: Method) -> int: match method: Method.GET: return HTTPClient.METHOD_GET Method.POST: return HTTPClient.METHOD_POST Method.PUT: return HTTPClient.METHOD_PUT Method.DELETE: return HTTPClient.METHOD_DELETE Method.PATCH: return HTTPClient.METHOD_PATCH _: return HTTPClient.METHOD_GET ## Internal: Convert Method enum to string func _method_to_string(method: Method) -> String: match method: Method.GET: return "GET" Method.POST: return "POST" Method.PUT: return "PUT" Method.DELETE: return "DELETE" Method.PATCH: return "PATCH" _: return "UNKNOWN" ## Internal: Convert HTTPRequest result to string func _result_to_string(result: int) -> String: match result: HTTPRequest.RESULT_SUCCESS: return "Success" HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH: return "Chunked body size mismatch" HTTPRequest.RESULT_CANT_CONNECT: return "Can't connect to server" HTTPRequest.RESULT_CANT_RESOLVE: return "Can't resolve hostname" HTTPRequest.RESULT_CONNECTION_ERROR: return "Connection error" HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR: return "TLS handshake error" HTTPRequest.RESULT_NO_RESPONSE: return "No response from server" HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED: return "Body size limit exceeded" HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED: return "Body decompression failed" HTTPRequest.RESULT_REQUEST_FAILED: return "Request failed" HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN: return "Can't open download file" HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR: return "Download file write error" HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: return "Redirect limit reached" HTTPRequest.RESULT_TIMEOUT: return "Request timeout" _: return "Unknown error (%d)" % result