333 lines
13 KiB
C#
333 lines
13 KiB
C#
using System.Diagnostics;
|
||
using System.Net.WebSockets;
|
||
using System.Text;
|
||
using CORP_MAUI.Models;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace CORP_MAUI.Services;
|
||
|
||
public class WebSocketService
|
||
{
|
||
private ClientWebSocket? _webSocket;
|
||
|
||
// Возможные варианты WebSocket URL (будем пробовать по порядку)
|
||
private static readonly string[] WebSocketUrls =
|
||
{
|
||
"wss://corp.robotail.pro/ws",
|
||
"ws://corp.robotail.pro/ws",
|
||
"wss://corp.robotail.pro/ws/",
|
||
"ws://corp.robotail.pro/ws/",
|
||
"wss://corp.robotail.pro/api/ws",
|
||
"wss://corp.robotail.pro/api/v1/ws/chat/"
|
||
};
|
||
|
||
private string? _connectedUrl;
|
||
private CancellationTokenSource? _cancellationTokenSource;
|
||
private bool _isConnected;
|
||
|
||
public event EventHandler<Message>? MessageReceived;
|
||
public event EventHandler<List<Message>>? MessageHistoryReceived;
|
||
public event EventHandler<User>? UserStatusChanged;
|
||
public event EventHandler<bool>? ConnectionStatusChanged;
|
||
|
||
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();
|
||
|
||
// 🔑 Устанавливаем 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();
|
||
|
||
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 })
|
||
{
|
||
if (_cancellationTokenSource != null)
|
||
{
|
||
await _cancellationTokenSource.CancelAsync();
|
||
}
|
||
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<byte> buffer = new ArraySegment<byte>(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<byte> buffer = new ArraySegment<byte>(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<byte> buffer = new ArraySegment<byte>(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<byte>(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<WebSocketMessage>(json);
|
||
|
||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||
{
|
||
switch (wrapper?.Type)
|
||
{
|
||
case "message":
|
||
Message? message = JsonConvert.DeserializeObject<Message>(wrapper.Data.ToString() ?? string.Empty);
|
||
if (message != null) MessageReceived?.Invoke(this, message);
|
||
break;
|
||
|
||
case "message_history":
|
||
List<Message>? history = JsonConvert.DeserializeObject<List<Message>>(wrapper.Data.ToString() ?? string.Empty);
|
||
if (history != null) MessageHistoryReceived?.Invoke(this, history);
|
||
break;
|
||
|
||
case "user_status":
|
||
User? user = JsonConvert.DeserializeObject<User>(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();
|
||
}
|
||
}
|
||
|