diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs new file mode 100644 index 0000000..17021ea --- /dev/null +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -0,0 +1,170 @@ +using Account; +using Dalamud.Logging; +using Grpc.Core; +using Grpc.Net.Client; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Pal.Client.Net +{ + internal partial class RemoteApi + { + private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) + { + if (Service.Configuration.Mode != Configuration.EMode.Online) + { + PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); + return (false, "You are not online."); + } + + if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) + { + Dispose(); + + PluginLog.Information("TryConnect: Creating new gRPC channel"); + _channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions + { + HttpHandler = new SocketsHttpHandler + { + ConnectTimeout = TimeSpan.FromSeconds(5), + SslOptions = GetSslClientAuthenticationOptions(), + }, + LoggerFactory = loggerFactory, + }); + + PluginLog.Information($"TryConnect: Connecting to upstream service at {RemoteUrl}"); + await _channel.ConnectAsync(cancellationToken); + } + + var accountClient = new AccountService.AccountServiceClient(_channel); + if (AccountId == null) + { + PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account"); + var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + if (createAccountReply.Success) + { + Account = new Configuration.AccountInfo + { + Id = Guid.Parse(createAccountReply.AccountId), + }; + PluginLog.Information($"TryConnect: Account created with id {PartialAccountId}"); + + Service.Configuration.Save(); + } + else + { + PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); + if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) + { + Service.Chat.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer."); + _warnedAboutUpgrade = true; + } + return (false, $"Could not create account ({createAccountReply.Error})."); + } + } + + if (AccountId == null) + { + PluginLog.Warning("TryConnect: No account id to login with"); + return (false, "No account-id after account was attempted to be created."); + } + + if (!_loginInfo.IsValid) + { + PluginLog.Information($"TryConnect: Logging in with account id {PartialAccountId}"); + LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = AccountId?.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + if (loginReply.Success) + { + PluginLog.Information($"TryConnect: Login successful with account id: {PartialAccountId}"); + _loginInfo = new LoginInfo(loginReply.AuthToken); + + var account = Account; + if (account != null) + { + account.CachedRoles = _loginInfo.Claims?.Roles?.ToList() ?? new List(); + Service.Configuration.Save(); + } + } + else + { + PluginLog.Error($"TryConnect: Login failed with error {loginReply.Error}"); + _loginInfo = new LoginInfo(null); + if (loginReply.Error == LoginError.InvalidAccountId) + { + Account = null; + Service.Configuration.Save(); + if (retry) + { + PluginLog.Information("TryConnect: Attempting connection retry without account id"); + return await TryConnect(cancellationToken, retry: false); + } + else + return (false, "Invalid account id."); + } + if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) + { + Service.Chat.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer."); + _warnedAboutUpgrade = true; + } + return (false, $"Could not log in ({loginReply.Error})."); + } + } + + if (!_loginInfo.IsValid) + { + PluginLog.Error($"TryConnect: Login state is loggedIn={_loginInfo.IsLoggedIn}, expired={_loginInfo.IsExpired}"); + return (false, "No login information available."); + } + + return (true, string.Empty); + } + + private async Task Connect(CancellationToken cancellationToken) + { + var result = await TryConnect(cancellationToken); + return result.Success; + } + + public async Task VerifyConnection(CancellationToken cancellationToken = default) + { + _warnedAboutUpgrade = false; + + var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory); + if (!connectionResult.Success) + return $"Could not connect to server: {connectionResult.Error}"; + + PluginLog.Information("VerifyConnection: Connection established, trying to verify auth token"); + var accountClient = new AccountService.AccountServiceClient(_channel); + await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + return "Connection successful."; + } + + internal class LoginInfo + { + public LoginInfo(string? authToken) + { + if (!string.IsNullOrEmpty(authToken)) + { + IsLoggedIn = true; + AuthToken = authToken; + Claims = JwtClaims.FromAuthToken(authToken!); + } + else + IsLoggedIn = false; + } + + public bool IsLoggedIn { get; } + public string? AuthToken { get; } + public JwtClaims? Claims { get; } + public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; + public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; + + public bool IsValid => IsLoggedIn && !IsExpired; + } + } +} diff --git a/Pal.Client/Net/RemoteApi.ExportService.cs b/Pal.Client/Net/RemoteApi.ExportService.cs new file mode 100644 index 0000000..09af73b --- /dev/null +++ b/Pal.Client/Net/RemoteApi.ExportService.cs @@ -0,0 +1,23 @@ +using Account; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Pal.Client.Net +{ + internal partial class RemoteApi + { + public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return new(false, new()); + + var exportClient = new ExportService.ExportServiceClient(_channel); + var exportReply = await exportClient.ExportAsync(new ExportRequest + { + ServerUrl = RemoteUrl, + }, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken); + return (exportReply.Success, exportReply.Data); + } + } +} diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs new file mode 100644 index 0000000..cd38321 --- /dev/null +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -0,0 +1,78 @@ +using Palace; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pal.Client.Net +{ + internal partial class RemoteApi + { + public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return (false, new()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); + return (downloadReply.Success, downloadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); + } + + public async Task<(bool, List)> UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + { + if (markers.Count == 0) + return (true, new()); + + if (!await Connect(cancellationToken)) + return (false, new()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var uploadRequest = new UploadFloorsRequest + { + TerritoryType = territoryType, + }; + uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject + { + Type = (ObjectType)m.Type, + X = m.Position.X, + Y = m.Position.Y, + Z = m.Position.Z + })); + var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); + return (uploadReply.Success, uploadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); + } + + public async Task MarkAsSeen(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + { + if (markers.Count == 0) + return true; + + if (!await Connect(cancellationToken)) + return false; + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; + foreach (var marker in markers) + seenRequest.NetworkIds.Add(marker.NetworkId.ToString()); + + var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + return seenReply.Success; + } + + private Marker CreateMarkerFromNetworkObject(PalaceObject obj) => + new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); + + public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return new(false, new List()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken); + return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList()); + } + } +} diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs new file mode 100644 index 0000000..f7a3a2f --- /dev/null +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -0,0 +1,63 @@ +using Dalamud.Logging; +using Grpc.Core; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Pal.Client.Net +{ + internal partial class RemoteApi + { + private Metadata UnauthorizedHeaders() => new Metadata + { + { "User-Agent", UserAgent }, + }; + + private Metadata AuthorizedHeaders() => new Metadata + { + { "Authorization", $"Bearer {_loginInfo?.AuthToken}" }, + { "User-Agent", UserAgent }, + }; + + private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions() + { +#if !DEBUG + var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets"); + if (secrets == null) + return null; + + var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string; + if (pass == null) + return null; + + var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx"); + if (manifestResourceStream == null) + return null; + + var bytes = new byte[manifestResourceStream.Length]; + manifestResourceStream.Read(bytes, 0, bytes.Length); + + var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet); + PluginLog.Debug($"Using client certificate {certificate.GetCertHashString()}"); + return new SslClientAuthenticationOptions + { + ClientCertificates = new X509CertificateCollection() + { + certificate, + }, + }; +#else + PluginLog.Debug("Not using client certificate"); + return null; +#endif + } + + public bool HasRoleOnCurrentServer(string role) + { + if (Service.Configuration.Mode != Configuration.EMode.Online) + return false; + + var account = Account; + return account == null || account.CachedRoles.Contains(role); + } + } +} diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 91f64ea..b436989 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -1,30 +1,16 @@ -using Account; -using Dalamud.Logging; -using Grpc.Core; +using Dalamud.Logging; using Grpc.Net.Client; using Microsoft.Extensions.Logging; -using Palace; using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Security; -using System.Numerics; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; namespace Pal.Client.Net { - internal class RemoteApi : IDisposable + internal partial class RemoteApi : IDisposable { #if DEBUG - private const string remoteUrl = "http://localhost:5145"; + public static string RemoteUrl { get; } = "http://localhost:5145"; #else - private const string remoteUrl = "https://pal.μ.tv"; + public static string RemoteUrl { get; } = "https://pal.μ.tv"; #endif private readonly string UserAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; @@ -36,13 +22,13 @@ namespace Pal.Client.Net public Configuration.AccountInfo? Account { - get => Service.Configuration.Accounts.TryGetValue(remoteUrl, out Configuration.AccountInfo? accountInfo) ? accountInfo : null; + get => Service.Configuration.Accounts.TryGetValue(RemoteUrl, out Configuration.AccountInfo? accountInfo) ? accountInfo : null; set { if (value != null) - Service.Configuration.Accounts[remoteUrl] = value; + Service.Configuration.Accounts[RemoteUrl] = value; else - Service.Configuration.Accounts.Remove(remoteUrl); + Service.Configuration.Accounts.Remove(RemoteUrl); } } @@ -51,283 +37,11 @@ namespace Pal.Client.Net private string PartialAccountId => Account?.Id?.ToString()?.PadRight(14).Substring(0, 13) ?? "[no account id]"; - private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true) - { - if (Service.Configuration.Mode != Configuration.EMode.Online) - { - PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection"); - return (false, "You are not online."); - } - - if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) - { - Dispose(); - - PluginLog.Information("TryConnect: Creating new gRPC channel"); - _channel = GrpcChannel.ForAddress(remoteUrl, new GrpcChannelOptions - { - HttpHandler = new SocketsHttpHandler - { - ConnectTimeout = TimeSpan.FromSeconds(5), - SslOptions = GetSslClientAuthenticationOptions(), - }, - LoggerFactory = loggerFactory, - }); - - PluginLog.Information($"TryConnect: Connecting to upstream service at {remoteUrl}"); - await _channel.ConnectAsync(cancellationToken); - } - - var accountClient = new AccountService.AccountServiceClient(_channel); - if (AccountId == null) - { - PluginLog.Information($"TryConnect: No account information saved for {remoteUrl}, creating new account"); - var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - if (createAccountReply.Success) - { - Account = new Configuration.AccountInfo - { - Id = Guid.Parse(createAccountReply.AccountId), - }; - PluginLog.Information($"TryConnect: Account created with id {PartialAccountId}"); - - Service.Configuration.Save(); - } - else - { - PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}"); - if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade) - { - Service.Chat.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer."); - _warnedAboutUpgrade = true; - } - return (false, $"Could not create account ({createAccountReply.Error})."); - } - } - - if (AccountId == null) - { - PluginLog.Warning("TryConnect: No account id to login with"); - return (false, "No account-id after account was attempted to be created."); - } - - if (!_loginInfo.IsValid) - { - PluginLog.Information($"TryConnect: Logging in with account id {PartialAccountId}"); - LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = AccountId?.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - if (loginReply.Success) - { - PluginLog.Information($"TryConnect: Login successful with account id: {PartialAccountId}"); - _loginInfo = new LoginInfo(loginReply.AuthToken); - - var account = Account; - if (account != null) - { - account.CachedRoles = _loginInfo.Claims?.Roles?.ToList() ?? new List(); - Service.Configuration.Save(); - } - } - else - { - PluginLog.Error($"TryConnect: Login failed with error {loginReply.Error}"); - _loginInfo = new LoginInfo(null); - if (loginReply.Error == LoginError.InvalidAccountId) - { - Account = null; - Service.Configuration.Save(); - if (retry) - { - PluginLog.Information("TryConnect: Attempting connection retry without account id"); - return await TryConnect(cancellationToken, retry: false); - } - else - return (false, "Invalid account id."); - } - if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) - { - Service.Chat.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer."); - _warnedAboutUpgrade = true; - } - return (false, $"Could not log in ({loginReply.Error})."); - } - } - - if (!_loginInfo.IsValid) - { - PluginLog.Error($"TryConnect: Login state is loggedIn={_loginInfo.IsLoggedIn}, expired={_loginInfo.IsExpired}"); - return (false, "No login information available."); - } - - return (true, string.Empty); - } - - private async Task Connect(CancellationToken cancellationToken) - { - var result = await TryConnect(cancellationToken); - return result.Success; - } - - public async Task VerifyConnection(CancellationToken cancellationToken = default) - { - _warnedAboutUpgrade = false; - - var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory); - if (!connectionResult.Success) - return $"Could not connect to server: {connectionResult.Error}"; - - PluginLog.Information("VerifyConnection: Connection established, trying to verify auth token"); - var accountClient = new AccountService.AccountServiceClient(_channel); - await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - return "Connection successful."; - } - - public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) - { - if (!await Connect(cancellationToken)) - return (false, new()); - - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return (downloadReply.Success, downloadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); - } - - public async Task<(bool, List)> UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) - { - if (markers.Count == 0) - return (true, new()); - - if (!await Connect(cancellationToken)) - return (false, new()); - - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var uploadRequest = new UploadFloorsRequest - { - TerritoryType = territoryType, - }; - uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject - { - Type = (ObjectType)m.Type, - X = m.Position.X, - Y = m.Position.Y, - Z = m.Position.Z - })); - var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return (uploadReply.Success, uploadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); - } - - public async Task MarkAsSeen(ushort territoryType, IList markers, CancellationToken cancellationToken = default) - { - if (markers.Count == 0) - return true; - - if (!await Connect(cancellationToken)) - return false; - - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; - foreach (var marker in markers) - seenRequest.NetworkIds.Add(marker.NetworkId.ToString()); - - var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); - return seenReply.Success; - } - - private Marker CreateMarkerFromNetworkObject(PalaceObject obj) => - new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); - - public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) - { - if (!await Connect(cancellationToken)) - return new(false, new List()); - - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken); - return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList()); - } - - private Metadata UnauthorizedHeaders() => new Metadata - { - { "User-Agent", UserAgent }, - }; - - private Metadata AuthorizedHeaders() => new Metadata - { - { "Authorization", $"Bearer {_loginInfo?.AuthToken}" }, - { "User-Agent", UserAgent }, - }; - - private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions() - { -#if !DEBUG - var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets"); - if (secrets == null) - return null; - - var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string; - if (pass == null) - return null; - - var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx"); - if (manifestResourceStream == null) - return null; - - var bytes = new byte[manifestResourceStream.Length]; - manifestResourceStream.Read(bytes, 0, bytes.Length); - - var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet); - PluginLog.Debug($"Using client certificate {certificate.GetCertHashString()}"); - return new SslClientAuthenticationOptions - { - ClientCertificates = new X509CertificateCollection() - { - certificate, - }, - }; -#else - PluginLog.Debug("Not using client certificate"); - return null; -#endif - } - - public bool HasRoleOnCurrentServer(string role) - { - if (Service.Configuration.Mode != Configuration.EMode.Online) - return false; - - var account = Account; - return account == null || account.CachedRoles.Contains(role); - } - public void Dispose() { PluginLog.Debug("Disposing gRPC channel"); _channel?.Dispose(); _channel = null; } - - internal class LoginInfo - { - public LoginInfo(string? authToken) - { - if (!string.IsNullOrEmpty(authToken)) - { - IsLoggedIn = true; - AuthToken = authToken; - Claims = JwtClaims.FromAuthToken(authToken!); - } - else - IsLoggedIn = false; - } - - public bool IsLoggedIn { get; } - public string? AuthToken { get; } - public JwtClaims? Claims { get; } - public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; - public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; - - public bool IsValid => IsLoggedIn && !IsExpired; - } - - } } diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index f7a8d5d..7415df0 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -49,6 +49,7 @@ + diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 3d8735c..19c70c5 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -1,12 +1,18 @@ -using Dalamud.Interface.Components; +using Account; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using Dalamud.Logging; using ECommons.Reflection; using ECommons.SplatoonAPI; +using Google.Protobuf; using ImGuiNET; +using Pal.Client.Net; using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using System.Reflection; @@ -32,6 +38,12 @@ namespace Pal.Client.Windows private string? _connectionText; private bool _switchToCommunityTab; + private string _openImportPath = string.Empty; + private string _saveExportPath = string.Empty; + private string? _openImportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + private FileDialogManager _importDialog; + private FileDialogManager _exportDialog; public ConfigWindow() : base("Palace Pal###PalPalaceConfig") { @@ -41,6 +53,9 @@ namespace Pal.Client.Windows SizeCondition = ImGuiCond.FirstUseEver; Position = new Vector2(300, 300); PositionCondition = ImGuiCond.FirstUseEver; + + _importDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _exportDialog = new FileDialogManager { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; } public override void OnOpen() @@ -59,6 +74,12 @@ namespace Pal.Client.Windows _connectionText = null; } + public override void OnClose() + { + _importDialog.Reset(); + _exportDialog.Reset(); + } + public override void Draw() { bool save = false; @@ -67,11 +88,15 @@ namespace Pal.Client.Windows { DrawTrapCofferTab(ref save, ref saveAndClose); DrawCommunityTab(ref saveAndClose); + //DrawImportTab(); + DrawExportTab(); DrawDebugTab(); ImGui.EndTabBar(); } + _importDialog.Draw(); + if (save || saveAndClose) { var config = Service.Configuration; @@ -169,6 +194,63 @@ namespace Pal.Client.Windows } } + private void DrawImportTab() + { + if (ImGui.BeginTabItem("Import")) + { + ImGui.Text("File to Import:"); + ImGui.SameLine(); + ImGui.InputTextWithHint("", "Path to *.pal file", ref _openImportPath, 260); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) + { + _importDialog.OpenFileDialog("Palace Pal - Import", "Palace Pal (*.pal) {*.pal}", (success, paths) => + { + if (success && paths.Count == 1) + { + _openImportPath = paths.First(); + } + }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); + _openImportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + } + ImGui.EndTabItem(); + } + } + + private void DrawExportTab() + { + if (Service.RemoteApi.HasRoleOnCurrentServer("export:run") && ImGui.BeginTabItem("Export")) + { + string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; + if (string.IsNullOrEmpty(_saveExportPath) && !string.IsNullOrEmpty(_saveExportDialogStartPath)) + _saveExportPath = Path.Join(_saveExportDialogStartPath, todaysFileName); + + ImGui.TextWrapped($"Export all markers from {RemoteApi.RemoteUrl}:"); + ImGui.Text("Save as:"); + ImGui.SameLine(); + ImGui.InputTextWithHint("", "Path to *.pal file", ref _saveExportPath, 260); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) + { + _importDialog.SaveFileDialog("Palace Pal - Export", "Palace Pal (*.pal) {*.pal}", todaysFileName, "pal", (success, path) => + { + if (success && !string.IsNullOrEmpty(path)) + { + _saveExportPath = path; + } + }, startPath: _saveExportDialogStartPath, isModal: false); + _saveExportDialogStartPath = null; // only use this once, FileDialogManager will save path between calls + } + + ImGui.BeginDisabled(string.IsNullOrEmpty(_saveExportPath) || File.Exists(_saveExportPath)); + if (ImGui.Button("Start Export")) + this.DoExport(_saveExportPath); + ImGui.EndDisabled(); + + ImGui.EndTabItem(); + } + } + private void DrawDebugTab() { if (ImGui.BeginTabItem("Debug")) @@ -227,10 +309,10 @@ namespace Pal.Client.Windows if (pos != null) { var elements = new List - { - Plugin.CreateSplatoonElement(Marker.EType.Trap, pos.Value, _trapColor), - Plugin.CreateSplatoonElement(Marker.EType.Hoard, pos.Value, _hoardColor), - }; + { + Plugin.CreateSplatoonElement(Marker.EType.Trap, pos.Value, _trapColor), + Plugin.CreateSplatoonElement(Marker.EType.Hoard, pos.Value, _hoardColor), + }; if (!Splatoon.AddDynamicElements("PalacePal.Test", elements.ToArray(), new long[] { Environment.TickCount64 + 10000 })) { @@ -299,5 +381,32 @@ namespace Pal.Client.Windows } }); } + + internal void DoExport(string destination) + { + Task.Run(async () => + { + try + { + (bool success, ExportRoot export) = await Service.RemoteApi.DoExport(); + if (success) + { + using var output = File.Create(destination); + export.WriteTo(output); + + Service.Chat.Print($"Export saved as {destination}."); + } + else + { + Service.Chat.PrintError("Export failed due to server error."); + } + } + catch (Exception e) + { + PluginLog.Error(e, "Export failed"); + Service.Chat.PrintError($"Export failed: {e}"); + } + }); + } } }