using Godot; using System; using System.Net.Http.Headers; using System.Threading.Tasks; using System.Text.Json; using System.IO; namespace BSEnroll { /// /// Singleton class for managing and storing user data across scenes. /// Handles sanitization of user IDs and tokens to ensure consistency. /// public partial class UserDataManager : Node { // Signal to notify when the token changes [Signal] public delegate void TokenChangedEventHandler(string newToken); // Signal to notify when the user ID changes [Signal] public delegate void UserIdChangedEventHandler(string newUserId); [Signal] public delegate void UserTrainedModelsChangedEventHandler(Godot.Collections.Array newUserTrainedModels); [Signal] public delegate void AuthenticationResponseEventHandler(int responseCode); public bool IsLoadingModel { get; private set; } public bool ReceivedModelsResponse { get; private set; } public string ModelLoadingError { get; set; } // Singleton instance private static UserDataManager _instance; // Private backing fields private string _userId = ""; private string _testingToken = "Please enter your user token"; private string _serverApiAddress = "http://104.171.203.65:8000"; private Godot.Collections.Array _userTrainedModels = new Godot.Collections.Array(); /// /// Resets all user data to default values. /// private void ResetData() { _userId = ""; _testingToken = ""; UserTrainedModels.Clear(); GD.Print("User data has been reset"); } /// /// User ID retrieved from the server. Quotes are automatically removed. /// public string UserId { get => _userId; private set { string sanitized = SanitizeString(value); if (_userId != sanitized) { _userId = sanitized; GD.Print($"User ID updated: {_userId}"); EmitSignal(SignalName.UserIdChanged, _userId); } } } /// /// Testing token for API authentication. /// public string TestingToken { get => _testingToken; set { ResetData(); var sanitized = SanitizeString(value); if (_testingToken == sanitized) return; _testingToken = sanitized; GD.Print($"Testing token updated."); if (!string.IsNullOrWhiteSpace(_testingToken)) { FetchUserIdFromApi(); FetchUserTrainedModels(); } EmitSignal(SignalName.TokenChanged, _testingToken); } } /// /// This tells the main/UI scene that the instance is meant to be enrollment (but likely failed to start). /// public bool IsEnrollmentInstance { get; set; } /// /// This tells the main/UI scene that the instance is meant to be alignment (but likely failed to start). /// public bool IsAlignmentInstance { get; set; } /// /// API server address to connect to. /// public string ServerApiAddress { get => _serverApiAddress; set { // Ensure URL doesn't end with a trailing slash _serverApiAddress = value?.TrimEnd('/') ?? "http://104.171.203.65"; GD.Print($"Server API address updated: {_serverApiAddress}"); } } /// /// List of trained models for the current user. /// public Godot.Collections.Array UserTrainedModels { get => _userTrainedModels; private set { bool hasChanged = true; if (_userTrainedModels != null && value != null) { if (_userTrainedModels.Count == value.Count) { hasChanged = false; for (int i = 0; i < _userTrainedModels.Count; i++) { if (!string.Equals(_userTrainedModels[i], value[i], StringComparison.Ordinal)) { hasChanged = true; break; } } } } if (hasChanged) { _userTrainedModels = value; GD.Print($"User trained models updated: {_userTrainedModels.Count} models"); EmitSignal(SignalName.UserTrainedModelsChanged, _userTrainedModels); } } } /// /// Singleton instance of UserDataManager. /// Creates a new instance if one doesn't exist yet. /// public static UserDataManager Instance { get { if (_instance == null) { _instance = new UserDataManager(); GD.Print("Created new UserDataManager instance"); } return _instance; } } /// /// Initializes the UserDataManager when added to the scene. /// public override void _Ready() { if (_instance != null && _instance != this) { GD.PrintErr("Multiple UserDataManager instances detected. Using existing instance."); QueueFree(); return; } _instance = this; GD.Print("UserDataManager initialized"); // Parse command line arguments ParseCommandLineArgs(); } /// /// Parses command line arguments for user ID and testing token. /// Expected format: --user-id "value" --token "value" /// private void ParseCommandLineArgs() { string[] args = OS.GetCmdlineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i] == "--user-id" && i + 1 < args.Length) { UserId = args[i + 1]; i++; // Skip the next argument since we've used it } else if (args[i] == "--token" && i + 1 < args.Length) { TestingToken = args[i + 1]; i++; // Skip the next argument since we've used it } else if (args[i] == "--enroll") { IsEnrollmentInstance = args[i + 1] == "1" ? true : false; i++; } else if (args[i] == "--align") { IsAlignmentInstance = args[i + 1] == "1" ? true : false; i++; } } if (!string.IsNullOrEmpty(UserId) || !string.IsNullOrEmpty(TestingToken)) { GD.Print($"Command line arguments parsed - User ID: {UserId}"); } } /// /// Sanitizes a string by removing quotes and trimming whitespace. /// /// The input string to sanitize /// Sanitized string without quotes private string SanitizeString(string input) { return input?.Replace("\"", "").Replace("'", "").Trim() ?? ""; } /// /// Fetches the user ID from the API using the current testing token. /// public async void FetchUserIdFromApi() { if (string.IsNullOrWhiteSpace(TestingToken)) { GD.Print("Cannot fetch user ID: Testing token is empty"); return; } string baseUrl = ServerApiAddress.TrimEnd('/'); string endpoint = $"{baseUrl}/get_user_id"; using (var httpClient = new System.Net.Http.HttpClient()) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TestingToken); var response = await httpClient.GetAsync(endpoint); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); string userId = content.Trim(); // Parse JSON if the response appears to be JSON if (content.StartsWith("{") || content.StartsWith("[")) { try { using (var jsonDoc = JsonDocument.Parse(content)) { if (jsonDoc.RootElement.ValueKind == JsonValueKind.Object && jsonDoc.RootElement.TryGetProperty("user_id", out var userIdElement)) { userId = userIdElement.GetString(); } } } catch (JsonException) { // If parsing fails, use content as is } } UserId = userId; EmitSignal(SignalName.AuthenticationResponse, 2); } else { GD.PrintErr($"API request failed: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(); GD.PrintErr($"Error response: {errorContent}"); if (errorContent.Contains("expired")) { EmitSignal(SignalName.AuthenticationResponse, 1); } else { EmitSignal(SignalName.AuthenticationResponse, 0); } } } } /// /// Fetches the user's trained models from the API using the current user ID and testing token. /// public async void FetchUserTrainedModels() { if (string.IsNullOrWhiteSpace(TestingToken)) { GD.Print("Cannot fetch user models: Testing token is empty"); return; } string baseUrl = ServerApiAddress.TrimEnd('/'); string endpoint = $"{baseUrl}/get_user_files"; using (var httpClient = new System.Net.Http.HttpClient()) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TestingToken); try { var response = await httpClient.GetAsync(endpoint); ReceivedModelsResponse = true; if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var models = new Godot.Collections.Array(); // Parse JSON response try { using (var jsonDoc = JsonDocument.Parse(content)) { if (jsonDoc.RootElement.ValueKind == JsonValueKind.Array) { foreach (var element in jsonDoc.RootElement.EnumerateArray()) { if (element.ValueKind == JsonValueKind.String) { string modelPath = element.GetString(); if (modelPath.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { models.Add(modelPath); } } else if (element.ValueKind == JsonValueKind.Object) { // If the model is an object, try to get a name property if (element.TryGetProperty("name", out var nameElement)) { string modelPath = nameElement.GetString(); if (modelPath.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { models.Add(modelPath); } } } } } else if (jsonDoc.RootElement.ValueKind == JsonValueKind.Object) { // If the response is an object, try to find an array property foreach (var property in jsonDoc.RootElement.EnumerateObject()) { if (property.Value.ValueKind == JsonValueKind.Array) { foreach (var element in property.Value.EnumerateArray()) { if (element.ValueKind == JsonValueKind.String) { string modelPath = element.GetString(); if (modelPath.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { models.Add(modelPath); } } } } } } } } catch (JsonException ex) { GD.PrintErr($"Error parsing JSON response: {ex.Message}"); // If parsing fails, try to split the content by newlines or commas var lines = content.Split(new[] { '\n', ',', ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var model = line.Trim().Replace("\"", "").Replace("'", ""); if (!string.IsNullOrWhiteSpace(model) && model.EndsWith(".model", StringComparison.OrdinalIgnoreCase)) { models.Add(model); } } } // Update the models list UserTrainedModels = models; EmitSignal(SignalName.AuthenticationResponse, 2); } else { GD.PrintErr($"API request failed: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(); GD.PrintErr($"Error response: {errorContent}"); if (errorContent.Contains("expired")) { EmitSignal(SignalName.AuthenticationResponse, 1); } else { EmitSignal(SignalName.AuthenticationResponse, 0); } } } catch (Exception ex) { ReceivedModelsResponse = false; } } } /// /// Downloads a model file from the API and saves it to the user's local storage. /// /// The path/name of the model to download /// The local path where the model was saved, or null if download failed public async Task DownloadModelAsync(string modelKey, string outputPath) { if (string.IsNullOrWhiteSpace(TestingToken)) { GD.Print("Cannot download model: Testing token is empty"); return null; } string baseUrl = ServerApiAddress.TrimEnd('/'); string endpoint = $"{baseUrl}/get_model"; IsLoadingModel = true; ModelLoadingError = ""; using (var httpClient = new System.Net.Http.HttpClient()) { // Add authorization header with bearer token string sanitizedToken = TestingToken.Trim().Replace("\r", "").Replace("\n", ""); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sanitizedToken); // Create the request URL with the model path as a query parameter string requestUrl = $"{endpoint}?model_s3_uri={Uri.EscapeDataString(modelKey)}"; try { // Make the request var response = await httpClient.GetAsync(requestUrl); if (response.IsSuccessStatusCode) { // Get the model data byte[] modelData = await response.Content.ReadAsByteArrayAsync(); // Create the models directory if it doesn't exist string modelsDir = "user://models"; string modelsDirPath = ProjectSettings.GlobalizePath(modelsDir); if (!Directory.Exists(modelsDirPath)) { Directory.CreateDirectory(modelsDirPath); } // Save the model file File.WriteAllBytes(outputPath, modelData); GD.Print($"Model downloaded and saved to: {outputPath}"); IsLoadingModel = false; } else { IsLoadingModel = false; ModelLoadingError = $"Failed to download model: {response.StatusCode}"; GD.PrintErr($"Failed to download model. Status code: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(); GD.PrintErr($"Error response: {errorContent}"); } return null; } catch (Exception ex) { IsLoadingModel = false; ModelLoadingError = $"Failed to download model: network error."; return null; } } } /// /// Enqueues a training job on the server for a previously uploaded zip file. /// /// Path to the zip file in the bucket /// True if the job was successfully enqueued, false otherwise public async Task EnqueueTrainingJobAsync(string bucketZipPath) { if (string.IsNullOrWhiteSpace(TestingToken)) { GD.Print("Cannot enqueue training job: Testing token is empty"); return false; } string baseUrl = ServerApiAddress.TrimEnd('/'); string endpoint = $"{baseUrl}/enqueue_training_job"; using (var httpClient = new System.Net.Http.HttpClient()) { try { // Add authorization header with bearer token string sanitizedToken = TestingToken.Trim().Replace("\r", "").Replace("\n", ""); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sanitizedToken); // Add bucket_zip_path as a query parameter string requestUrl = $"{endpoint}?bucket_zip_path={Uri.EscapeDataString(bucketZipPath)}"; // Make the request var response = await httpClient.PostAsync(requestUrl, null); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); GD.Print($"Training job enqueued successfully: {responseContent}"); return true; } else { GD.PrintErr($"Failed to enqueue training job. Status code: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(); GD.PrintErr($"Error response: {errorContent}"); return false; } } catch (Exception ex) { GD.PrintErr($"Exception when enqueuing training job: {ex.Message}"); return false; } } } } }