using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using QtCNETAPI.Dtos.User; using QtCNETAPI.Events; using QtCNETAPI.Models; using QtCNETAPI.Services.ApiService; namespace QtCNETAPI.Services.GatewayService { public class GatewayService(string GWUrl, IApiService apiService, LoggingService loggingService) : IGatewayService, IAsyncDisposable { internal string gwBaseUri = GWUrl; public Room? CurrentRoom { get; private set; } public bool InLobby { get; private set; } public HubConnection? HubConnection { get; private set; } public event EventHandler? OnRoomMessageReceived; public event EventHandler? OnRoomUserListReceived; public event EventHandler? OnGuestUserJoin; public event EventHandler? OnRefreshUserListsReceived; public event EventHandler? OnRefreshRoomListReceived; public event EventHandler? OnRoomDeleted; public event EventHandler? OnRefreshContactsListReceived; public event EventHandler? OnClientFunctionReceived; public event EventHandler? OnDirectMessageReceived; public event EventHandler? OnServerConfigReceived; public event EventHandler? OnServerDisconnect; public event EventHandler? OnServerReconnecting; public event EventHandler? OnServerReconnected; public event EventHandler? OnUserForceLogout; private IApiService _apiService = apiService; private LoggingService _loggingService = loggingService; public async Task StartAsync() { // build connection var gwConBuilder = new HubConnectionBuilder() .WithAutomaticReconnect() .ConfigureLogging((builder) => { builder.AddProvider(new LoggingServiceProvider(_loggingService)); if (System.Diagnostics.Debugger.IsAttached) builder.SetMinimumLevel(LogLevel.Debug); else builder.SetMinimumLevel(LogLevel.Error); }) .WithUrl(gwBaseUri, options => { options.AccessTokenProvider = async () => { // this should hopefully refresh the session every time the gateway connection is used to prevent connection aborts await _apiService.RefreshSessionIfInvalid(); return _apiService.SessionToken; }; }) .WithStatefulReconnect() .WithKeepAliveInterval(TimeSpan.FromMinutes(1)); HubConnection = gwConBuilder.Build(); // register events HubConnection.On("RoomMessage", (serverMessage) => OnRoomMessageReceived?.Invoke(this, new ServerMessageEventArgs { Message = serverMessage })); HubConnection.On("cf", (function) => OnClientFunctionReceived?.Invoke(this, new ClientFunctionEventArgs { Function = function })); HubConnection.On("ReceiveDirectMessage", (message, user) => OnDirectMessageReceived?.Invoke(this, new DirectMessageEventArgs { Message = message, User = user })); HubConnection.On("RefreshUserLists", () => OnRefreshUserListsReceived?.Invoke(this, EventArgs.Empty)); HubConnection.On("RefreshRoomList", () => OnRefreshRoomListReceived?.Invoke(this, EventArgs.Empty)); HubConnection.On("RefreshContactsList", () => OnRefreshContactsListReceived?.Invoke(this, EventArgs.Empty)); HubConnection.On("ReceiveServerConfig", (serverConfig) => OnServerConfigReceived?.Invoke(this, new ServerConfigEventArgs { ServerConfig = serverConfig })); HubConnection.On>("RoomUserList", (userList) => OnRoomUserListReceived?.Invoke(this, new RoomListEventArgs { UserList = userList })); HubConnection.On("GuestJoin", (username) => OnGuestUserJoin?.Invoke(this, new GuestUserJoinEventArgs { Username = username })); HubConnection.On("RoomDeleted", () => OnRoomDeleted?.Invoke(this, EventArgs.Empty)); HubConnection.On("ForceSignOut", () => OnUserForceLogout?.Invoke(this, EventArgs.Empty)); HubConnection.Closed += HubConnection_Closed; HubConnection.Reconnecting += HubConnection_Reconnecting; HubConnection.Reconnected += HubConnection_Reconnected; // start connection try { await HubConnection.StartAsync(); } catch (HttpRequestException ex) { _loggingService.LogString($"Unable To Connect To SignalR.\n{ex.Message}\n{ex.StackTrace}"); return; } // ensure current user is up to date (particularly status) await _apiService.SetCurrentUser(); } public async Task StopAsync() { if (HubConnection != null && HubConnection.State == HubConnectionState.Connected) { await HubConnection.StopAsync(); } } async ValueTask IAsyncDisposable.DisposeAsync() { if (HubConnection != null && HubConnection.State == HubConnectionState.Disconnected) { await HubConnection.DisposeAsync(); HubConnection = null; CurrentRoom = null; } } public async Task DisposeAsync() { if (HubConnection != null && HubConnection.State == HubConnectionState.Disconnected) { await HubConnection.DisposeAsync(); HubConnection = null; CurrentRoom = null; } } public async Task JoinLobbyAsync() { if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); await HubConnection.SendAsync("JoinLobby", _apiService.CurrentUser); InLobby = true; CurrentRoom = null; } public async Task JoinRoomAsync(Room room) { if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); if (InLobby == true) { await HubConnection.SendAsync("LeaveLobby", _apiService.CurrentUser); InLobby = false; } else if (CurrentRoom != null) { await HubConnection.SendAsync("LeaveRoom", _apiService.CurrentUser, CurrentRoom); } await HubConnection.SendAsync("JoinRoom", _apiService.CurrentUser, room); CurrentRoom = room; } public async Task LeaveRoomAsync() { if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); if (InLobby) { await HubConnection.SendAsync("LeaveLobby", _apiService.CurrentUser); InLobby = false; } else { await HubConnection.SendAsync("LeaveRoom", _apiService.CurrentUser, CurrentRoom); CurrentRoom = null; } } public async Task PostMessageAsync(Message message) { if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); await HubConnection.SendAsync("SendMessage", _apiService.CurrentUser, message, InLobby, CurrentRoom); } public async Task SendDirectMessageAsync(UserInformationDto user, Message message) { await _apiService.RefreshSessionIfInvalid(); if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); await HubConnection.SendAsync("SendDirectMessage", _apiService.CurrentUser, user, message); } public async Task UpdateStatus(int status) { if (HubConnection == null || HubConnection.State != HubConnectionState.Connected) throw new InvalidOperationException("Function was called before connection was made."); await HubConnection.SendAsync("UpdateStatus", _apiService.CurrentUser, status); // anything that changes the user should tell the api service to set it again await _apiService.SetCurrentUser(); } private Task HubConnection_Closed(Exception? arg) { OnServerDisconnect?.Invoke(this, new ServerConnectionClosedEventArgs { Error = arg }); return Task.CompletedTask; } private Task HubConnection_Reconnecting(Exception? arg) { OnServerReconnecting?.Invoke(this, new ServerConnectionReconnectingEventArgs { Error = arg }); return Task.CompletedTask; } private Task HubConnection_Reconnected(string? arg) { OnServerReconnected?.Invoke(this, EventArgs.Empty); return Task.CompletedTask; } } }