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