Files
Kalulu/sources/utils/autoloads/server_manager.gd
T
Adrien Ufferte 790260e2fd Add CHANGE_LANGUAGE feature to teacher settings
Reorders the left sidebar so risky actions sit at the bottom with a gap:
Dashboard, Synchronize, Logout | Change Language (red), Delete Account.

The new Change Language button opens a warning popup with a language
dropdown. On confirm, if the selected locale differs from the current
one, the client calls POST /reset_language. On success it wipes the
local teacher folder (progression, remediation, confusion matrix…),
applies the new language to device and teacher settings, logs out and
reloads from the splash screen. On error the user sees an error popup
and nothing local changes.
2026-04-17 16:38:00 +02:00

353 lines
13 KiB
GDScript

class_name ServerManagerClass
extends CanvasLayer
signal request_completed(success: bool, code: int, body: Dictionary)
signal internet_check_completed(has_access: bool)
const CONFIG_PATH: String = "user://environment.cfg"
const INTERNET_CHECK_URL: String = "https://google.com"
const AWS_API_GATEWAY_DOMAIN_ADRESS: String = "api.kalulu.org/"
const SUBDOMAIN_DEV: String = "dev."
const PROTOCOL: String = "https://"
const STAGE_DEV: String = "dev/"
const STAGE_PROD: String = "prod/"
# Response from the last request
var success: bool
var code: int
var json: Dictionary = {}
var environment_url: String = ""
var custom_environment_url: String = ""
var environment_setting: int = 1
@onready var internet_check: HTTPRequest = $InternetCheck
@onready var http_request: HTTPRequest = $HTTPRequest
@onready var loading_rect: TextureRect = $TextureRect
func _ready() -> void:
var config: ConfigFile = ConfigFile.new()
var load_error: Error = config.load(CONFIG_PATH)
if load_error == OK:
environment_setting = int(config.get_value("environment", "current", 0) as int)
custom_environment_url = str(config.get_value("environment", "custom_url", ""))
set_environment(environment_setting, custom_environment_url)
Log.info("ServerManager: Loaded environment config (setting=%d, custom_url=%s)" % [environment_setting, custom_environment_url])
else:
Log.warn("ServerManager: Could not load environment config at %s. Error: %s. Falling back to PROD environment." % [ProjectSettings.globalize_path(CONFIG_PATH), error_string(load_error)])
set_environment(1)
func set_environment(env: int, custom_url: String = "") -> void:
environment_setting = env
custom_environment_url = _normalize_url(custom_url)
environment_url = _resolve_environment_url()
_save_environment_config()
Log.info("ServerManager: Environment URL set to " + environment_url)
func set_environment_url(new_url: String) -> void:
custom_environment_url = _normalize_url(new_url)
environment_url = _resolve_environment_url()
_save_environment_config()
Log.info("ServerManager: Custom environment URL updated to " + environment_url)
func _resolve_environment_url() -> String:
if custom_environment_url.strip_edges() != "":
return _normalize_url(custom_environment_url)
match environment_setting:
0: return PROTOCOL + SUBDOMAIN_DEV + AWS_API_GATEWAY_DOMAIN_ADRESS + STAGE_DEV
1: return PROTOCOL + AWS_API_GATEWAY_DOMAIN_ADRESS + STAGE_PROD
_: return ""
func _normalize_url(url: String) -> String:
var normalized: String = url.strip_edges()
if normalized == "":
return ""
if not normalized.ends_with("/"):
normalized += "/"
return normalized
func _save_environment_config() -> void:
var config: ConfigFile = ConfigFile.new()
config.set_value("environment", "current", environment_setting)
config.set_value("environment", "custom_url", custom_environment_url)
var error: Error = config.save(CONFIG_PATH)
if error != OK:
Log.error("ServerManager: Failed to save environment config to %s. Error: %s" % [ProjectSettings.globalize_path(CONFIG_PATH), error_string(error)])
else:
Log.trace("ServerManager: Environment configuration saved to " + ProjectSettings.globalize_path(CONFIG_PATH))
func first_login_student() -> void:
await _post_json_request("submit_student_session", {"student_id": UserDataManager.student})
func check_email(email: String) -> Dictionary:
loading_rect.show()
await _get_request("checkemail", {"mail": email})
return _response()
func register(data: Dictionary) -> Dictionary:
loading_rect.show()
if data.has("email"):
Log.info("ServerManager: Registration request initiated for email %s" % str(data.email))
else:
Log.error("ServerManager: Registration request cancelled: email not provided")
reset_result()
return _response()
await _post_json_request("register", data)
return _response()
func login(mail: String, password: String) -> Dictionary:
loading_rect.show()
Log.info("ServerManager: Login request initiated for email %s" % mail)
await _post_json_request("login", {"mail": mail, "password": password})
return _response()
func reset_password(mail: String) -> Dictionary:
Log.info("ServerManager: Password reset requested for email %s" % mail)
await _post_json_request("forgot", {"email": mail})
return _response()
func delete_account() -> Dictionary:
loading_rect.show()
Log.warn("ServerManager: Delete account request initiated")
await _delete_request("delete_account")
return _response()
func get_language_pack_url(locale: String) -> Dictionary:
await _get_request("language", {"locale": locale})
return _response()
func pull_timestamps() -> Dictionary:
await _get_request("pull_timestamps", {})
return _response()
func send_server_synchronization_instructions(data: Dictionary) -> Dictionary:
await _post_json_request("pull_sync_status", data)
return _response()
func get_dashboard() -> Dictionary:
await _get_request("dashboard_user", {})
return _response()
func add_student(p_student: Dictionary) -> Dictionary:
Log.info("ServerManager: Add student request initiated")
await _post_request("add_student", p_student)
return _response()
func remove_student(p_code: int) -> Dictionary:
Log.info("ServerManager: Remove student request initiated for code %d" % p_code)
await _delete_request("remove_student", {"code": p_code})
return _response()
func set_student_data(student_code: int, data: Dictionary) -> Dictionary:
data.merge({"student_id": student_code})
Log.info("ServerManager: Set student data request initiated for code %d" % student_code)
await _post_request("set_student_data", data)
return _response()
func get_user_language() -> Dictionary:
await _get_request("get_language", {})
return _response()
func set_user_language(language: String) -> Dictionary:
var data: Dictionary = {"language": language}
await _post_request("set_language", data)
return _response()
func reset_language(language: String) -> Dictionary:
loading_rect.show()
Log.warn("ServerManager: Reset language request initiated for %s" % language)
var data: Dictionary = {"language": language}
await _post_request("reset_language", data)
return _response()
#region Sender functions
func check_internet_access() -> bool:
Log.trace("ServerManager: Sending simple request to " + INTERNET_CHECK_URL + " to check if internet is available")
var res: Error = internet_check.request(INTERNET_CHECK_URL)
if res == OK:
return await internet_check_completed
return false
func _create_uri_with_parameters(uri: String, params: Dictionary) -> String:
if params.is_empty():
return uri
var query_parts: Array[String] = []
for key: String in params.keys():
var encoded_key: String = str(key).uri_encode()
var encoded_value: String = str(params[key]).uri_encode()
query_parts.append("%s=%s" % [encoded_key, encoded_value])
var full_uri: String = uri + "?" + "&".join(query_parts)
if not params.has("password"):
Log.trace("ServerManager: Encoded URI = %s" % full_uri)
else:
Log.warn("ServerManager: A password should probably not be sent as URI parameter")
return full_uri
func _create_request_headers(content_type_json: bool = false) -> PackedStringArray:
var headers: PackedStringArray = []
var teacher_settings: TeacherSettings = UserDataManager.teacher_settings
if teacher_settings and teacher_settings.token:
headers.append("Authorization: Bearer " + teacher_settings.token)
if content_type_json:
headers.append("Content-Type: application/json")
Log.trace("ServerManager: Create Header: " + str(headers))
return headers
func _response() -> Dictionary:
var res: Dictionary = {
"success": success,
"code": code,
"body": json
}
return res
func _get_request(uri: String, params: Dictionary) -> void:
reset_result()
var headers: PackedStringArray = _create_request_headers()
if params.has("password"):
Log.trace("ServerManager: Sending GET request.\n URI = %s\n Parameters not logged because it contains a password." % uri)
else:
Log.trace("ServerManager: Sending GET request.\n URI = %s\n Parameters = %s" % [uri, params])
var req_url: String = _create_uri_with_parameters(environment_url + uri, params)
var request_error: Error = http_request.request(req_url, headers)
if request_error == OK:
await request_completed
else:
Log.error("ServerManager: Error sending GET request to %s. Error: %s" % [req_url, error_string(request_error)])
code = 500
json = {message = "Internal Server Error"}
func _post_request(uri: String, params: Dictionary) -> void:
reset_result()
var url: String = _create_uri_with_parameters(environment_url + uri, params)
var headers: PackedStringArray = _create_request_headers()
if params.has("password"):
Log.trace("ServerManager: Sending POST request.\n URI = %s\n Parameters not logged because it contains a password." % uri)
else:
Log.trace("ServerManager: Sending POST request.\n URI = %s\n Parameters = %s" % [uri, params])
var request_error: Error = http_request.request(url, headers, HTTPClient.METHOD_POST, "")
if request_error == OK:
await request_completed
else:
Log.error("ServerManager: Error sending POST request to %s. Error: %s" % [url, error_string(request_error)])
code = 500
json = {message = "Internal Server Error"}
func _post_json_request(uri: String, data: Dictionary) -> void:
reset_result()
var req: String = environment_url + uri
var headers: PackedStringArray = _create_request_headers(true)
if data.has("password"):
Log.trace("ServerManager: Sending POST JSON request.\n URI = %s\n Data not logged because it contains a password." % uri)
else:
Log.trace("ServerManager: Sending POST JSON request.\n URI = %s\n Data = %s" % [uri, data])
var request_error: Error = http_request.request(req, headers, HTTPClient.METHOD_POST, JSON.stringify(data))
if request_error == OK:
await request_completed
else:
Log.error("ServerManager: Error sending POST JSON request to %s. Error: %s" % [req, error_string(request_error)])
code = 500
json = {message = "Internal Server Error"}
func _delete_request(uri: String, params: Dictionary = {}) -> void:
reset_result()
var req: String = _create_uri_with_parameters(environment_url + uri, params)
var headers: PackedStringArray = _create_request_headers()
Log.trace("ServerManager: Sending DELETE request.\n URI = %s\n Parameters = %s" % [uri, params])
var request_error: Error = http_request.request(req, headers, HTTPClient.METHOD_DELETE, "")
if request_error == OK:
await request_completed
else:
Log.error("ServerManager: Error sending DELETE request to %s. Error: %s" % [req, error_string(request_error)])
code = 500
json = {message = "Internal Server Error"}
#endregion
func _on_http_request_request_completed(result_code: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result_code != OK:
Log.warn("ServerManager: Cannot complete http request. Error code %d = %s" % [result_code, error_string(result_code)])
else:
code = response_code
if code == 200:
Log.trace("ServerManager: Request Completed. Response code = %d" % response_code)
else:
Log.warn("ServerManager: Request Completed. Response code = %d" % response_code)
if body:
var str_body: String = body.get_string_from_utf8()
var result: Variant = JSON.parse_string(str_body)
if result is Dictionary or result is Array:
var pretty: String = JSON.stringify(result, "\t")
if code == 200:
Log.trace("ServerManager: Prettyfied Body received :\n%s" % pretty)
else:
Log.warn("ServerManager: Prettyfied Body received :\n%s" % pretty)
else:
if code == 200:
Log.trace("ServerManager: Body received = %s" % str_body)
else:
Log.warn("ServerManager: Body received = %s" % str_body)
if result != null:
json = result
else:
if code == 200:
Log.trace("ServerManager: Result null after parsing String to JSON")
else:
Log.warn("ServerManager: Result null after parsing String to JSON")
else:
if code == 200:
Log.trace("ServerManager: Body received is empty")
else:
Log.warn("ServerManager: Body received is empty")
success = result_code == OK and response_code == 200
request_completed.emit(success, response_code, json)
loading_rect.hide()
func _on_internet_check_request_completed(result_code: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void:
if result_code != OK:
Log.warn("ServerManager: Cannot check internet request. Error code %d = %s" % [result_code, error_string(result_code)])
else:
Log.trace("ServerManager: Internet check completed.\n Response code = %s. (200 = OK)" % str(response_code))
success = result_code == OK and response_code == 200
internet_check_completed.emit(success)
func reset_result() -> void:
success = false
code = 0
json = {}