diff --git a/MareSynchronos/Models/CachedPlayer.cs b/MareSynchronos/Models/CachedPlayer.cs index f5b6c16..8122a83 100644 --- a/MareSynchronos/Models/CachedPlayer.cs +++ b/MareSynchronos/Models/CachedPlayer.cs @@ -11,6 +11,7 @@ using MareSynchronos.FileCacheDB; using MareSynchronos.Managers; using MareSynchronos.Utils; using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Utils; namespace MareSynchronos.Models; diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 71f7bda..bfcfeca 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -61,7 +61,7 @@ namespace MareSynchronos _fileDialogManager = new FileDialogManager(); var uiSharedComponent = - new UiShared(_ipcManager, _apiController, _fileCacheManager, _fileDialogManager, _configuration); + new UiShared(_ipcManager, _apiController, _fileCacheManager, _fileDialogManager, _configuration, _dalamudUtil); _mainUi = new MainUi(_windowSystem, uiSharedComponent, _configuration, _apiController); _introUi = new IntroUi(_windowSystem, uiSharedComponent, _configuration, _fileCacheManager); diff --git a/MareSynchronos/UI/DownloadUi.cs b/MareSynchronos/UI/DownloadUi.cs index 34796b6..a5e013d 100644 --- a/MareSynchronos/UI/DownloadUi.cs +++ b/MareSynchronos/UI/DownloadUi.cs @@ -52,10 +52,10 @@ public class DownloadUi : Window, IDisposable if (_apiController.CurrentUploads.Any()) { - var doneUploads = _apiController.CurrentUploads.Count(c => c.Value.Item1 == c.Value.Item2); - var totalUploads = _apiController.CurrentUploads.Keys.Count; - var totalUploaded = _apiController.CurrentUploads.Sum(c => c.Value.Item1); - var totalToUpload = _apiController.CurrentUploads.Sum(c => c.Value.Item2); + var doneUploads = _apiController.CurrentUploads.Count(c => c.Total == c.Transferred); + var totalUploads = _apiController.CurrentUploads.Count; + var totalUploaded = _apiController.CurrentUploads.Sum(c => c.Transferred); + var totalToUpload = _apiController.CurrentUploads.Sum(c => c.Total); UiShared.DrawOutlinedFont(drawList, "▲", new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * 0.5)), UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); @@ -70,10 +70,10 @@ public class DownloadUi : Window, IDisposable if (_apiController.CurrentDownloads.Any()) { var multBase = _apiController.CurrentUploads.Any() ? 0 : 2; - var doneDownloads = _apiController.CurrentDownloads.Count(c => c.Value.Item1 == c.Value.Item2); - var totalDownloads = _apiController.CurrentDownloads.Keys.Count; - var totalDownloaded = _apiController.CurrentDownloads.Sum(c => c.Value.Item1); - var totalToDownload = _apiController.CurrentDownloads.Sum(c => c.Value.Item2); + var doneDownloads = _apiController.CurrentDownloads.Count(c => c.Total == c.Transferred); + var totalDownloads = _apiController.CurrentDownloads.Count; + var totalDownloaded = _apiController.CurrentDownloads.Sum(c => c.Transferred); + var totalToDownload = _apiController.CurrentDownloads.Sum(c => c.Total); UiShared.DrawOutlinedFont(drawList, "▼", new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * multBase + (yDistance * 0.5))), UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); diff --git a/MareSynchronos/UI/MainUi.cs b/MareSynchronos/UI/MainUi.cs index e24648a..746f90f 100644 --- a/MareSynchronos/UI/MainUi.cs +++ b/MareSynchronos/UI/MainUi.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Numerics; using System.Threading.Tasks; +using MareSynchronos.API; using MareSynchronos.Utils; namespace MareSynchronos.UI @@ -107,13 +108,274 @@ namespace MareSynchronos.UI DrawFileCacheSettings(); if (_apiController.IsConnected) DrawCurrentTransfers(); - DrawAdministration(_apiController.IsConnected); + DrawUserAdministration(_apiController.IsConnected); + if (_apiController.IsConnected && _apiController.IsModerator) + DrawAdministration(); + } + + private string _forbiddenFileHashEntry = string.Empty; + private string _forbiddenFileHashForbiddenBy = string.Empty; + private string _bannedUserHashEntry = string.Empty; + private string _bannedUserReasonEntry = string.Empty; + + private void DrawAdministration() + { + if (ImGui.TreeNode("Administrative Actions")) + { + if (ImGui.TreeNode("Forbidden Files Changes")) + { + if (ImGui.BeginTable("ForbiddenFilesTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("File Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Forbidden By", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var forbiddenFile in _apiController.ForbiddenFiles) + { + ImGui.TableNextColumn(); + + ImGui.Text(forbiddenFile.Hash); + ImGui.TableNextColumn(); + string by = forbiddenFile.ForbiddenBy; + if (ImGui.InputText("##forbiddenBy" + forbiddenFile.Hash, ref by, 255)) + { + forbiddenFile.ForbiddenBy = by; + } + + ImGui.TableNextColumn(); + if (_apiController.IsAdmin) + { + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button( + FontAwesomeIcon.Upload.ToIconString() + "##updateFile" + forbiddenFile.Hash)) + { + _ = _apiController.AddOrUpdateForbiddenFileEntry(forbiddenFile); + } + + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteFile" + + forbiddenFile.Hash)) + { + _ = _apiController.DeleteForbiddenFileEntry(forbiddenFile); + } + + ImGui.PopFont(); + } + + } + + if (_apiController.IsAdmin) + { + ImGui.TableNextColumn(); + ImGui.InputText("##addFileHash", ref _forbiddenFileHashEntry, 255); + ImGui.TableNextColumn(); + ImGui.InputText("##addForbiddenBy", ref _forbiddenFileHashForbiddenBy, 255); + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + { + _ = _apiController.AddOrUpdateForbiddenFileEntry(new ForbiddenFileDto() + { + ForbiddenBy = _forbiddenFileHashForbiddenBy, + Hash = _forbiddenFileHashEntry + }); + } + + ImGui.PopFont(); + ImGui.NextColumn(); + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Banned Users")) + { + if (ImGui.BeginTable("BannedUsersTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var bannedUser in _apiController.BannedUsers) + { + ImGui.TableNextColumn(); + ImGui.Text(bannedUser.CharacterHash); + + ImGui.TableNextColumn(); + string reason = bannedUser.Reason; + ImGuiInputTextFlags moderatorFlags = _apiController.IsModerator + ? ImGuiInputTextFlags.ReadOnly + : ImGuiInputTextFlags.None; + if (ImGui.InputText("##bannedReason" + bannedUser.CharacterHash, ref reason, 255, + moderatorFlags)) + { + bannedUser.Reason = reason; + } + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (_apiController.IsAdmin) + { + if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString() + "##updateUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(bannedUser); + } + + ImGui.SameLine(); + } + + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.DeleteBannedUserEntry(bannedUser); + } + + ImGui.PopFont(); + } + + ImGui.TableNextColumn(); + ImGui.InputText("##addUserHash", ref _bannedUserHashEntry, 255); + + ImGui.TableNextColumn(); + if (_apiController.IsAdmin) + { + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255); + } + else + { + _bannedUserReasonEntry = "Banned by " + _uiShared.PlayerName; + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255, + ImGuiInputTextFlags.ReadOnly); + } + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto() + { + CharacterHash = _forbiddenFileHashForbiddenBy, + Reason = _forbiddenFileHashEntry + }); + } + + ImGui.PopFont(); + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Online Users")) + { + if (ImGui.Button("Refresh Online Users")) + { + _ = _apiController.RefreshOnlineUsers(); + } + + if (ImGui.BeginTable("OnlineUsersTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 100); + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 300); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var onlineUser in _apiController.AdminOnlineUsers) + { + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + string icon = onlineUser.IsModerator + ? FontAwesomeIcon.ChessKing.ToIconString() + : onlineUser.IsAdmin + ? FontAwesomeIcon.Crown.ToIconString() + : FontAwesomeIcon.User.ToIconString(); + ImGui.Text(icon); + ImGui.PopFont(); + ImGui.SameLine(); + + ImGui.Text(onlineUser.UID); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyUID" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + string charNameHash = onlineUser.CharacterNameHash; + ImGui.InputText("##onlineUserHash" + onlineUser.CharacterNameHash, ref charNameHash, 255, + ImGuiInputTextFlags.ReadOnly); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyHash" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.SkullCrossbones.ToIconString() + "##onlineUserBan" + + onlineUser.CharacterNameHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto + { + CharacterHash = onlineUser.CharacterNameHash, + Reason = "Banned by " + _uiShared.PlayerName + }); + } + ImGui.SameLine(); + if (onlineUser.UID != _apiController.UID && _apiController.IsAdmin) + { + if (!onlineUser.IsModerator) + { + if (ImGui.Button(FontAwesomeIcon.ChessKing.ToIconString() + + "##onlineUserModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.PromoteToModerator(onlineUser.UID); + } + } + else + { + if (ImGui.Button(FontAwesomeIcon.User.ToIconString() + + "##onlineUserNonModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.DemoteFromModerator(onlineUser.UID); + } + } + } + + ImGui.PopFont(); + } + ImGui.EndTable(); + } + ImGui.TreePop(); + } + ImGui.TreePop(); + } } private bool _deleteFilesPopupModalShown = false; private bool _deleteAccountPopupModalShown = false; - private void DrawAdministration(bool serverAlive) + private void DrawUserAdministration(bool serverAlive) { if (ImGui.TreeNode( $"User Administration")) @@ -242,8 +504,8 @@ namespace MareSynchronos.UI if (ImGui.BeginTable("TransfersTable", 2)) { ImGui.TableSetupColumn( - $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Value.Item1))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Value.Item2))})"); - ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.Sum(a => a.Value.Item1))} / {UiShared.ByteToString(_apiController.CurrentDownloads.Sum(a => a.Value.Item2))})"); + $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Total))})"); + ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentDownloads.Sum(a => a.Total))})"); ImGui.TableHeadersRow(); @@ -254,16 +516,16 @@ namespace MareSynchronos.UI ImGui.TableSetupColumn("Uploaded"); ImGui.TableSetupColumn("Size"); ImGui.TableHeadersRow(); - foreach (var hash in _apiController.CurrentUploads.Keys) + foreach (var transfer in _apiController.CurrentUploads) { - var color = UiShared.UploadColor(_apiController.CurrentUploads[hash]); + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); ImGui.PushStyleColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); - ImGui.Text(hash); + ImGui.Text(transfer.Hash); ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(_apiController.CurrentUploads[hash].Item1)); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(_apiController.CurrentUploads[hash].Item2)); + ImGui.Text(UiShared.ByteToString(transfer.Total)); ImGui.PopStyleColor(); ImGui.TableNextRow(); } @@ -278,16 +540,16 @@ namespace MareSynchronos.UI ImGui.TableSetupColumn("Downloaded"); ImGui.TableSetupColumn("Size"); ImGui.TableHeadersRow(); - foreach (var hash in _apiController.CurrentDownloads.Keys) + foreach (var transfer in _apiController.CurrentDownloads) { - var color = UiShared.UploadColor(_apiController.CurrentDownloads[hash]); + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); ImGui.PushStyleColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); - ImGui.Text(hash); + ImGui.Text(transfer.Hash); ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(_apiController.CurrentDownloads[hash].Item1)); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(_apiController.CurrentDownloads[hash].Item2)); + ImGui.Text(UiShared.ByteToString(transfer.Total)); ImGui.PopStyleColor(); ImGui.TableNextRow(); } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index 5d160c2..47c26d6 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using MareSynchronos.Managers; +using MareSynchronos.Utils; using MareSynchronos.WebAPI; namespace MareSynchronos.UI @@ -19,16 +20,18 @@ namespace MareSynchronos.UI private readonly FileCacheManager _fileCacheManager; private readonly FileDialogManager _fileDialogManager; private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; public long FileCacheSize => _fileCacheManager.FileCacheSize; public bool ShowClientSecret = true; - - public UiShared(IpcManager ipcManager, ApiController apiController, FileCacheManager fileCacheManager, FileDialogManager fileDialogManager, Configuration pluginConfiguration) + public string PlayerName => _dalamudUtil.PlayerName; + public UiShared(IpcManager ipcManager, ApiController apiController, FileCacheManager fileCacheManager, FileDialogManager fileDialogManager, Configuration pluginConfiguration, DalamudUtil dalamudUtil) { _ipcManager = ipcManager; _apiController = apiController; _fileCacheManager = fileCacheManager; _fileDialogManager = fileDialogManager; _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; } public bool DrawOtherPluginState() diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs new file mode 100644 index 0000000..0b9fc1c --- /dev/null +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using LZ4; +using MareSynchronos.API; +using MareSynchronos.FileCacheDB; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI +{ + public partial class ApiController + { + public void CancelUpload() + { + if (_uploadCancellationTokenSource != null) + { + Logger.Warn("Cancelling upload"); + _uploadCancellationTokenSource?.Cancel(); + _fileHub!.InvokeAsync("AbortUpload"); + } + } + + public async Task DeleteAllMyFiles() + { + await _fileHub!.SendAsync("DeleteAllFiles"); + } + + public async Task DownloadFile(string hash, CancellationToken ct) + { + var reader = _fileHub!.StreamAsync("DownloadFileAsync", hash, ct); + string fileName = Path.GetTempFileName(); + await using var fs = File.OpenWrite(fileName); + await foreach (var data in reader.WithCancellation(ct)) + { + //Logger.Debug("Getting chunk of " + hash); + CurrentDownloads.Single(f => f.Hash == hash).Transferred += data.Length; + await fs.WriteAsync(data, ct); + Debug.WriteLine("Wrote chunk " + data.Length + " into " + fileName); + } + return fileName; + } + + public async Task DownloadFiles(List fileReplacementDto, CancellationToken ct) + { + IsDownloading = true; + + foreach (var file in fileReplacementDto) + { + var downloadFileDto = await _fileHub!.InvokeAsync("GetFileSize", file.Hash, ct); + CurrentDownloads.Add(new FileTransfer + { + Total = downloadFileDto.Size, + Hash = downloadFileDto.Hash + }); + } + + List downloadedHashes = new(); + foreach (var file in fileReplacementDto.Where(f => CurrentDownloads.Single(t => f.Hash == t.Hash).Transferred > 0)) + { + if (downloadedHashes.Contains(file.Hash)) + { + continue; + } + + var hash = file.Hash; + var tempFile = await DownloadFile(hash, ct); + if (ct.IsCancellationRequested) + { + File.Delete(tempFile); + break; + } + + var tempFileData = await File.ReadAllBytesAsync(tempFile, ct); + var extractedFile = LZ4Codec.Unwrap(tempFileData); + File.Delete(tempFile); + var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); + await File.WriteAllBytesAsync(filePath, extractedFile, ct); + var fi = new FileInfo(filePath); + Func RandomDayFunc() + { + DateTime start = new DateTime(1995, 1, 1); + Random gen = new Random(); + int range = (DateTime.Today - start).Days; + return () => start.AddDays(gen.Next(range)); + } + + fi.CreationTime = RandomDayFunc().Invoke(); + fi.LastAccessTime = RandomDayFunc().Invoke(); + fi.LastWriteTime = RandomDayFunc().Invoke(); + downloadedHashes.Add(hash); + } + + var allFilesInDb = false; + while (!allFilesInDb && !ct.IsCancellationRequested) + { + await using (var db = new FileCacheContext()) + { + allFilesInDb = downloadedHashes.All(h => db.FileCaches.Any(f => f.Hash == h)); + } + + await Task.Delay(250, ct); + } + + CurrentDownloads.Clear(); + IsDownloading = false; + } + + private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + await using var db = new FileCacheContext(); + var fileCache = db.FileCaches.First(f => f.Hash == fileHash); + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0, + (int)new FileInfo(fileCache.Filepath).Length)); + } + + private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) + { + if (uploadToken.IsCancellationRequested) return; + + async IAsyncEnumerable AsyncFileData() + { + var chunkSize = 1024 * 512; // 512kb + using var ms = new MemoryStream(compressedFile); + var buffer = new byte[chunkSize]; + int bytesRead; + while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, uploadToken)) > 0) + { + CurrentUploads.Single(f => f.Hash == fileHash).Transferred += bytesRead; + uploadToken.ThrowIfCancellationRequested(); + yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); + } + } + + await _fileHub!.SendAsync("UploadFileStreamAsync", fileHash, AsyncFileData(), uploadToken); + } + } + +} diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs new file mode 100644 index 0000000..ced2425 --- /dev/null +++ b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MareSynchronos.API; +using MareSynchronos.FileCacheDB; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI +{ + public partial class ApiController + { + public async Task DeleteAccount() + { + _pluginConfiguration.ClientSecret.Remove(ApiUri); + _pluginConfiguration.Save(); + await _fileHub!.SendAsync("DeleteAllFiles"); + await _userHub!.SendAsync("DeleteAccount"); + await CreateConnections(); + } + + public async Task GetCharacterData(Dictionary hashedCharacterNames) + { + await _userHub!.InvokeAsync("GetCharacterData", + hashedCharacterNames); + } + + public async Task Register() + { + if (!ServerAlive) return; + Logger.Debug("Registering at service " + ApiUri); + var response = await _userHub!.InvokeAsync("Register"); + _pluginConfiguration.ClientSecret[ApiUri] = response; + _pluginConfiguration.Save(); + ChangingServers?.Invoke(null, EventArgs.Empty); + await CreateConnections(); + } + + public async Task SendCharacterData(CharacterCacheDto character, List visibleCharacterIds) + { + if (!IsConnected || SecretKey == "-") return; + Logger.Debug("Sending Character data to service " + ApiUri); + + CancelUpload(); + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.Verbose("New Token Created"); + + var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); + + IsUploading = true; + + foreach (var file in filesToUpload.Where(f => f.IsForbidden == false)) + { + await using var db = new FileCacheContext(); + CurrentUploads.Add(new FileTransfer() + { + Hash = file.Hash, + Total = new FileInfo(db.FileCaches.First(f => f.Hash == file.Hash).Filepath).Length + }); + } + + Logger.Verbose("Compressing and uploading files"); + foreach (var file in filesToUpload) + { + Logger.Verbose("Compressing and uploading " + file); + var data = await GetCompressedFileData(file.Hash, uploadToken); + CurrentUploads.Single(e => e.Hash == data.Item1).Total = data.Item2.Length; + _ = UploadFile(data.Item2, file.Hash, uploadToken); + if (!uploadToken.IsCancellationRequested) continue; + Logger.Warn("Cancel in filesToUpload loop detected"); + CurrentUploads.Clear(); + break; + } + + Logger.Verbose("Upload tasks complete, waiting for server to confirm"); + var anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); + Logger.Verbose("Uploads open: " + anyUploadsOpen); + while (anyUploadsOpen && !uploadToken.IsCancellationRequested) + { + anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); + await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); + Logger.Verbose("Waiting for uploads to finish"); + } + + CurrentUploads.Clear(); + IsUploading = false; + + if (!uploadToken.IsCancellationRequested) + { + Logger.Verbose("=== Pushing character data ==="); + await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken); + } + else + { + Logger.Warn("=== Upload operation was cancelled ==="); + } + + Logger.Verbose("Upload complete for " + character.Hash); + _uploadCancellationTokenSource = null; + } + + public async Task> GetOnlineCharacters() + { + return await _userHub!.InvokeAsync>("GetOnlineCharacters"); + } + + public async Task SendPairedClientAddition(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientAddition", uid); + } + + public async Task SendPairedClientPauseChange(string uid, bool paused) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused); + } + + public async Task SendPairedClientRemoval(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientRemoval", uid); + } + } + +} diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs new file mode 100644 index 0000000..5167279 --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.Connectivity.cs @@ -0,0 +1,283 @@ +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.Utils; +using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI +{ + public partial class ApiController : IDisposable + { +#if DEBUG + public const string MainServer = "darkarchons Debug Server (Dev Server (CH))"; + public const string MainServiceUri = "wss://darkarchon.internet-box.ch:5001"; +#else + public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; + public const string MainServiceUri = "to be defined"; +#endif + + private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; + + private CancellationTokenSource _cts; + + private HubConnection? _fileHub; + + private HubConnection? _heartbeatHub; + + private HubConnection? _adminHub; + + private CancellationTokenSource? _uploadCancellationTokenSource; + + private HubConnection? _userHub; + private LoggedInUserDto? _loggedInUser; + public bool IsModerator => (_loggedInUser?.IsAdmin ?? false) || (_loggedInUser?.IsModerator ?? false); + + public bool IsAdmin => _loggedInUser?.IsAdmin ?? false; + + public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil) + { + Logger.Debug("Creating " + nameof(ApiController)); + + _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; + _cts = new CancellationTokenSource(); + _dalamudUtil.LogIn += DalamudUtilOnLogIn; + _dalamudUtil.LogOut += DalamudUtilOnLogOut; + + if (_dalamudUtil.IsLoggedIn) + { + DalamudUtilOnLogIn(); + } + } + + private void DalamudUtilOnLogOut() + { + Task.Run(async () => await StopAllConnections(_cts.Token)); + } + + private void DalamudUtilOnLogIn() + { + Task.Run(CreateConnections); + } + + + public event EventHandler? ChangingServers; + + public event EventHandler? CharacterReceived; + + public event EventHandler? Connected; + + public event EventHandler? Disconnected; + + public event EventHandler? PairedClientOffline; + + public event EventHandler? PairedClientOnline; + + public event EventHandler? PairedWithOther; + + public event EventHandler? UnpairedFromOther; + + public List CurrentDownloads { get; } = new(); + + public List CurrentUploads { get; } = new(); + + public List BannedUsers { get; private set; } = new(); + + public List ForbiddenFiles { get; private set; } = new(); + + public bool IsConnected => !string.IsNullOrEmpty(UID); + + public bool IsDownloading { get; private set; } + + public bool IsUploading { get; private set; } + + public List PairedClients { get; set; } = new(); + + public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-"; + + public bool ServerAlive => + (_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected; + + public Dictionary ServerDictionary => new Dictionary() { { MainServiceUri, MainServer } } + .Concat(_pluginConfiguration.CustomServerList) + .ToDictionary(k => k.Key, k => k.Value); + + public string UID => _loggedInUser?.UID ?? string.Empty; + + private string ApiUri => _pluginConfiguration.ApiUri; + public int OnlineUsers { get; private set; } + + public async Task CreateConnections() + { + await StopAllConnections(_cts.Token); + + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + while (!ServerAlive && !token.IsCancellationRequested) + { + await StopAllConnections(token); + + try + { + Logger.Debug("Building connection"); + _heartbeatHub = BuildHubConnection("heartbeat"); + _userHub = BuildHubConnection("user"); + _fileHub = BuildHubConnection("files"); + _adminHub = BuildHubConnection("admin"); + + await _heartbeatHub.StartAsync(token); + await _userHub.StartAsync(token); + await _fileHub.StartAsync(token); + await _adminHub.StartAsync(token); + + OnlineUsers = await _userHub.InvokeAsync("GetOnlineUsers", token); + + if (_pluginConfiguration.FullPause) + { + _loggedInUser = null; + return; + } + + _loggedInUser = await _heartbeatHub.InvokeAsync("Heartbeat", token); + if (!string.IsNullOrEmpty(UID) && !token.IsCancellationRequested) // user is authorized + { + Logger.Debug("Initializing data"); + _userHub.On("UpdateClientPairs", UpdateLocalClientPairsCallback); + _userHub.On("ReceiveCharacterData", ReceiveCharacterDataCallback); + _userHub.On("RemoveOnlinePairedPlayer", + (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty)); + _userHub.On("AddOnlinePairedPlayer", + (s) => PairedClientOnline?.Invoke(s, EventArgs.Empty)); + _userHub.On("UsersOnline", (count) => OnlineUsers = count); + _adminHub.On("ForcedReconnect", UserForcedReconnectCallback); + + PairedClients = await _userHub!.InvokeAsync>("GetPairedClients", token); + + _heartbeatHub.Closed += HeartbeatHubOnClosed; + _heartbeatHub.Reconnected += HeartbeatHubOnReconnected; + _heartbeatHub.Reconnecting += HeartbeatHubOnReconnecting; + + if (IsModerator) + { + ForbiddenFiles = await _adminHub.InvokeAsync>("GetForbiddenFiles", token); + BannedUsers = await _adminHub.InvokeAsync>("GetBannedUsers", token); + _adminHub.On("UpdateOrAddBannedUser", UpdateOrAddBannedUserCallback); + _adminHub.On("DeleteBannedUser", DeleteBannedUserCallback); + _adminHub.On("UpdateOrAddForbiddenFile", UpdateOrAddForbiddenFileCallback); + _adminHub.On("DeleteForbiddenFile", DeleteForbiddenFileCallback); + } + + Connected?.Invoke(this, EventArgs.Empty); + } + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + Logger.Debug("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(5), token); + } + } + } + + public void Dispose() + { + Logger.Debug("Disposing " + nameof(ApiController)); + + _dalamudUtil.LogIn -= DalamudUtilOnLogIn; + _dalamudUtil.LogOut -= DalamudUtilOnLogOut; + + Task.Run(async () => await StopAllConnections(_cts.Token)); + _cts?.Cancel(); + } + + private HubConnection BuildHubConnection(string hubName) + { + return new HubConnectionBuilder() + .WithUrl(ApiUri + "/" + hubName, options => + { + if (!string.IsNullOrEmpty(SecretKey) && !_pluginConfiguration.FullPause) + { + options.Headers.Add("Authorization", SecretKey); + options.Headers.Add("CharacterNameHash", _dalamudUtil.PlayerNameHashed); + } + + options.Transports = HttpTransportType.WebSockets; +#if DEBUG + options.HttpMessageHandlerFactory = (message) => new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; +#endif + }) + .WithAutomaticReconnect(new ForeverRetryPolicy()) + .Build(); + } + + private Task HeartbeatHubOnClosed(Exception? arg) + { + Logger.Debug("Connection closed"); + Disconnected?.Invoke(null, EventArgs.Empty); + return Task.CompletedTask; + } + + private Task HeartbeatHubOnReconnected(string? arg) + { + Logger.Debug("Connection restored"); + OnlineUsers = _userHub!.InvokeAsync("GetOnlineUsers").Result; + _loggedInUser = _heartbeatHub!.InvokeAsync("Heartbeat").Result; + Connected?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } + + private Task HeartbeatHubOnReconnecting(Exception? arg) + { + Logger.Debug("Connection closed... Reconnecting…"); + Disconnected?.Invoke(null, EventArgs.Empty); + return Task.CompletedTask; + } + + private async Task StopAllConnections(CancellationToken token) + { + if (_heartbeatHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) + { + await _heartbeatHub.StopAsync(token); + _heartbeatHub.Closed -= HeartbeatHubOnClosed; + _heartbeatHub.Reconnected -= HeartbeatHubOnReconnected; + _heartbeatHub.Reconnecting += HeartbeatHubOnReconnecting; + await _heartbeatHub.DisposeAsync(); + _heartbeatHub = null; + } + + if (_fileHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) + { + await _fileHub.StopAsync(token); + await _fileHub.DisposeAsync(); + _fileHub = null; + } + + if (_userHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) + { + await _userHub.StopAsync(token); + await _userHub.DisposeAsync(); + _userHub = null; + } + + if (_adminHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) + { + await _adminHub.StopAsync(token); + await _adminHub.DisposeAsync(); + _adminHub = null; + } + } + } +} diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs new file mode 100644 index 0000000..218c695 --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MareSynchronos.API; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI +{ + public partial class ApiController + { + public async Task AddOrUpdateForbiddenFileEntry(ForbiddenFileDto forbiddenFile) + { + await _adminHub!.SendAsync("UpdateOrAddForbiddenFile", forbiddenFile); + } + + public async Task DeleteForbiddenFileEntry(ForbiddenFileDto forbiddenFile) + { + await _adminHub!.SendAsync("DeleteForbiddenFile", forbiddenFile); + } + + public async Task AddOrUpdateBannedUserEntry(BannedUserDto bannedUser) + { + await _adminHub!.SendAsync("UpdateOrAddBannedUser", bannedUser); + } + + public async Task DeleteBannedUserEntry(BannedUserDto bannedUser) + { + await _adminHub!.SendAsync("DeleteBannedUser", bannedUser); + } + + public async Task RefreshOnlineUsers() + { + AdminOnlineUsers = await _adminHub!.InvokeAsync>("GetOnlineUsers"); + } + + public List AdminOnlineUsers { get; set; } = new List(); + + public void PromoteToModerator(string onlineUserUID) + { + _adminHub!.SendAsync("ChangeModeratorStatus", onlineUserUID, true); + } + + public void DemoteFromModerator(string onlineUserUID) + { + _adminHub!.SendAsync("ChangeModeratorStatus", onlineUserUID, false); + } + } +} diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs new file mode 100644 index 0000000..b9d1900 --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MareSynchronos.API; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Utils; + +namespace MareSynchronos.WebAPI +{ + public partial class ApiController + { + private void UserForcedReconnectCallback() + { + _ = CreateConnections(); + } + + private void UpdateLocalClientPairsCallback(ClientPairDto dto, string characterIdentifier) + { + var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); + if (dto.IsRemoved) + { + PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); + UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); + return; + } + if (entry == null) + { + PairedClients.Add(dto); + return; + } + + if ((entry.IsPausedFromOthers != dto.IsPausedFromOthers || entry.IsSynced != dto.IsSynced || entry.IsPaused != dto.IsPaused) + && !dto.IsPaused && dto.IsSynced && !dto.IsPausedFromOthers) + { + PairedWithOther?.Invoke(characterIdentifier, EventArgs.Empty); + } + + entry.IsPaused = dto.IsPaused; + entry.IsPausedFromOthers = dto.IsPausedFromOthers; + entry.IsSynced = dto.IsSynced; + + if (dto.IsPaused || dto.IsPausedFromOthers || !dto.IsSynced) + { + UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); + } + } + + private Task ReceiveCharacterDataCallback(CharacterCacheDto character, string characterHash) + { + Logger.Verbose("Received DTO for " + characterHash); + CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); + return Task.CompletedTask; + } + + private void UpdateOrAddBannedUserCallback(BannedUserDto obj) + { + var user = BannedUsers.SingleOrDefault(b => b.CharacterHash == obj.CharacterHash); + if (user == null) + { + BannedUsers.Add(obj); + } + else + { + user.Reason = obj.Reason; + } + } + + private void DeleteBannedUserCallback(BannedUserDto obj) + { + BannedUsers.RemoveAll(a => a.CharacterHash == obj.CharacterHash); + } + + private void UpdateOrAddForbiddenFileCallback(ForbiddenFileDto obj) + { + var user = ForbiddenFiles.SingleOrDefault(b => b.Hash == obj.Hash); + if (user == null) + { + ForbiddenFiles.Add(obj); + } + else + { + user.ForbiddenBy = obj.ForbiddenBy; + } + } + + private void DeleteForbiddenFileCallback(ForbiddenFileDto obj) + { + ForbiddenFiles.RemoveAll(f => f.Hash == obj.Hash); + } + } +} diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs deleted file mode 100644 index 22334e1..0000000 --- a/MareSynchronos/WebAPI/ApiController.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using LZ4; -using MareSynchronos.API; -using MareSynchronos.FileCacheDB; -using MareSynchronos.Utils; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.SignalR.Client; - -namespace MareSynchronos.WebAPI -{ - public partial class ApiController : IDisposable - { -#if DEBUG - public const string MainServer = "darkarchons Debug Server (Dev Server (CH))"; - public const string MainServiceUri = "wss://darkarchon.internet-box.ch:5001"; -#else - public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; - public const string MainServiceUri = "to be defined"; -#endif - - private readonly Configuration _pluginConfiguration; - private readonly DalamudUtil _dalamudUtil; - - private CancellationTokenSource _cts; - - private HubConnection? _fileHub; - - private HubConnection? _heartbeatHub; - - private CancellationTokenSource? _uploadCancellationTokenSource; - - private HubConnection? _userHub; - public bool IsAdmin { private set; get; } - - public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil) - { - Logger.Debug("Creating " + nameof(ApiController)); - - _pluginConfiguration = pluginConfiguration; - _dalamudUtil = dalamudUtil; - _cts = new CancellationTokenSource(); - _dalamudUtil.LogIn += DalamudUtilOnLogIn; - _dalamudUtil.LogOut += DalamudUtilOnLogOut; - - if (_dalamudUtil.IsLoggedIn) - { - DalamudUtilOnLogIn(); - } - } - - private void DalamudUtilOnLogOut() - { - Task.Run(async () => await StopAllConnections(_cts.Token)); - } - - private void DalamudUtilOnLogIn() - { - Task.Run(CreateConnections); - } - - - public event EventHandler? ChangingServers; - - public event EventHandler? CharacterReceived; - - public event EventHandler? Connected; - - public event EventHandler? Disconnected; - - public event EventHandler? PairedClientOffline; - - public event EventHandler? PairedClientOnline; - - public event EventHandler? PairedWithOther; - - public event EventHandler? UnpairedFromOther; - - public ConcurrentDictionary CurrentDownloads { get; } = new(); - - public ConcurrentDictionary CurrentUploads { get; } = new(); - - public bool IsConnected => !string.IsNullOrEmpty(UID); - - public bool IsDownloading { get; private set; } - - public bool IsUploading { get; private set; } - - public List PairedClients { get; set; } = new(); - - public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-"; - - public bool ServerAlive => - (_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected; - - public Dictionary ServerDictionary => new Dictionary() { { MainServiceUri, MainServer } } - .Concat(_pluginConfiguration.CustomServerList) - .ToDictionary(k => k.Key, k => k.Value); - public string UID { get; private set; } = string.Empty; - - private string ApiUri => _pluginConfiguration.ApiUri; - public int OnlineUsers { get; private set; } - - public async Task CreateConnections() - { - await StopAllConnections(_cts.Token); - - _cts = new CancellationTokenSource(); - var token = _cts.Token; - - while (!ServerAlive && !token.IsCancellationRequested) - { - await StopAllConnections(token); - - try - { - Logger.Debug("Building connection"); - _heartbeatHub = BuildHubConnection("heartbeat"); - _userHub = BuildHubConnection("user"); - _fileHub = BuildHubConnection("files"); - - await _heartbeatHub.StartAsync(token); - await _userHub.StartAsync(token); - await _fileHub.StartAsync(token); - - OnlineUsers = await _userHub.InvokeAsync("GetOnlineUsers", token); - - if (_pluginConfiguration.FullPause) - { - UID = string.Empty; - return; - } - - var userDto = await _heartbeatHub.InvokeAsync("Heartbeat", token); - UID = userDto.UID; - IsAdmin = userDto.IsAdmin; - if (!string.IsNullOrEmpty(UID) && !token.IsCancellationRequested) // user is authorized - { - Logger.Debug("Initializing data"); - _userHub.On("UpdateClientPairs", UpdateLocalClientPairs); - _userHub.On("ReceiveCharacterData", ReceiveCharacterData); - _userHub.On("RemoveOnlinePairedPlayer", - (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty)); - _userHub.On("AddOnlinePairedPlayer", - (s) => PairedClientOnline?.Invoke(s, EventArgs.Empty)); - _userHub.On("UsersOnline", (count) => OnlineUsers = count); - - PairedClients = await _userHub!.InvokeAsync>("GetPairedClients", token); - - _heartbeatHub.Closed += HeartbeatHubOnClosed; - _heartbeatHub.Reconnected += HeartbeatHubOnReconnected; - _heartbeatHub.Reconnecting += HeartbeatHubOnReconnecting; - Connected?.Invoke(this, EventArgs.Empty); - } - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace); - Logger.Debug("Failed to establish connection, retrying"); - await Task.Delay(TimeSpan.FromSeconds(5), token); - } - } - } - - public void Dispose() - { - Logger.Debug("Disposing " + nameof(ApiController)); - - _dalamudUtil.LogIn -= DalamudUtilOnLogIn; - _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - - Task.Run(async () => await StopAllConnections(_cts.Token)); - _cts?.Cancel(); - } - - private HubConnection BuildHubConnection(string hubName) - { - return new HubConnectionBuilder() - .WithUrl(ApiUri + "/" + hubName, options => - { - if (!string.IsNullOrEmpty(SecretKey) && !_pluginConfiguration.FullPause) - { - options.Headers.Add("Authorization", SecretKey); - options.Headers.Add("CharacterNameHash", _dalamudUtil.PlayerNameHashed); - } - - options.Transports = HttpTransportType.WebSockets; -#if DEBUG - options.HttpMessageHandlerFactory = (message) => new HttpClientHandler() - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; -#endif - }) - .WithAutomaticReconnect(new ForeverRetryPolicy()) - .Build(); - } - - private Task HeartbeatHubOnClosed(Exception? arg) - { - Logger.Debug("Connection closed"); - Disconnected?.Invoke(null, EventArgs.Empty); - return Task.CompletedTask; - } - - private Task HeartbeatHubOnReconnected(string? arg) - { - Logger.Debug("Connection restored"); - OnlineUsers = _userHub!.InvokeAsync("GetOnlineUsers").Result; - var userDto = _heartbeatHub!.InvokeAsync("Heartbeat").Result; - IsAdmin = userDto.IsAdmin; - UID = userDto.UID; - Connected?.Invoke(this, EventArgs.Empty); - return Task.CompletedTask; - } - - private Task HeartbeatHubOnReconnecting(Exception? arg) - { - Logger.Debug("Connection closed... Reconnecting…"); - Disconnected?.Invoke(null, EventArgs.Empty); - return Task.CompletedTask; - } - - private async Task StopAllConnections(CancellationToken token) - { - if (_heartbeatHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) - { - await _heartbeatHub.StopAsync(token); - _heartbeatHub.Closed -= HeartbeatHubOnClosed; - _heartbeatHub.Reconnected -= HeartbeatHubOnReconnected; - _heartbeatHub.Reconnecting += HeartbeatHubOnReconnecting; - await _heartbeatHub.DisposeAsync(); - _heartbeatHub = null; - } - - if (_fileHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) - { - await _fileHub.StopAsync(token); - await _fileHub.DisposeAsync(); - _fileHub = null; - } - - if (_userHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) - { - await _userHub.StopAsync(token); - await _userHub.DisposeAsync(); - _userHub = null; - } - } - } - - public partial class ApiController - { - public void CancelUpload() - { - if (_uploadCancellationTokenSource != null) - { - Logger.Warn("Cancelling upload"); - _uploadCancellationTokenSource?.Cancel(); - _fileHub!.InvokeAsync("AbortUpload"); - } - } - - public async Task DeleteAccount() - { - _pluginConfiguration.ClientSecret.Remove(ApiUri); - _pluginConfiguration.Save(); - await _fileHub!.SendAsync("DeleteAllFiles"); - await _userHub!.SendAsync("DeleteAccount"); - await CreateConnections(); - } - - public async Task DeleteAllMyFiles() - { - await _fileHub!.SendAsync("DeleteAllFiles"); - } - - public async Task DownloadFile(string hash, CancellationToken ct) - { - var reader = _fileHub!.StreamAsync("DownloadFileAsync", hash, ct); - string fileName = Path.GetTempFileName(); - await using var fs = File.OpenWrite(fileName); - await foreach (var data in reader.WithCancellation(ct)) - { - //Logger.Debug("Getting chunk of " + hash); - CurrentDownloads[hash] = (CurrentDownloads[hash].Item1 + data.Length, CurrentDownloads[hash].Item2); - await fs.WriteAsync(data, ct); - Debug.WriteLine("Wrote chunk " + data.Length + " into " + fileName); - } - return fileName; - } - - public async Task DownloadFiles(List fileReplacementDto, CancellationToken ct) - { - IsDownloading = true; - - foreach (var file in fileReplacementDto) - { - var downloadFileDto = await _fileHub!.InvokeAsync("GetFileSize", file.Hash, ct); - CurrentDownloads[file.Hash] = (0, downloadFileDto.Size); - } - - List downloadedHashes = new(); - foreach (var file in fileReplacementDto.Where(f => CurrentDownloads[f.Hash].Item2 > 0)) - { - if (downloadedHashes.Contains(file.Hash)) - { - continue; - } - - var hash = file.Hash; - var tempFile = await DownloadFile(hash, ct); - if (ct.IsCancellationRequested) - { - File.Delete(tempFile); - break; - } - - var tempFileData = await File.ReadAllBytesAsync(tempFile, ct); - var extractedFile = LZ4Codec.Unwrap(tempFileData); - File.Delete(tempFile); - var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); - await File.WriteAllBytesAsync(filePath, extractedFile, ct); - var fi = new FileInfo(filePath); - Func RandomDayFunc() - { - DateTime start = new DateTime(1995, 1, 1); - Random gen = new Random(); - int range = (DateTime.Today - start).Days; - return () => start.AddDays(gen.Next(range)); - } - - fi.CreationTime = RandomDayFunc().Invoke(); - fi.LastAccessTime = RandomDayFunc().Invoke(); - fi.LastWriteTime = RandomDayFunc().Invoke(); - downloadedHashes.Add(hash); - } - - var allFilesInDb = false; - while (!allFilesInDb && !ct.IsCancellationRequested) - { - await using (var db = new FileCacheContext()) - { - allFilesInDb = downloadedHashes.All(h => db.FileCaches.Any(f => f.Hash == h)); - } - - await Task.Delay(250, ct); - } - - CurrentDownloads.Clear(); - IsDownloading = false; - } - - public async Task GetCharacterData(Dictionary hashedCharacterNames) - { - await _userHub!.InvokeAsync("GetCharacterData", - hashedCharacterNames); - } - - public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash) - { - Logger.Verbose("Received DTO for " + characterHash); - CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); - return Task.CompletedTask; - } - - public async Task Register() - { - if (!ServerAlive) return; - Logger.Debug("Registering at service " + ApiUri); - var response = await _userHub!.InvokeAsync("Register"); - _pluginConfiguration.ClientSecret[ApiUri] = response; - _pluginConfiguration.Save(); - ChangingServers?.Invoke(null, EventArgs.Empty); - await CreateConnections(); - } - public async Task SendCharacterData(CharacterCacheDto character, List visibleCharacterIds) - { - if (!IsConnected || SecretKey == "-") return; - Logger.Debug("Sending Character data to service " + ApiUri); - - CancelUpload(); - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; - Logger.Verbose("New Token Created"); - - var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); - - IsUploading = true; - - foreach (var file in filesToUpload.Where(f => f.IsForbidden == false)) - { - await using var db = new FileCacheContext(); - CurrentUploads[file.Hash] = (0, new FileInfo(db.FileCaches.First(f => f.Hash == file.Hash).Filepath).Length); - } - - Logger.Verbose("Compressing and uploading files"); - foreach (var file in filesToUpload) - { - Logger.Verbose("Compressing and uploading " + file); - var data = await GetCompressedFileData(file.Hash, uploadToken); - CurrentUploads[data.Item1] = (0, data.Item2.Length); - _ = UploadFile(data.Item2, file.Hash, uploadToken); - if (!uploadToken.IsCancellationRequested) continue; - Logger.Warn("Cancel in filesToUpload loop detected"); - CurrentUploads.Clear(); - break; - } - - Logger.Verbose("Upload tasks complete, waiting for server to confirm"); - var anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); - Logger.Verbose("Uploads open: " + anyUploadsOpen); - while (anyUploadsOpen && !uploadToken.IsCancellationRequested) - { - anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); - await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); - Logger.Verbose("Waiting for uploads to finish"); - } - - CurrentUploads.Clear(); - IsUploading = false; - - if (!uploadToken.IsCancellationRequested) - { - Logger.Verbose("=== Pushing character data ==="); - await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken); - } - else - { - Logger.Warn("=== Upload operation was cancelled ==="); - } - - Logger.Verbose("Upload complete for " + character.Hash); - _uploadCancellationTokenSource = null; - } - - public async Task> GetOnlineCharacters() - { - return await _userHub!.InvokeAsync>("GetOnlineCharacters"); - } - - public async Task SendPairedClientAddition(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientAddition", uid); - } - - public async Task SendPairedClientPauseChange(string uid, bool paused) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused); - } - - public async Task SendPairedClientRemoval(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientRemoval", uid); - } - - private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - await using var db = new FileCacheContext(); - var fileCache = db.FileCaches.First(f => f.Hash == fileHash); - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0, - (int)new FileInfo(fileCache.Filepath).Length)); - } - - private void UpdateLocalClientPairs(ClientPairDto dto, string characterIdentifier) - { - var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); - if (dto.IsRemoved) - { - PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); - UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); - return; - } - if (entry == null) - { - PairedClients.Add(dto); - return; - } - - if ((entry.IsPausedFromOthers != dto.IsPausedFromOthers || entry.IsSynced != dto.IsSynced || entry.IsPaused != dto.IsPaused) - && !dto.IsPaused && dto.IsSynced && !dto.IsPausedFromOthers) - { - PairedWithOther?.Invoke(characterIdentifier, EventArgs.Empty); - } - - entry.IsPaused = dto.IsPaused; - entry.IsPausedFromOthers = dto.IsPausedFromOthers; - entry.IsSynced = dto.IsSynced; - - if (dto.IsPaused || dto.IsPausedFromOthers || !dto.IsSynced) - { - UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); - } - } - - private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) - { - if (uploadToken.IsCancellationRequested) return; - - async IAsyncEnumerable AsyncFileData() - { - var chunkSize = 1024 * 512; // 512kb - using var ms = new MemoryStream(compressedFile); - var buffer = new byte[chunkSize]; - int bytesRead; - while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, uploadToken)) > 0) - { - CurrentUploads[fileHash] = (CurrentUploads[fileHash].Item1 + bytesRead, CurrentUploads[fileHash].Item2); - uploadToken.ThrowIfCancellationRequested(); - yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); - } - } - - await _fileHub!.SendAsync("UploadFileStreamAsync", fileHash, AsyncFileData(), uploadToken); - } - } - - public class CharacterReceivedEventArgs : EventArgs - { - public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData) - { - CharacterData = characterData; - CharacterNameHash = characterNameHash; - } - - public CharacterCacheDto CharacterData { get; set; } - public string CharacterNameHash { get; set; } - } - - public class ForeverRetryPolicy : IRetryPolicy - { - public TimeSpan? NextRetryDelay(RetryContext retryContext) - { - return TimeSpan.FromSeconds(5); - } - } -} diff --git a/MareSynchronos/WebAPI/Utils/CharacterReceivedEventArgs.cs b/MareSynchronos/WebAPI/Utils/CharacterReceivedEventArgs.cs new file mode 100644 index 0000000..a4aedeb --- /dev/null +++ b/MareSynchronos/WebAPI/Utils/CharacterReceivedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using MareSynchronos.API; + +namespace MareSynchronos.WebAPI.Utils; + +public class CharacterReceivedEventArgs : EventArgs +{ + public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData) + { + CharacterData = characterData; + CharacterNameHash = characterNameHash; + } + + public CharacterCacheDto CharacterData { get; set; } + public string CharacterNameHash { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Utils/FileTransfer.cs b/MareSynchronos/WebAPI/Utils/FileTransfer.cs new file mode 100644 index 0000000..8a3c0b9 --- /dev/null +++ b/MareSynchronos/WebAPI/Utils/FileTransfer.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.WebAPI.Utils; + +public class FileTransfer +{ + public long Transferred { get; set; } = 0; + public long Total { get; set; } = 0; + public string Hash { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs b/MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs new file mode 100644 index 0000000..98eddbc --- /dev/null +++ b/MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs @@ -0,0 +1,12 @@ +using System; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI.Utils; + +public class ForeverRetryPolicy : IRetryPolicy +{ + public TimeSpan? NextRetryDelay(RetryContext retryContext) + { + return TimeSpan.FromSeconds(5); + } +} \ No newline at end of file