using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace KbChatMq { /// /// KBChatMQ Hub — HTTP client. Chỉ URL Hub trong ctor; credential truyền từng lần gọi. /// .NET 5+ (hoặc .NET Standard 2.0 + HttpClient + System.Text.Json package). /// public sealed class KbChatMqClient : IDisposable { private readonly HttpClient _http; private readonly string _apiBase; private readonly JsonSerializerOptions _jsonOpt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public KbChatMqClient(string baseUrl, HttpMessageHandler handler = null) { if (string.IsNullOrWhiteSpace(baseUrl)) throw new ArgumentException("baseUrl required", nameof(baseUrl)); var s = baseUrl.Trim().TrimEnd('/'); _apiBase = s.EndsWith("/api", StringComparison.OrdinalIgnoreCase) ? s : s + "/api"; _http = handler == null ? new HttpClient() : new HttpClient(handler); _http.DefaultRequestHeaders.Accept.Clear(); _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } public void Dispose() => _http.Dispose(); private async Task<(bool Ok, int Status, JsonElement Data)> JsonAsync( string path, HttpMethod method, object body = null, CancellationToken ct = default) { var p = path.StartsWith("/") ? path : "/" + path; var req = new HttpRequestMessage(method, _apiBase + p); if (body != null) { var json = JsonSerializer.Serialize(body, _jsonOpt); req.Content = new StringContent(json, Encoding.UTF8, "application/json"); } HttpResponseMessage res; try { res = await _http.SendAsync(req, ct).ConfigureAwait(false); } catch { return (false, 0, JsonDocument.Parse("{\"ok\":false,\"error\":\"network\"}").RootElement); } var status = (int)res.StatusCode; var txt = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); JsonElement data; try { using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(txt) ? "{}" : txt); data = doc.RootElement.Clone(); } catch { data = JsonDocument.Parse("{\"ok\":false,\"error\":\"invalid_json\"}").RootElement; } var okFlag = status >= 200 && status < 300; if (data.TryGetProperty("ok", out var okProp) && okProp.ValueKind == JsonValueKind.False) okFlag = false; return (okFlag, status, data); } public Task<(bool Ok, int Status, JsonElement Data)> LoginWithPasswordAsync(string userId, string password, CancellationToken ct = default) => JsonAsync("/auth/login", HttpMethod.Post, new { userId, password }, ct); public Task<(bool Ok, int Status, JsonElement Data)> LoginWithUserTokenAsync(string userToken, CancellationToken ct = default) => JsonAsync("/auth/login-token", HttpMethod.Post, new { userToken }, ct); public Task<(bool Ok, int Status, JsonElement Data)> LoginVisitorAsync(string websiteToken, string visitorId = null, CancellationToken ct = default) { object body = string.IsNullOrEmpty(visitorId) ? new { websiteToken } : new { websiteToken, visitorId }; return JsonAsync("/auth/login-visitor", HttpMethod.Post, body, ct); } public Task<(bool Ok, int Status, JsonElement Data)> LoginDeviceAsync(string deviceToken, string deviceId = null, CancellationToken ct = default) { object body = string.IsNullOrEmpty(deviceId) ? new { deviceToken } : new { deviceToken, deviceId }; return JsonAsync("/auth/login-device", HttpMethod.Post, body, ct); } public Task<(bool Ok, int Status, JsonElement Data)> ChatRoomsAsync(object authBody, CancellationToken ct = default) => JsonAsync("/auth/chat-rooms", HttpMethod.Post, authBody, ct); public Task<(bool Ok, int Status, JsonElement Data)> GetHeartbeatConfigAsync(CancellationToken ct = default) => JsonAsync("/chat/heartbeat-config", HttpMethod.Get, null, ct); public Task<(bool Ok, int Status, JsonElement Data)> HeartbeatAsync(object payload, CancellationToken ct = default) => JsonAsync("/chat/heartbeat", HttpMethod.Post, payload, ct); public Task<(bool Ok, int Status, JsonElement Data)> GetPresenceAsync(CancellationToken ct = default) => JsonAsync("/chat/presence", HttpMethod.Get, null, ct); } }