using System.Diagnostics; using System.Net.WebSockets; using System.Text; using CORP_MAUI.Models; using Newtonsoft.Json; using System.Threading; namespace CORP_MAUI.Services; public class WebSocketService { private ClientWebSocket? _webSocket; // Возможные варианты WebSocket URL (будем пробовать по порядку) private static readonly string[] WebSocketUrls = new string[] { "wss://corp.robotail.pro/ws", "ws://corp.robotail.pro/ws" }; private string? _connectedUrl; private CancellationTokenSource? _cancellationTokenSource; private bool _isConnected; public event EventHandler? MessageReceived; public event EventHandler>? MessageHistoryReceived; public event EventHandler? UserStatusChanged; public event EventHandler? ConnectionStatusChanged; // Сделаем выражение полностью булевым (защита от null) public bool IsConnected => _isConnected && (_webSocket?.State == WebSocketState.Open); public async Task ConnectAsync(string authToken) { Trace.WriteLine("\n╔════════════════════════════════════════════════════════════"); Trace.WriteLine("║ [WebSocket] CONNECTION ATTEMPT"); Trace.WriteLine("╚════════════════════════════════════════════════════════════"); Trace.WriteLine($"[WebSocket] Has Token: {!string.IsNullOrEmpty(authToken)}"); if (string.IsNullOrEmpty(authToken)) { Trace.WriteLine("[WebSocket] ❌ ERROR: No authentication token provided!"); throw new ArgumentException("Authentication token is required", nameof(authToken)); } if (_webSocket is { State: WebSocketState.Open }) { Trace.WriteLine("[WebSocket] Closing existing connection..."); await DisconnectAsync(); } // Пробуем подключиться к разным URL по очереди Exception? lastException = null; foreach (string url in WebSocketUrls) { try { Trace.WriteLine($"\n[WebSocket] >>> Trying: {url}"); _webSocket = new ClientWebSocket(); // Настройка для Android - увеличиваем таймаут и буферы _webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30); // 🔑 Устанавливаем Authorization заголовок Trace.WriteLine($"[WebSocket] 🔑 Setting Authorization header..."); Trace.WriteLine($"[WebSocket] Token (first 30 chars): {authToken.Substring(0, Math.Min(30, authToken.Length))}..."); Trace.WriteLine($"[WebSocket] Token length: {authToken.Length}"); _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {authToken}"); Trace.WriteLine($"[WebSocket] ✅ Authorization header set: Bearer [token]"); _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(15)); // Таймаут подключения Uri uri = new Uri(url); Trace.WriteLine($"[WebSocket] Connecting to: {uri}"); await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token); // Если дошли сюда - подключение успешно! _isConnected = true; _connectedUrl = url; ConnectionStatusChanged?.Invoke(this, true); Trace.WriteLine($"[WebSocket] ✅ Connected successfully!"); Trace.WriteLine($"[WebSocket] URL: {url}"); Trace.WriteLine($"[WebSocket] State: {_webSocket.State}\n"); // Запускаем прослушивание сообщений _ = Task.Run(async () => await ReceiveMessagesAsync()); return; // Успешно подключились - выходим } catch (WebSocketException wsEx) { Trace.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}"); Trace.WriteLine($" WebSocketError: {wsEx.WebSocketErrorCode}"); Trace.WriteLine($" Message: {wsEx.Message}"); Trace.WriteLine($" InnerException: {wsEx.InnerException?.Message}"); lastException = wsEx; // Закрываем и очищаем _webSocket?.Dispose(); _webSocket = null; _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } catch (Exception ex) { Trace.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}"); Trace.WriteLine($" Exception: {ex.GetType().Name}"); Trace.WriteLine($" Message: {ex.Message}"); lastException = ex; // Закрываем и очищаем _webSocket?.Dispose(); _webSocket = null; _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } } // Если дошли сюда - ни один URL не сработал _isConnected = false; ConnectionStatusChanged?.Invoke(this, false); Trace.WriteLine($"\n[WebSocket] ❌ ALL CONNECTION ATTEMPTS FAILED!"); Trace.WriteLine($"[WebSocket] Tried {WebSocketUrls.Length} different URLs"); Trace.WriteLine($"[WebSocket] Last error: {lastException?.Message}\n"); throw new Exception($"Не удалось подключиться к WebSocket. Попробовано {WebSocketUrls.Length} вариантов URL.", lastException); } public async Task DisconnectAsync() { try { if (_webSocket is { State: WebSocketState.Open }) { // Отмена токена — не асинхронный вызов _cancellationTokenSource?.Cancel(); await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); } _isConnected = false; ConnectionStatusChanged?.Invoke(this, false); } catch (Exception ex) { Trace.WriteLine($"Ошибка отключения WebSocket: {ex.Message}"); } finally { _webSocket?.Dispose(); _cancellationTokenSource?.Dispose(); _webSocket = null; _cancellationTokenSource = null; } } public async Task SendMessageAsync(Message message) { if (!IsConnected || _webSocket == null || _cancellationTokenSource == null) { throw new InvalidOperationException("WebSocket не подключен"); } try { string json = JsonConvert.SerializeObject(new { type = "message", data = message }); byte[] bytes = Encoding.UTF8.GetBytes(json); ArraySegment buffer = new ArraySegment(bytes); await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token); } catch (Exception ex) { Trace.WriteLine($"Ошибка отправки сообщения через WebSocket: {ex.Message}"); throw; } } public async Task SendTypingIndicatorAsync(int recipientId, bool isTyping) { if (!IsConnected || _webSocket == null || _cancellationTokenSource == null) { return; } try { string json = JsonConvert.SerializeObject(new { type = "typing", data = new { recipientId, isTyping } }); byte[] bytes = Encoding.UTF8.GetBytes(json); ArraySegment buffer = new ArraySegment(bytes); await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token); } catch (Exception ex) { Trace.WriteLine($"Ошибка отправки индикатора набора: {ex.Message}"); } } public async Task RequestMessageHistoryAsync(int userId) { if (!IsConnected || _webSocket == null || _cancellationTokenSource == null) { Trace.WriteLine("[WebSocket] Не подключен - невозможно запросить историю"); return; } try { string json = JsonConvert.SerializeObject(new { type = "get_history", data = new { userId } }); byte[] bytes = Encoding.UTF8.GetBytes(json); ArraySegment buffer = new ArraySegment(bytes); await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token); Trace.WriteLine($"[WebSocket] Запрошена история сообщений с пользователем {userId}"); } catch (Exception ex) { Trace.WriteLine($"[WebSocket] Ошибка запроса истории: {ex.Message}"); } } private async Task ReceiveMessagesAsync() { byte[] buffer = new byte[8192]; try { while (_webSocket?.State == WebSocketState.Open && _cancellationTokenSource is { Token.IsCancellationRequested: false }) { WebSocketReceiveResult result = await _webSocket.ReceiveAsync( new ArraySegment(buffer), _cancellationTokenSource.Token); if (result.MessageType == WebSocketMessageType.Close) { await _webSocket.CloseAsync( WebSocketCloseStatus.NormalClosure, "Closed by server", CancellationToken.None); _isConnected = false; ConnectionStatusChanged?.Invoke(this, false); break; } string json = Encoding.UTF8.GetString(buffer, 0, result.Count); await ProcessMessageAsync(json); } } catch (OperationCanceledException) { // Нормальное завершение } catch (Exception ex) { Trace.WriteLine($"Ошибка получения сообщений WebSocket: {ex.Message}"); _isConnected = false; ConnectionStatusChanged?.Invoke(this, false); } } private async Task ProcessMessageAsync(string json) { try { WebSocketMessage? wrapper = JsonConvert.DeserializeObject(json); await MainThread.InvokeOnMainThreadAsync(() => { switch (wrapper?.Type) { case "message": Message? message = JsonConvert.DeserializeObject(wrapper.Data.ToString() ?? string.Empty); if (message != null) MessageReceived?.Invoke(this, message); break; case "message_history": List? history = JsonConvert.DeserializeObject>(wrapper.Data.ToString() ?? string.Empty); if (history != null) MessageHistoryReceived?.Invoke(this, history); break; case "user_status": User? user = JsonConvert.DeserializeObject(wrapper.Data.ToString() ?? string.Empty); if (user != null) UserStatusChanged?.Invoke(this, user); break; case "typing": // Обработка индикатора набора break; default: Trace.WriteLine($"Неизвестный тип сообщения: {wrapper?.Type ?? "null"}"); break; } }); } catch (Exception ex) { Trace.WriteLine($"Ошибка обработки сообщения: {ex.Message}"); } } private class WebSocketMessage { [JsonProperty("type")] public string Type { get; set; } = string.Empty; [JsonProperty("data")] public object Data { get; set; } = new(); } }