using Dalamud.Utility; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto; using MareSynchronos.API.SignalR; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI.SignalR; using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using System.Reflection; namespace MareSynchronos.WebAPI; #pragma warning disable MA0040 public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient { public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; public const string MainServiceUri = "wss://maresynchronos.com"; private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private CancellationTokenSource _connectionCancellationTokenSource; private ConnectionDto? _connectionDto; private bool _doNotNotifyOnNextInfo = false; private CancellationTokenSource? _healthCheckTokenSource = new(); private bool _initialized; private string? _lastUsedToken; private HubConnection? _mareHub; private ServerState _serverState; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator, TokenProvider tokenProvider) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; _pairManager = pairManager; _serverManager = serverManager; _tokenProvider = tokenProvider; _connectionCancellationTokenSource = new CancellationTokenSource(); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); Mediator.Subscribe(this, (msg) => MareHubOnClosed(msg.Exception)); Mediator.Subscribe(this, async (msg) => await MareHubOnReconnected().ConfigureAwait(false)); Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); Mediator.Subscribe(this, (msg) => _ = CyclePause(msg.UserData)); ServerState = ServerState.Offline; if (_dalamudUtil.IsLoggedIn) { DalamudUtilOnLogIn(); } } public string AuthFailureMessage { get; private set; } = string.Empty; public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0); public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null; public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty; public bool IsConnected => ServerState == ServerState.Connected; public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0)); public int OnlineUsers => SystemInfoDto.OnlineUsers; public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo(); public ServerState ServerState { get => _serverState; private set { Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState); _serverState = value; } } public SystemInfoDto SystemInfoDto { get; private set; } = new(); public string UID => _connectionDto?.User.UID ?? string.Empty; public async Task CheckClientHealth() { return await _mareHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); } public async Task CreateConnections() { Logger.LogDebug("CreateConnections called"); if (_serverManager.CurrentServer?.FullPause ?? true) { Logger.LogInformation("Not recreating Connection, paused"); _connectionDto = null; await StopConnection(ServerState.Disconnected).ConfigureAwait(false); _connectionCancellationTokenSource.Cancel(); return; } var secretKey = _serverManager.GetSecretKey(); if (secretKey.IsNullOrEmpty()) { Logger.LogWarning("No secret key set for current character"); _connectionDto = null; await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false); _connectionCancellationTokenSource.Cancel(); return; } await StopConnection(ServerState.Disconnected).ConfigureAwait(false); Logger.LogInformation("Recreating Connection"); _connectionCancellationTokenSource.Cancel(); _connectionCancellationTokenSource = new CancellationTokenSource(); var token = _connectionCancellationTokenSource.Token; while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) { AuthFailureMessage = string.Empty; await StopConnection(ServerState.Disconnected).ConfigureAwait(false); ServerState = ServerState.Connecting; try { Logger.LogDebug("Building connection"); try { _lastUsedToken = await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false); } catch (MareAuthFailureException ex) { AuthFailureMessage = ex.Reason; throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized); } while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested) { Logger.LogDebug("Player not loaded in yet, waiting"); await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); } if (token.IsCancellationRequested) break; _mareHub = _hubFactory.GetOrCreate(token); InitializeApiHooks(); await _mareHub.StartAsync(token).ConfigureAwait(false); _connectionDto = await GetConnectionDto().ConfigureAwait(false); ServerState = ServerState.Connected; var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!; if (_connectionDto.ServerVersion != IMareHub.ApiVersion) { if (_connectionDto.CurrentClientVersion > currentClientVer) { Mediator.Publish(new NotificationMessage("Client incompatible", $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + $"This client version is incompatible and will not be able to connect. Please update your Mare Synchronos client.", Dalamud.Interface.Internal.Notifications.NotificationType.Error)); } await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); return; } if (_connectionDto.CurrentClientVersion > currentClientVer) { Mediator.Publish(new NotificationMessage("Client outdated", $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + $"Please keep your Mare Synchronos client up-to-date.", Dalamud.Interface.Internal.Notifications.NotificationType.Warning)); } await LoadIninitialPairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false); } catch (OperationCanceledException) { Logger.LogWarning("Connection attempt cancelled"); return; } catch (HttpRequestException ex) { Logger.LogWarning(ex, "HttpRequestException on Connection"); if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { await StopConnection(ServerState.Unauthorized).ConfigureAwait(false); return; } ServerState = ServerState.Reconnecting; Logger.LogInformation("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); } catch (Exception ex) { Logger.LogWarning(ex, "Exception on Connection"); Logger.LogInformation("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); } } } public Task CyclePause(UserData userData) { CancellationTokenSource cts = new(); cts.CancelAfter(TimeSpan.FromSeconds(5)); _ = Task.Run(async () => { var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); var perm = pair.UserPair!.OwnPermissions; perm.SetPaused(paused: true); await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); // wait until it's changed while (pair.UserPair!.OwnPermissions != perm) { await Task.Delay(250, cts.Token).ConfigureAwait(false); Logger.LogTrace("Waiting for permissions change for {data}", userData); } perm.SetPaused(paused: false); await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); }, cts.Token).ContinueWith((t) => cts.Dispose()); return Task.CompletedTask; } public async Task GetConnectionDto() { var dto = await _mareHub!.InvokeAsync(nameof(GetConnectionDto)).ConfigureAwait(false); Mediator.Publish(new ConnectedMessage(dto)); return dto; } protected override void Dispose(bool disposing) { base.Dispose(disposing); _healthCheckTokenSource?.Cancel(); _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); _connectionCancellationTokenSource?.Cancel(); } private async Task ClientHealthCheck(CancellationToken ct) { while (!ct.IsCancellationRequested && _mareHub != null) { await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); Logger.LogDebug("Checking Client Health State"); bool requireReconnect = await RefreshToken(ct).ConfigureAwait(false); if (requireReconnect) continue; _ = await CheckClientHealth().ConfigureAwait(false); } } private void DalamudUtilOnLogIn() { _ = Task.Run(() => CreateConnections()); } private void DalamudUtilOnLogOut() { _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); ServerState = ServerState.Offline; } private void InitializeApiHooks() { if (_mareHub == null) return; Logger.LogDebug("Initializing data"); OnDownloadReady((guid) => _ = Client_DownloadReady(guid)); OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg)); OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto)); OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto)); OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto)); OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto)); OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto)); OnUserSendOnline(dto => _ = Client_UserSendOnline(dto)); OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto)); OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); OnUserDefaultPermissionUpdate(dto => _ = Client_UserUpdateDefaultPermissions(dto)); OnUpdateUserIndividualPairStatusDto(dto => _ = Client_UpdateUserIndividualPairStatusDto(dto)); OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto)); OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto)); OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto)); OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto)); OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose(); _healthCheckTokenSource = new CancellationTokenSource(); _ = ClientHealthCheck(_healthCheckTokenSource.Token); _initialized = true; } private async Task LoadIninitialPairs() { foreach (var entry in await GroupsGetAll().ConfigureAwait(false)) { Logger.LogDebug("Group: {entry}", entry); _pairManager.AddGroup(entry); } foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) { Logger.LogDebug("Individual Pair: {userPair}", userPair); _pairManager.AddUserPair(userPair); } } private async Task LoadOnlinePairs() { foreach (var entry in await UserGetOnlinePairs().ConfigureAwait(false)) { Logger.LogDebug("Pair online: {pair}", entry); _pairManager.MarkPairOnline(entry, sendNotif: false); } } private void MareHubOnClosed(Exception? arg) { _healthCheckTokenSource?.Cancel(); Mediator.Publish(new DisconnectedMessage()); ServerState = ServerState.Offline; if (arg != null) { Logger.LogWarning(arg, "Connection closed"); } else { Logger.LogInformation("Connection closed"); } } private async Task MareHubOnReconnected() { ServerState = ServerState.Connecting; try { InitializeApiHooks(); _connectionDto = await GetConnectionDto().ConfigureAwait(false); if (_connectionDto.ServerVersion != IMareHub.ApiVersion) { await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); return; } await LoadIninitialPairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false); ServerState = ServerState.Connected; } catch (Exception ex) { Logger.LogCritical(ex, "Failure to obtain data after reconnection"); await StopConnection(ServerState.Disconnected).ConfigureAwait(false); } } private void MareHubOnReconnecting(Exception? arg) { _doNotNotifyOnNextInfo = true; _healthCheckTokenSource?.Cancel(); ServerState = ServerState.Reconnecting; Logger.LogWarning(arg, "Connection closed... Reconnecting"); } private async Task RefreshToken(CancellationToken ct) { Logger.LogDebug("Checking token"); bool requireReconnect = false; try { var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); if (!string.Equals(token, _lastUsedToken, StringComparison.Ordinal)) { Logger.LogDebug("Reconnecting due to updated token"); _doNotNotifyOnNextInfo = true; await CreateConnections().ConfigureAwait(false); requireReconnect = true; } } catch (MareAuthFailureException ex) { AuthFailureMessage = ex.Reason; await StopConnection(ServerState.Unauthorized).ConfigureAwait(false); requireReconnect = true; } catch (Exception ex) { Logger.LogWarning(ex, "Could not refresh token, forcing reconnect"); _doNotNotifyOnNextInfo = true; await CreateConnections().ConfigureAwait(false); requireReconnect = true; } return requireReconnect; } private async Task StopConnection(ServerState state) { ServerState = ServerState.Disconnecting; Logger.LogInformation("Stopping existing connection"); await _hubFactory.DisposeHubAsync().ConfigureAwait(false); if (_mareHub is not null) { _initialized = false; _healthCheckTokenSource?.Cancel(); Mediator.Publish(new DisconnectedMessage()); _mareHub = null; _connectionDto = null; } ServerState = state; } } #pragma warning restore MA0040