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;
}
}
}
}
}