using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using UnityEngine; using UnityEngine.Networking; namespace MyInfoMate.Vr.Net { /// /// Le seul endroit du projet qui parle à manager-service. /// /// Trois règles tenues ici, parce qu'elles se paient cher ailleurs : /// la clé d'API part en en-tête X-Api-Key et jamais dans l'URL ; /// un échec produit une phrase lisible comme , /// jamais un null silencieux ; et le JSON est en camelCase côté serveur, /// PascalCase côté C#. /// public class ApiClient { public const string ApiKeyHeader = "X-Api-Key"; /// Une borne sur un wifi de musée : ni instantané, ni infini. const int TimeoutSeconds = 20; static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, MissingMemberHandling = MissingMemberHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }; public string BaseUrl { get; } /// Null tant que l'appairage n'a pas eu lieu. public string ApiKey { get; set; } public ApiClient(string baseUrl, string apiKey = null) { // Une base d'URL avec un slash final produit "//api/..." — accepté par ASP.NET, // mais pas par tous les proxies devant. BaseUrl = baseUrl?.TrimEnd('/'); ApiKey = apiKey; } public class Result { public T Value; /// Null si tout va bien ; sinon une phrase destinée au visiteur. public string Error; public bool Ok => Error == null; } public Task> GetAsync(string path) => SendAsync(UnityWebRequest.Get(BaseUrl + path), path); public Task> PostAsync(string path, object body) => SendBody(path, body, UnityWebRequest.kHttpVerbPOST); public Task> PutAsync(string path, object body) => SendBody(path, body, UnityWebRequest.kHttpVerbPUT); Task> SendBody(string path, object body, string verb) { var json = JsonConvert.SerializeObject(body, Settings); var request = new UnityWebRequest(BaseUrl + path, verb) { uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)), downloadHandler = new DownloadHandlerBuffer() }; request.SetRequestHeader("Content-Type", "application/json"); return SendAsync(request, path); } /// /// Le texte brut, pour l'export de configuration : c'est un /// FileContentResult côté serveur, et on veut pouvoir l'écrire tel quel /// dans le cache disque avant de le désérialiser. /// public async Task> GetRawAsync(string path) { using var request = UnityWebRequest.Get(BaseUrl + path); var sent = await Send(request, path); return sent != null ? new Result { Error = sent } : new Result { Value = request.downloadHandler.text }; } async Task> SendAsync(UnityWebRequest request, string path) { using (request) { var sent = await Send(request, path); if (sent != null) return new Result { Error = sent }; // Un 204 n'a pas de corps — c'est le cas nominal de la télémétrie, pas // une réponse illisible. if (string.IsNullOrWhiteSpace(request.downloadHandler?.text)) return new Result(); try { return new Result { Value = JsonConvert.DeserializeObject( request.downloadHandler.text, Settings) }; } catch (JsonException e) { Debug.LogError($"[Api] {path} : réponse illisible — {e.Message}"); return new Result { Error = "Le serveur a répondu quelque chose d'inattendu." }; } } } /// Null si la requête est passée ; sinon la phrase à afficher. async Task Send(UnityWebRequest request, string path) { request.timeout = TimeoutSeconds; if (!string.IsNullOrEmpty(ApiKey)) request.SetRequestHeader(ApiKeyHeader, ApiKey); UnityWebRequestAsyncOperation operation; try { operation = request.SendWebRequest(); } catch (System.InvalidOperationException e) { // Le cas connu : « Insecure connection not allowed », quand le Player // interdit le HTTP en clair et que le serveur est en http://. Ça lève // au lieu de rendre un code, donc sans ce catch l'await remonte // l'exception et le casque n'affiche rien du tout. Debug.LogError($"[Api] {path} : requête refusée avant l'envoi — {e.Message}"); return "Le serveur est injoignable. Vérifiez la connexion du casque."; } while (!operation.isDone) await Task.Yield(); if (request.result == UnityWebRequest.Result.Success) return null; Debug.LogError($"[Api] {path} : {(long)request.responseCode} {request.error}"); // Ces trois codes-là ne sont pas des pannes réseau, ce sont des erreurs // d'appairage — et c'est ce que la personne devant le casque doit lire. return request.responseCode switch { 401 => "Ce casque n'est plus autorisé. Refaites l'appairage.", 403 => "Ce casque n'a pas accès à ce contenu.", 404 => "Ce contenu n'existe plus sur le serveur.", _ => "Le serveur est injoignable. Vérifiez la connexion du casque." }; } } }