Files
Code_of_Conquest/godot_client/scripts/services/http_client.gd
2025-11-24 23:10:55 -06:00

365 lines
10 KiB
GDScript

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