using Dalamud.Configuration; using Dalamud.Logging; using MareSynchronos.Models; using System; using System.Buffers.Text; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices.ComTypes; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.Types; using LZ4; using MareSynchronos.API; using MareSynchronos.FileCacheDB; using Microsoft.AspNetCore.SignalR.Client; namespace MareSynchronos.WebAPI { public class CharacterReceivedEventArgs : EventArgs { public CharacterCacheDto CharacterData { get; set; } public string CharacterNameHash { get; set; } } public class ApiController : IDisposable { private readonly Configuration pluginConfiguration; private const string MainService = "https://darkarchon.internet-box.ch:5001"; public string UID { get; private set; } = string.Empty; public string SecretKey => pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? pluginConfiguration.ClientSecret[ApiUri] : "-"; private string CacheFolder => pluginConfiguration.CacheFolder; public bool UseCustomService { get => pluginConfiguration.UseCustomService; set { pluginConfiguration.UseCustomService = value; pluginConfiguration.Save(); } } private string ApiUri => UseCustomService ? pluginConfiguration.ApiUri : MainService; public bool ServerAlive => (_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected; public bool IsConnected => !string.IsNullOrEmpty(UID); public event EventHandler? Connected; public event EventHandler? Disconnected; public event EventHandler? CharacterReceived; public event EventHandler? RemovedFromWhitelist; public event EventHandler? AddedToWhitelist; public List WhitelistEntries { get; set; } = new List(); readonly CancellationTokenSource cts; private HubConnection? _heartbeatHub; private IDisposable? _fileUploadRequest; private HubConnection? _fileHub; private HubConnection? _userHub; public ApiController(Configuration pluginConfiguration) { this.pluginConfiguration = pluginConfiguration; cts = new CancellationTokenSource(); _ = Heartbeat(); } public async Task Heartbeat() { while (!ServerAlive && !cts.Token.IsCancellationRequested) { try { PluginLog.Debug("Attempting to establish heartbeat connection to " + ApiUri); _heartbeatHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/heartbeat", options => { if (!string.IsNullOrEmpty(SecretKey)) { options.Headers.Add("Authorization", SecretKey); } #if DEBUG options.HttpMessageHandlerFactory = (message) => { if (message is HttpClientHandler clientHandler) clientHandler.ServerCertificateCustomValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; return message; }; #endif }).Build(); PluginLog.Debug("Heartbeat service built to: " + ApiUri); await _heartbeatHub.StartAsync(cts.Token); UID = await _heartbeatHub!.InvokeAsync("Heartbeat"); PluginLog.Debug("Heartbeat started: " + ApiUri); try { await InitializeHubConnections(); await LoadInitialData(); Connected?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { PluginLog.Error(ex, "Error during Heartbeat initialization"); } _heartbeatHub.Closed += OnHeartbeatHubOnClosed; _heartbeatHub.Reconnected += OnHeartbeatHubOnReconnected; PluginLog.Debug("Heartbeat established to: " + ApiUri); } catch (Exception ex) { PluginLog.Error(ex, "Creating heartbeat failure"); } } } private async Task LoadInitialData() { var whiteList = await _userHub!.InvokeAsync>("GetWhitelist"); WhitelistEntries = whiteList.ToList(); } public void RestartHeartbeat() { PluginLog.Debug("Restarting heartbeat"); _heartbeatHub!.Closed -= OnHeartbeatHubOnClosed; _heartbeatHub!.Reconnected -= OnHeartbeatHubOnReconnected; Task.Run(async () => { await _heartbeatHub.StopAsync(cts.Token); await _heartbeatHub.DisposeAsync(); _heartbeatHub = null!; _ = Heartbeat(); }); } private async Task OnHeartbeatHubOnReconnected(string? s) { PluginLog.Debug("Reconnected: " + ApiUri); UID = await _heartbeatHub!.InvokeAsync("Heartbeat"); } private Task OnHeartbeatHubOnClosed(Exception? exception) { PluginLog.Debug("Connection closed: " + ApiUri); Disconnected?.Invoke(null, EventArgs.Empty); RestartHeartbeat(); return Task.CompletedTask; } private async Task DisposeHubConnections() { if (_fileHub != null) { PluginLog.Debug("Disposing File Hub"); _fileUploadRequest?.Dispose(); await _fileHub!.StopAsync(); await _fileHub!.DisposeAsync(); } if (_userHub != null) { PluginLog.Debug("Disposing User Hub"); await _userHub.StopAsync(); await _userHub.DisposeAsync(); } } private async Task InitializeHubConnections() { await DisposeHubConnections(); PluginLog.Debug("Creating User Hub"); _userHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/user", options => { options.Headers.Add("Authorization", SecretKey); #if DEBUG options.HttpMessageHandlerFactory = (message) => { if (message is HttpClientHandler clientHandler) clientHandler.ServerCertificateCustomValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; return message; }; #endif }) .Build(); await _userHub.StartAsync(); _userHub.On("UpdateWhitelist", UpdateLocalWhitelist); _userHub.On("ReceiveCharacterData", ReceiveCharacterData); PluginLog.Debug("Creating File Hub"); _fileHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/files", options => { options.Headers.Add("Authorization", SecretKey); #if DEBUG options.HttpMessageHandlerFactory = (message) => { if (message is HttpClientHandler clientHandler) clientHandler.ServerCertificateCustomValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; return message; }; #endif }) .Build(); await _fileHub.StartAsync(cts.Token); _fileUploadRequest = _fileHub!.On("FileRequest", UploadFile); } private void UpdateLocalWhitelist(WhitelistDto dto, string characterIdentifier) { var entry = WhitelistEntries.SingleOrDefault(e => e.OtherUID == dto.OtherUID); if (entry == null) { RemovedFromWhitelist?.Invoke(characterIdentifier, EventArgs.Empty); return; } if ((entry.IsPausedFromOthers != dto.IsPausedFromOthers || entry.IsSynced != dto.IsSynced || entry.IsPaused != dto.IsPaused) && !dto.IsPaused && dto.IsSynced && !dto.IsPausedFromOthers) { AddedToWhitelist?.Invoke(characterIdentifier, EventArgs.Empty); } entry.IsPaused = dto.IsPaused; entry.IsPausedFromOthers = dto.IsPausedFromOthers; entry.IsSynced = dto.IsSynced; if (dto.IsPaused || dto.IsPausedFromOthers || !dto.IsSynced) { RemovedFromWhitelist?.Invoke(characterIdentifier, EventArgs.Empty); } } private async Task UploadFile(string fileHash) { PluginLog.Debug("Requested fileHash: " + fileHash); await using var db = new FileCacheContext(); var fileCache = db.FileCaches.First(f => f.Hash == fileHash); var compressedFile = LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath), 0, (int)new FileInfo(fileCache.Filepath).Length); var response = await _fileHub!.InvokeAsync("UploadFile", fileHash, compressedFile, cts.Token); PluginLog.Debug("Success: " + response); } public async Task Register() { if (!ServerAlive) return; PluginLog.Debug("Registering at service " + ApiUri); var response = await _userHub!.InvokeAsync("Register"); pluginConfiguration.ClientSecret[ApiUri] = response; pluginConfiguration.Save(); RestartHeartbeat(); } public async Task SendCharacterData(CharacterCacheDto character, List visibleCharacterIds) { if (!IsConnected || SecretKey == "-") return; PluginLog.Debug("Sending Character data to service " + ApiUri); await _fileHub!.InvokeAsync("SendFiles", character.FileReplacements, cts.Token); while (await _fileHub!.InvokeAsync("IsUploadFinished")) { await Task.Delay(TimeSpan.FromSeconds(0.5)); PluginLog.Debug("Waiting for uploads to finish"); } await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds); } public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash) { PluginLog.Debug("Received DTO for " + characterHash); CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs() { CharacterData = character, CharacterNameHash = characterHash }); return Task.CompletedTask; } public async Task DownloadData(string hash) { return await _fileHub!.InvokeAsync("DownloadFile", hash); } public async Task GetCharacterData(Dictionary hashedCharacterNames) { await _userHub!.InvokeAsync("GetCharacterData", hashedCharacterNames); } public async Task SendWhitelistPauseChange(string uid, bool paused) { if (!IsConnected || SecretKey == "-") return; await _userHub!.SendAsync("SendWhitelistPauseChange", uid, paused); } public async Task SendWhitelistAddition(string uid) { if (!IsConnected || SecretKey == "-") return; await _userHub!.SendAsync("SendWhitelistAddition", uid); } public async Task SendWhitelistRemoval(string uid) { if (!IsConnected || SecretKey == "-") return; await _userHub!.SendAsync("SendWhitelistRemoval", uid); } public async Task SendWhitelist() { if (!IsConnected || SecretKey == "-") return; await _userHub!.SendAsync("SendWhitelist", WhitelistEntries.ToList()); WhitelistEntries = (await _userHub!.InvokeAsync>("GetWhitelist")).ToList(); } public async Task> GetWhitelist() { PluginLog.Debug("Getting whitelist from service " + ApiUri); List whitelist = new(); return whitelist; } public void Dispose() { cts?.Cancel(); _ = DisposeHubConnections(); } public async Task SendCharacterName(string hashedName) { await _userHub!.SendAsync("SendCharacterNameHash", hashedName); } public async Task SendVisibilityData(List visibilities) { if (!IsConnected) return; await _userHub!.SendAsync("SendVisibilityList", visibilities); } } }