using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; using MareSynchronos.FileCache; using MareSynchronos.Utils; using MareSynchronos.WebAPI.Utils; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; namespace MareSynchronos.WebAPI; public delegate void SimpleStringDelegate(string str); public partial class ApiController : IDisposable { public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; public const string MainServiceUri = "wss://maresynchronos.com"; public readonly int[] SupportedServerVersions = { Api.Version }; private readonly Configuration _pluginConfiguration; private readonly DalamudUtil _dalamudUtil; private readonly FileCacheManager _fileDbManager; private CancellationTokenSource _connectionCancellationTokenSource; private HubConnection? _mareHub; private CancellationTokenSource? _uploadCancellationTokenSource = new(); private ConnectionDto? _connectionDto; public SystemInfoDto SystemInfoDto { get; private set; } = new(); public bool IsModerator => (_connectionDto?.IsAdmin ?? false) || (_connectionDto?.IsModerator ?? false); public bool IsAdmin => _connectionDto?.IsAdmin ?? false; public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil, FileCacheManager fileDbManager) { Logger.Verbose("Creating " + nameof(ApiController)); _pluginConfiguration = pluginConfiguration; _dalamudUtil = dalamudUtil; _fileDbManager = fileDbManager; _connectionCancellationTokenSource = new CancellationTokenSource(); _dalamudUtil.LogIn += DalamudUtilOnLogIn; _dalamudUtil.LogOut += DalamudUtilOnLogOut; ServerState = ServerState.Offline; _verifiedUploadedHashes = new(); if (_dalamudUtil.IsLoggedIn) { DalamudUtilOnLogIn(); } } private void DalamudUtilOnLogOut() { Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); ServerState = ServerState.Offline; } private void DalamudUtilOnLogIn() { Task.Run(CreateConnections); } public event EventHandler? CharacterReceived; public event VoidDelegate? Connected; public event VoidDelegate? Disconnected; public event SimpleStringDelegate? PairedClientOffline; public event SimpleStringDelegate? PairedClientOnline; public event VoidDelegate? DownloadStarted; public event VoidDelegate? DownloadFinished; public ConcurrentDictionary> CurrentDownloads { get; } = new(); public List CurrentUploads { get; } = new(); public List ForbiddenTransfers { get; } = new(); public List AdminBannedUsers { get; private set; } = new(); public List AdminForbiddenFiles { get; private set; } = new(); public bool IsConnected => ServerState == ServerState.Connected; public bool IsDownloading => CurrentDownloads.Count > 0; public bool IsUploading => CurrentUploads.Count > 0; public List PairedClients { get; set; } = new(); public List GroupPairedClients { get; set; } = new(); public List Groups { get; set; } = new(); public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : string.Empty; public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; public Dictionary ServerDictionary => new Dictionary() { { MainServiceUri, MainServer } } .Concat(_pluginConfiguration.CustomServerList) .ToDictionary(k => k.Key, k => k.Value); public string UID => _connectionDto?.UID ?? string.Empty; public string DisplayName => _connectionDto?.UID ?? string.Empty; private string ApiUri => _pluginConfiguration.ApiUri; public int OnlineUsers => SystemInfoDto.OnlineUsers; private ServerState _serverState; public ServerState ServerState { get => _serverState; private set { Logger.Debug($"New ServerState: {value}, prev ServerState: {_serverState}"); _serverState = value; } } public async Task CreateConnections() { Logger.Debug("CreateConnections called"); if (_pluginConfiguration.FullPause) { Logger.Info("Not recreating Connection, paused"); ServerState = ServerState.Disconnected; _connectionDto = null; await StopConnection(_connectionCancellationTokenSource.Token); return; } await StopConnection(_connectionCancellationTokenSource.Token); Logger.Info("Recreating Connection"); _connectionCancellationTokenSource.Cancel(); _connectionCancellationTokenSource = new CancellationTokenSource(); var token = _connectionCancellationTokenSource.Token; _verifiedUploadedHashes.Clear(); while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) { if (string.IsNullOrEmpty(SecretKey)) { await Task.Delay(TimeSpan.FromSeconds(2)); continue; } await StopConnection(token); try { Logger.Debug("Building connection"); while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) { Logger.Debug("Player not loaded in yet, waiting"); await Task.Delay(TimeSpan.FromSeconds(1), token); } if (token.IsCancellationRequested) break; _mareHub = BuildHubConnection(Api.Path); await _mareHub.StartAsync(token); _mareHub.On(Api.OnUpdateSystemInfo, (dto) => SystemInfoDto = dto); _connectionDto = await _mareHub.InvokeAsync(Api.InvokeHeartbeat, _dalamudUtil.PlayerNameHashed, token); ServerState = ServerState.Connected; if (_connectionDto.ServerVersion != Api.Version) { ServerState = ServerState.VersionMisMatch; await StopConnection(token); return; } if (ServerState is ServerState.Connected) // user is authorized && server is legit { await InitializeData(token); _mareHub.Closed += MareHubOnClosed; _mareHub.Reconnecting += MareHubOnReconnecting; _mareHub.Reconnected += MareHubOnReconnected; } } catch (HubException ex) { Logger.Warn(ex.GetType().ToString()); Logger.Warn(ex.Message); Logger.Warn(ex.StackTrace ?? string.Empty); ServerState = ServerState.RateLimited; await StopConnection(token); return; } catch (HttpRequestException ex) { Logger.Warn(ex.GetType().ToString()); Logger.Warn(ex.Message); Logger.Warn(ex.StackTrace ?? string.Empty); if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { ServerState = ServerState.Unauthorized; await StopConnection(token); return; } else { ServerState = ServerState.Offline; Logger.Info("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); } } catch (Exception ex) { Logger.Warn(ex.GetType().ToString()); Logger.Warn(ex.Message); Logger.Warn(ex.StackTrace ?? string.Empty); Logger.Info("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); } } } private Task MareHubOnReconnected(string? arg) { _ = Task.Run(CreateConnections); return Task.CompletedTask; } private async Task InitializeData(CancellationToken token) { if (_mareHub == null) return; Logger.Debug("Initializing data"); _mareHub.On(Api.OnUserUpdateClientPairs, UpdateLocalClientPairsCallback); _mareHub.On(Api.OnUserReceiveCharacterData, ReceiveCharacterDataCallback); _mareHub.On(Api.OnUserRemoveOnlinePairedPlayer, (s) => PairedClientOffline?.Invoke(s)); _mareHub.On(Api.OnUserAddOnlinePairedPlayer, (s) => PairedClientOnline?.Invoke(s)); _mareHub.On(Api.OnAdminForcedReconnect, UserForcedReconnectCallback); _mareHub.On(Api.OnGroupChange, GroupChangedCallback); _mareHub.On(Api.OnGroupUserChange, GroupPairChangedCallback); PairedClients = await _mareHub!.InvokeAsync>(Api.InvokeUserGetPairedClients, token); Groups = await GetGroups(); GroupPairedClients.Clear(); foreach (var group in Groups) { GroupPairedClients.AddRange(await GetUsersInGroup(group.GID)); } if (IsModerator) { AdminForbiddenFiles = await _mareHub.InvokeAsync>(Api.InvokeAdminGetForbiddenFiles, token); AdminBannedUsers = await _mareHub.InvokeAsync>(Api.InvokeAdminGetBannedUsers, token); _mareHub.On(Api.OnAdminUpdateOrAddBannedUser, UpdateOrAddBannedUserCallback); _mareHub.On(Api.OnAdminDeleteBannedUser, DeleteBannedUserCallback); _mareHub.On(Api.OnAdminUpdateOrAddForbiddenFile, UpdateOrAddForbiddenFileCallback); _mareHub.On(Api.OnAdminDeleteForbiddenFile, DeleteForbiddenFileCallback); } Connected?.Invoke(); } public void Dispose() { Logger.Verbose("Disposing " + nameof(ApiController)); _dalamudUtil.LogIn -= DalamudUtilOnLogIn; _dalamudUtil.LogOut -= DalamudUtilOnLogOut; Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); _connectionCancellationTokenSource?.Cancel(); } private HubConnection BuildHubConnection(string hubName) { return new HubConnectionBuilder() .WithUrl(ApiUri + hubName, options => { options.Headers.Add("Authorization", SecretKey); options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; }) .WithAutomaticReconnect(new ForeverRetryPolicy()) .ConfigureLogging(a => { a.ClearProviders().AddProvider(new DalamudLoggingProvider()); a.SetMinimumLevel(LogLevel.Warning); }) .Build(); } private Task MareHubOnClosed(Exception? arg) { CurrentUploads.Clear(); CurrentDownloads.Clear(); _uploadCancellationTokenSource?.Cancel(); Disconnected?.Invoke(); ServerState = ServerState.Offline; Logger.Info("Connection closed"); return Task.CompletedTask; } private Task MareHubOnReconnecting(Exception? arg) { ServerState = ServerState.Disconnected; Logger.Warn("Connection closed... Reconnecting"); Logger.Warn(arg?.Message ?? string.Empty); Logger.Warn(arg?.StackTrace ?? string.Empty); Disconnected?.Invoke(); ServerState = ServerState.Offline; return Task.CompletedTask; } private async Task StopConnection(CancellationToken token) { if (_mareHub is not null) { _uploadCancellationTokenSource?.Cancel(); Logger.Info("Stopping existing connection"); _mareHub.Closed -= MareHubOnClosed; _mareHub.Reconnecting -= MareHubOnReconnecting; _mareHub.Reconnected -= MareHubOnReconnected; await _mareHub.StopAsync(token); await _mareHub.DisposeAsync(); CurrentUploads.Clear(); CurrentDownloads.Clear(); _uploadCancellationTokenSource?.Cancel(); Disconnected?.Invoke(); _mareHub = null; } if (ServerState != ServerState.Disconnected) { while (ServerState != ServerState.Offline) { await Task.Delay(16); } } } }