first commit
This commit is contained in:
364
godot_client/scripts/services/http_client.gd
Normal file
364
godot_client/scripts/services/http_client.gd
Normal file
@@ -0,0 +1,364 @@
|
||||
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
|
||||
Reference in New Issue
Block a user