365 lines
10 KiB
GDScript
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
|