add oauth2 to client

This commit is contained in:
Stanley Dimant
2024-10-29 13:05:33 +01:00
parent 6f9347917d
commit ed427de0ab
19 changed files with 1060 additions and 539 deletions

Submodule MareAPI updated: 4e939e8cd8...040add0608

View File

@@ -3,7 +3,6 @@ using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using NReco.Logging.File;
namespace MareSynchronos.Interop;

View File

@@ -10,7 +10,7 @@ public class ServerConfig : IMareConfiguration
public List<ServerStorage> ServerStorage { get; set; } = new()
{
{ new ServerStorage() { ServerName = ApiController.MainServer, ServerUri = ApiController.MainServiceUri } },
{ new ServerStorage() { ServerName = ApiController.MainServer, ServerUri = ApiController.MainServiceUri, UseOAuth2 = true } },
};
public bool SendCensusData { get; set; } = false;

View File

@@ -6,4 +6,5 @@ public record Authentication
public string CharacterName { get; set; } = string.Empty;
public uint WorldId { get; set; } = 0;
public int SecretKeyIdx { get; set; } = -1;
public string? UID { get; set; }
}

View File

@@ -8,4 +8,6 @@ public class ServerStorage
public Dictionary<int, SecretKey> SecretKeys { get; set; } = [];
public string ServerName { get; set; } = string.Empty;
public string ServerUri { get; set; } = string.Empty;
public bool UseOAuth2 { get; set; } = false;
public string? OAuthToken { get; set; } = null;
}

View File

@@ -1,6 +1,5 @@
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.PlayerData.Services;
using MareSynchronos.Services;

View File

@@ -1,5 +1,4 @@
using MareSynchronos.API.Dto.User;
using MareSynchronos.FileCache;
using MareSynchronos.FileCache;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;

View File

@@ -79,7 +79,7 @@ public sealed class CommandManagerService : IDisposable
{
_serverConfigurationManager.CurrentServer.FullPause = fullPause;
_serverConfigurationManager.Save();
_ = _apiController.CreateConnections();
_ = _apiController.CreateConnectionsAsync();
}
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))

View File

@@ -1,9 +1,15 @@
using MareSynchronos.MareConfiguration;
using Dalamud.Utility;
using MareSynchronos.API.Routes;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
namespace MareSynchronos.Services.ServerConfiguration;
@@ -76,6 +82,45 @@ public class ServerConfigurationManager
}
}
public (string OAuthToken, string UID)? GetOAuth2(out bool hasMulti, int serverIdx = -1)
{
ServerStorage? currentServer;
currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
if (currentServer == null)
{
currentServer = new();
Save();
}
hasMulti = false;
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
if (auth.Count >= 2)
{
_logger.LogTrace("GetSecretKey accessed, returning null because multiple ({count}) identical characters.", auth.Count);
hasMulti = true;
return null;
}
if (auth.Count == 0)
{
_logger.LogTrace("GetSecretKey accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId);
return null;
}
if (!string.IsNullOrEmpty(auth.Single().UID) && !string.IsNullOrEmpty(currentServer.OAuthToken))
{
_logger.LogTrace("GetSecretKey accessed, returning {key} ({keyValue}) for {chara} on {world}", auth.Single().UID, string.Join("", currentServer.OAuthToken.Take(10)), charaName, worldId);
return (currentServer.OAuthToken, auth.Single().UID!);
}
_logger.LogTrace("GetSecretKey accessed, returning null because no UID found for {chara} on {world} or OAuthToken is not configured.", charaName, worldId);
return null;
}
public string? GetSecretKey(out bool hasMulti, int serverIdx = -1)
{
ServerStorage? currentServer;
@@ -145,6 +190,14 @@ public class ServerConfigurationManager
}
}
public string GetDiscordUserFromToken(ServerStorage server)
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (server.OAuthToken == null) return string.Empty;
var token = handler.ReadJwtToken(server.OAuthToken);
return token.Claims.First(f => string.Equals(f.Type, "discord_user", StringComparison.Ordinal)).Value!;
}
public string[] GetServerNames()
{
return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray();
@@ -152,7 +205,7 @@ public class ServerConfigurationManager
public bool HasValidConfig()
{
return CurrentServer != null;
return CurrentServer != null && CurrentServer.Authentications.Count > 0;
}
public void Save()
@@ -181,7 +234,7 @@ public class ServerConfigurationManager
{
CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(),
WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(),
SecretKeyIdx = server.SecretKeys.Last().Key,
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
});
Save();
}
@@ -391,7 +444,7 @@ public class ServerConfigurationManager
{
if (_configService.Current.ServerStorage.Count == 0 || !string.Equals(_configService.Current.ServerStorage[0].ServerUri, ApiController.MainServiceUri, StringComparison.OrdinalIgnoreCase))
{
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer });
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer, UseOAuth2 = true });
}
Save();
}
@@ -411,4 +464,67 @@ public class ServerConfigurationManager
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
public async Task<Dictionary<string, string>> GetUIDsWithDiscordToken(string serverUri, string token)
{
using HttpClient client = new HttpClient();
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetUIDs(new Uri(baseUri));
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await client.GetAsync(oauthCheckUri).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure getting UIDs");
return [];
}
}
public async Task<Uri?> CheckDiscordOAuth(string serverUri)
{
using HttpClient client = new HttpClient();
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetDiscordOAuthEndpoint(new Uri(baseUri));
var response = await client.GetFromJsonAsync<Uri?>(oauthCheckUri).ConfigureAwait(false);
return response;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure checking for Discord Auth");
return null;
}
}
public async Task<string?> GetDiscordOAuthToken(Uri discordAuthUri, string serverUri, CancellationToken token)
{
var sessionId = BitConverter.ToString(RandomNumberGenerator.GetBytes(64)).Replace("-", "").ToLower();
Util.OpenLink(discordAuthUri.ToString() + "?sessionId=" + sessionId);
string? discordToken = null;
using HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(60);
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetDiscordOAuthToken(new Uri(baseUri), sessionId);
var response = await client.GetAsync(oauthCheckUri, token).ConfigureAwait(false);
discordToken = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure getting Discord Token");
return null;
}
if (discordToken == null)
return null;
return discordToken;
}
}

View File

@@ -260,7 +260,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_serverManager.Save();
_ = _apiController.CreateConnections();
_ = _apiController.CreateConnectionsAsync();
}
_uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
@@ -345,7 +345,7 @@ public class CompactUi : WindowMediatorSubscriberBase
{
_serverManager.CurrentServer.FullPause = !_serverManager.CurrentServer.FullPause;
_serverManager.Save();
_ = _apiController.CreateConnections();
_ = _apiController.CreateConnectionsAsync();
}
}
@@ -446,8 +446,18 @@ public class CompactUi : WindowMediatorSubscriberBase
{
DrawAddCharacter();
}
if (_apiController.ServerState is ServerState.OAuthLoginTokenStale)
{
DrawRenewOAuth2();
}
}
}
private void DrawRenewOAuth2()
{
ImGuiHelpers.ScaledDummy(10f);
// add some text and a button to restart discord authentication
}
private IEnumerable<IDrawFolder> GetDrawFolders()
{
@@ -599,6 +609,8 @@ public class CompactUi : WindowMediatorSubscriberBase
ServerState.Connected => string.Empty,
ServerState.NoSecretKey => "You have no secret key set for this current character. Use the button below or open the settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.",
ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.",
ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and functioning and a UID assigned to your current character.",
ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.",
_ => string.Empty
};
}
@@ -618,6 +630,8 @@ public class CompactUi : WindowMediatorSubscriberBase
ServerState.RateLimited => ImGuiColors.DalamudYellow,
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
ServerState.MultiChara => ImGuiColors.DalamudYellow,
ServerState.OAuthMisconfigured => ImGuiColors.DalamudRed,
ServerState.OAuthLoginTokenStale => ImGuiColors.DalamudRed,
_ => ImGuiColors.DalamudRed
};
}
@@ -636,6 +650,8 @@ public class CompactUi : WindowMediatorSubscriberBase
ServerState.RateLimited => "Rate Limited",
ServerState.NoSecretKey => "No Secret Key",
ServerState.MultiChara => "Duplicate Characters",
ServerState.OAuthMisconfigured => "Misconfigured OAuth2",
ServerState.OAuthLoginTokenStale => "Stale OAuth2",
ServerState.Connected => _apiController.DisplayName,
_ => string.Empty
};

View File

@@ -93,7 +93,7 @@ public class DrawUserPair
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
{
_ = _apiController.CyclePause(_pair.UserData);
_ = _apiController.CyclePauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
ImGui.Separator();

View File

@@ -1,5 +1,6 @@
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using ImGuiNET;
using MareSynchronos.FileCache;
@@ -30,6 +31,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
private string _timeoutLabel = string.Empty;
private Task? _timeoutTask;
private string[]? _tosParagraphs;
private bool _useLegacyLogin = false;
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService,
CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator,
@@ -60,6 +62,8 @@ public partial class IntroUi : WindowMediatorSubscriberBase
});
}
private int _prevIdx = -1;
protected override void DrawInternal()
{
if (_uiShared.IsInGpose) return;
@@ -216,8 +220,23 @@ public partial class IntroUi : WindowMediatorSubscriberBase
UiSharedService.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below.");
_ = _uiShared.DrawServiceSelection(selectOnChange: true);
var serverIdx = _uiShared.DrawServiceSelection(selectOnChange: true, showConnect: false);
if (serverIdx != _prevIdx)
{
_uiShared.RestOAuthTasksState();
_prevIdx = serverIdx;
}
var selectedServer = _serverConfigurationManager.GetServerByIndex(serverIdx);
_useLegacyLogin = !selectedServer.UseOAuth2;
if (ImGui.Checkbox("Use Legacy Login with Secret Key", ref _useLegacyLogin))
{
_serverConfigurationManager.GetServerByIndex(serverIdx).UseOAuth2 = !_useLegacyLogin;
_serverConfigurationManager.Save();
}
if (_useLegacyLogin)
{
var text = "Enter Secret Key";
var buttonText = "Save";
var buttonWidth = _secretKey.Length != 64 ? 0 : ImGuiHelpers.GetButtonSize(buttonText).X + ImGui.GetStyle().ItemSpacing.X;
@@ -259,7 +278,53 @@ public partial class IntroUi : WindowMediatorSubscriberBase
};
}
_secretKey = string.Empty;
_ = Task.Run(() => _uiShared.ApiController.CreateConnections());
_ = Task.Run(() => _uiShared.ApiController.CreateConnectionsAsync());
}
}
}
else
{
if (selectedServer.OAuthToken == null)
{
UiSharedService.TextWrapped("Press the button below to verify the server has OAuth2 capabilities. Afterwards, authenticate using Discord in the Browser window.");
_uiShared.DrawOAuth(selectedServer);
}
else
{
UiSharedService.ColorTextWrapped($"OAuth2 is enabled, linked to: Discord User {_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)}", ImGuiColors.HealerGreen);
UiSharedService.TextWrapped("Now press the update UIDs button to get a list of all of your UIDs on the server.");
_uiShared.DrawUpdateOAuthUIDsButton(selectedServer);
var playerName = _dalamudUtilService.GetPlayerName();
var playerWorld = _dalamudUtilService.GetHomeWorldId();
UiSharedService.TextWrapped($"Once pressed, select the UID you want to use for your current character {_dalamudUtilService.GetPlayerName()}. If no UIDs are visible, make sure you are connected to the correct Discord account. " +
$"If that is not the case, use the unlink button below (hold CTRL to unlink).");
_uiShared.DrawUnlinkOAuthButton(selectedServer);
var auth = selectedServer.Authentications.Find(a => string.Equals(a.CharacterName, playerName, StringComparison.Ordinal) && a.WorldId == playerWorld);
if (auth == null)
{
selectedServer.Authentications.Add(new Authentication()
{
CharacterName = playerName,
WorldId = playerWorld
});
_serverConfigurationManager.Save();
}
if (auth != null)
{
_uiShared.DrawUIDComboForAuthentication(0, auth, selectedServer.ServerUri);
using (ImRaii.Disabled(string.IsNullOrEmpty(auth.UID)))
{
if (_uiShared.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Link, "Connect to Service"))
{
_ = Task.Run(() => _uiShared.ApiController.CreateConnectionsAsync());
}
}
if (string.IsNullOrEmpty(auth.UID))
UiSharedService.AttachToolTip("Select a UID to be able to connect to the service");
}
}
}
}

View File

@@ -31,36 +31,37 @@ namespace MareSynchronos.UI;
public class SettingsUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly IpcManager _ipcManager;
private readonly CacheMonitor _cacheMonitor;
private readonly DalamudUtilService _dalamudUtilService;
private readonly MareConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileCacheManager _fileCacheManager;
private readonly FileCompactor _fileCompactor;
private readonly FileUploadManager _fileTransferManager;
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly FileCacheManager _fileCacheManager;
private readonly IpcManager _ipcManager;
private readonly MareCharaFileManager _mareCharaFileManager;
private readonly PairManager _pairManager;
private readonly PerformanceCollectorService _performanceCollector;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared;
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private (int, int, FileCacheEntity) _currentProgress;
private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false;
private string _exportDescription = string.Empty;
private Task? _exportTask;
private string _lastTab = string.Empty;
private bool? _notesSuccessfullyApplied = null;
private bool _overwriteExistingLabels = false;
private bool _readClearCache = false;
private bool _readExport = false;
private bool _wasOpen = false;
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private Task<List<FileCacheEntity>>? _validationTask;
private int _selectedEntry = -1;
private string _uidToAddForIgnore = string.Empty;
private CancellationTokenSource? _validationCts;
private (int, int, FileCacheEntity) _currentProgress;
private Task? _exportTask;
private Task<List<FileCacheEntity>>? _validationTask;
private bool _wasOpen = false;
public SettingsUi(ILogger<SettingsUi> logger,
UiSharedService uiShared, MareConfigService configService,
MareCharaFileManager mareCharaFileManager, PairManager pairManager,
@@ -111,11 +112,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
public CharacterData? LastCreatedCharacterData { private get; set; }
private ApiController ApiController => _uiShared.ApiController;
protected override void DrawInternal()
public override void OnOpen()
{
_ = _uiShared.DrawOtherPluginState();
DrawSettingsContent();
_uiShared.RestOAuthTasksState();
}
public override void OnClose()
@@ -126,6 +125,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
base.OnClose();
}
protected override void DrawInternal()
{
_ = _uiShared.DrawOtherPluginState();
DrawSettingsContent();
}
private static bool InputDtrColors(string label, ref DtrEntry.Colors colors)
{
using var id = ImRaii.PushId(label);
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X;
var foregroundColor = ConvertColor(colors.Foreground);
var glowColor = ConvertColor(colors.Glow);
var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color");
ImGui.SameLine(0.0f, innerSpacing);
ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color");
ImGui.SameLine(0.0f, innerSpacing);
ImGui.TextUnformatted(label);
if (ret)
colors = new(ConvertBackColor(foregroundColor), ConvertBackColor(glowColor));
return ret;
static Vector3 ConvertColor(uint color)
=> unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f));
static uint ConvertBackColor(Vector3 color)
=> byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16);
}
private void DrawBlockedTransfers()
{
_lastTab = "BlockedTransfers";
@@ -975,410 +1011,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note.");
}
private static bool InputDtrColors(string label, ref DtrEntry.Colors colors)
{
using var id = ImRaii.PushId(label);
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X;
var foregroundColor = ConvertColor(colors.Foreground);
var glowColor = ConvertColor(colors.Glow);
var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color");
ImGui.SameLine(0.0f, innerSpacing);
ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color");
ImGui.SameLine(0.0f, innerSpacing);
ImGui.TextUnformatted(label);
if (ret)
colors = new(ConvertBackColor(foregroundColor), ConvertBackColor(glowColor));
return ret;
static Vector3 ConvertColor(uint color)
=> unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f));
static uint ConvertBackColor(Vector3 color)
=> byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16);
}
private void DrawServerConfiguration()
{
_lastTab = "Service Settings";
if (ApiController.ServerAlive)
{
_uiShared.BigText("Service Actions");
ImGuiHelpers.ScaledDummy(new Vector2(5, 5));
if (ImGui.Button("Delete all my files"))
{
_deleteFilesPopupModalShown = true;
ImGui.OpenPopup("Delete all your files?");
}
_uiShared.DrawHelpText("Completely deletes all your uploaded files on the service.");
if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"All your own uploaded files on the service will be deleted.\nThis operation cannot be undone.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0)))
{
_ = Task.Run(_fileTransferManager.DeleteAllFiles);
_deleteFilesPopupModalShown = false;
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteFilesPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
ImGui.SameLine();
if (ImGui.Button("Delete account"))
{
_deleteAccountPopupModalShown = true;
ImGui.OpenPopup("Delete your account?");
}
_uiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service.");
if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"Your account and all associated files and data on the service will be deleted.");
UiSharedService.TextWrapped("Your UID will be removed from all pairing lists.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete account", new Vector2(buttonSize, 0)))
{
_ = Task.Run(ApiController.UserDelete);
_deleteAccountPopupModalShown = false;
Mediator.Publish(new SwitchToIntroUiMessage());
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteAccountPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
ImGui.Separator();
}
_uiShared.BigText("Service & Character Settings");
ImGuiHelpers.ScaledDummy(new Vector2(5, 5));
var sendCensus = _serverConfigurationManager.SendCensusData;
if (ImGui.Checkbox("Send Statistical Census Data", ref sendCensus))
{
_serverConfigurationManager.SendCensusData = sendCensus;
}
_uiShared.DrawHelpText("This will allow sending census data to the currently connected service." + UiSharedService.TooltipSeparator
+ "Census data contains:" + Environment.NewLine
+ "- Current World" + Environment.NewLine
+ "- Current Gender" + Environment.NewLine
+ "- Current Race" + Environment.NewLine
+ "- Current Clan (this is not your Free Company, this is e.g. Keeper or Seeker for Miqo'te)" + UiSharedService.TooltipSeparator
+ "The census data is only saved temporarily and will be removed from the server on disconnect. It is stored temporarily associated with your UID while you are connected." + UiSharedService.TooltipSeparator
+ "If you do not wish to participate in the statistical census, untick this box and reconnect to the server.");
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var idx = _uiShared.DrawServiceSelection();
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var selectedServer = _serverConfigurationManager.GetServerByIndex(idx);
if (selectedServer == _serverConfigurationManager.CurrentServer)
{
UiSharedService.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", ImGuiColors.DalamudYellow);
}
if (ImGui.BeginTabBar("serverTabBar"))
{
if (ImGui.BeginTabItem("Character Management"))
{
if (selectedServer.SecretKeys.Any())
{
UiSharedService.ColorTextWrapped("Characters listed here will automatically connect to the selected Mare service with the settings as provided below." +
" Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", ImGuiColors.DalamudYellow);
int i = 0;
foreach (var item in selectedServer.Authentications.ToList())
{
using var charaId = ImRaii.PushId("selectedChara" + i);
var worldIdx = (ushort)item.WorldId;
var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value);
if (!data.TryGetValue(worldIdx, out string? worldPreview))
{
worldPreview = data.First().Value;
}
var secretKeyIdx = item.SecretKeyIdx;
var keys = selectedServer.SecretKeys;
if (!keys.TryGetValue(secretKeyIdx, out var secretKey))
{
secretKey = new();
}
var friendlyName = secretKey.FriendlyName;
bool thisIsYou = false;
if (string.Equals(_dalamudUtilService.GetPlayerName(), item.CharacterName, StringComparison.OrdinalIgnoreCase)
&& _dalamudUtilService.GetWorldId() == worldIdx)
{
thisIsYou = true;
}
if (ImGui.TreeNode($"chara", (thisIsYou ? "[CURRENT] " : "") + $"Character: {item.CharacterName}, World: {worldPreview}, Secret Key: {friendlyName}"))
{
var charaName = item.CharacterName;
if (ImGui.InputText("Character Name", ref charaName, 64))
{
item.CharacterName = charaName;
_serverConfigurationManager.Save();
}
_uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value,
(w) =>
{
if (item.WorldId != w.Key)
{
item.WorldId = w.Key;
_serverConfigurationManager.Save();
}
}, EqualityComparer<KeyValuePair<ushort, string>>.Default.Equals(data.FirstOrDefault(f => f.Key == worldIdx), default) ? data.First() : data.First(f => f.Key == worldIdx));
_uiShared.DrawCombo("Secret Key##" + item.CharacterName + i, keys, (w) => w.Value.FriendlyName,
(w) =>
{
if (w.Key != item.SecretKeyIdx)
{
item.SecretKeyIdx = w.Key;
_serverConfigurationManager.Save();
}
}, EqualityComparer<KeyValuePair<int, SecretKey>>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx));
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed())
_serverConfigurationManager.RemoveCharacterFromServer(idx, item);
UiSharedService.AttachToolTip("Hold CTRL to delete this entry.");
ImGui.TreePop();
}
i++;
}
ImGui.Separator();
if (!selectedServer.Authentications.Exists(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal)
&& c.WorldId == _uiShared.WorldId))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character"))
{
_serverConfigurationManager.AddCurrentCharacterToServer(idx);
}
ImGui.SameLine();
}
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character"))
{
_serverConfigurationManager.AddEmptyCharacterToServer(idx);
}
}
else
{
UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", ImGuiColors.DalamudYellow);
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Secret Key Management"))
{
foreach (var item in selectedServer.SecretKeys.ToList())
{
using var id = ImRaii.PushId("key" + item.Key);
var friendlyName = item.Value.FriendlyName;
if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255))
{
item.Value.FriendlyName = friendlyName;
_serverConfigurationManager.Save();
}
var key = item.Value.Key;
if (ImGui.InputText("Secret Key", ref key, 64))
{
item.Value.Key = key;
_serverConfigurationManager.Save();
}
if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed())
{
selectedServer.SecretKeys.Remove(item.Key);
_serverConfigurationManager.Save();
}
UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry");
}
else
{
UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", ImGuiColors.DalamudYellow);
}
if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault())
ImGui.Separator();
}
ImGui.Separator();
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key"))
{
selectedServer.SecretKeys.Add(selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, new SecretKey()
{
FriendlyName = "New Secret Key",
});
_serverConfigurationManager.Save();
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Service Settings"))
{
var serverName = selectedServer.ServerName;
var serverUri = selectedServer.ServerUri;
var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase);
var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None;
if (ImGui.InputText("Service URI", ref serverUri, 255, flags))
{
selectedServer.ServerUri = serverUri;
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the URI of the main service.");
}
if (ImGui.InputText("Service Name", ref serverName, 255, flags))
{
selectedServer.ServerName = serverName;
_serverConfigurationManager.Save();
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the name of the main service.");
}
if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer)
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed())
{
_serverConfigurationManager.DeleteServer(selectedServer);
}
_uiShared.DrawHelpText("Hold CTRL to delete this service");
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Permission Settings"))
{
_uiShared.BigText("Default Permission Settings");
if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected)
{
UiSharedService.TextWrapped("Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells.");
UiSharedService.TextWrapped("Note: The default permissions settings here are sent and stored on the connected service.");
ImGuiHelpers.ScaledDummy(5f);
var perms = _apiController.DefaultPermissions!;
bool individualIsSticky = perms.IndividualIsSticky;
bool disableIndividualSounds = perms.DisableIndividualSounds;
bool disableIndividualAnimations = perms.DisableIndividualAnimations;
bool disableIndividualVFX = perms.DisableIndividualVFX;
if (ImGui.Checkbox("Individually set permissions become preferred permissions", ref individualIsSticky))
{
perms.IndividualIsSticky = individualIsSticky;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " +
"(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " +
"if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + Environment.NewLine + Environment.NewLine +
"This setting means:" + Environment.NewLine +
" - All new individual pairs get their permissions defaulted to preferred permissions." + Environment.NewLine +
" - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + Environment.NewLine + Environment.NewLine +
"It is possible to remove or set the preferred permission state for any pair at any time." + Environment.NewLine + Environment.NewLine +
"If unsure, leave this setting off.");
ImGuiHelpers.ScaledDummy(3f);
if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds))
{
perms.DisableIndividualSounds = disableIndividualSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations))
{
perms.DisableIndividualAnimations = disableIndividualAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX))
{
perms.DisableIndividualVFX = disableIndividualVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs.");
ImGuiHelpers.ScaledDummy(5f);
bool disableGroundSounds = perms.DisableGroupSounds;
bool disableGroupAnimations = perms.DisableGroupAnimations;
bool disableGroupVFX = perms.DisableGroupVFX;
if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds))
{
perms.DisableGroupSounds = disableGroundSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable sound sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations))
{
perms.DisableGroupAnimations = disableGroupAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable animation sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX))
{
perms.DisableGroupVFX = disableGroupVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells.");
}
else
{
UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " +
"You need to connect to this service to change the default permissions since they are stored on the service.", ImGuiColors.DalamudYellow);
}
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private void DrawPerformance()
{
_uiShared.BigText("Performance Settings");
@@ -1527,8 +1159,421 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
}
private string _uidToAddForIgnore = string.Empty;
private int _selectedEntry = -1;
private void DrawServerConfiguration()
{
_lastTab = "Service Settings";
if (ApiController.ServerAlive)
{
_uiShared.BigText("Service Actions");
ImGuiHelpers.ScaledDummy(new Vector2(5, 5));
if (ImGui.Button("Delete all my files"))
{
_deleteFilesPopupModalShown = true;
ImGui.OpenPopup("Delete all your files?");
}
_uiShared.DrawHelpText("Completely deletes all your uploaded files on the service.");
if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"All your own uploaded files on the service will be deleted.\nThis operation cannot be undone.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0)))
{
_ = Task.Run(_fileTransferManager.DeleteAllFiles);
_deleteFilesPopupModalShown = false;
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteFilesPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
ImGui.SameLine();
if (ImGui.Button("Delete account"))
{
_deleteAccountPopupModalShown = true;
ImGui.OpenPopup("Delete your account?");
}
_uiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service.");
if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiSharedService.PopupWindowFlags))
{
UiSharedService.TextWrapped(
"Your account and all associated files and data on the service will be deleted.");
UiSharedService.TextWrapped("Your UID will be removed from all pairing lists.");
ImGui.TextUnformatted("Are you sure you want to continue?");
ImGui.Separator();
ImGui.Spacing();
var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X -
ImGui.GetStyle().ItemSpacing.X) / 2;
if (ImGui.Button("Delete account", new Vector2(buttonSize, 0)))
{
_ = Task.Run(ApiController.UserDelete);
_deleteAccountPopupModalShown = false;
Mediator.Publish(new SwitchToIntroUiMessage());
}
ImGui.SameLine();
if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0)))
{
_deleteAccountPopupModalShown = false;
}
UiSharedService.SetScaledWindowSize(325);
ImGui.EndPopup();
}
ImGui.Separator();
}
_uiShared.BigText("Service & Character Settings");
ImGuiHelpers.ScaledDummy(new Vector2(5, 5));
var sendCensus = _serverConfigurationManager.SendCensusData;
if (ImGui.Checkbox("Send Statistical Census Data", ref sendCensus))
{
_serverConfigurationManager.SendCensusData = sendCensus;
}
_uiShared.DrawHelpText("This will allow sending census data to the currently connected service." + UiSharedService.TooltipSeparator
+ "Census data contains:" + Environment.NewLine
+ "- Current World" + Environment.NewLine
+ "- Current Gender" + Environment.NewLine
+ "- Current Race" + Environment.NewLine
+ "- Current Clan (this is not your Free Company, this is e.g. Keeper or Seeker for Miqo'te)" + UiSharedService.TooltipSeparator
+ "The census data is only saved temporarily and will be removed from the server on disconnect. It is stored temporarily associated with your UID while you are connected." + UiSharedService.TooltipSeparator
+ "If you do not wish to participate in the statistical census, untick this box and reconnect to the server.");
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var idx = _uiShared.DrawServiceSelection();
if (_lastSelectedServerIndex != idx)
{
_uiShared.RestOAuthTasksState();
_lastSelectedServerIndex = idx;
}
ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
var selectedServer = _serverConfigurationManager.GetServerByIndex(idx);
if (selectedServer == _serverConfigurationManager.CurrentServer)
{
UiSharedService.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", ImGuiColors.DalamudYellow);
}
bool useOauth = selectedServer.UseOAuth2;
if (ImGui.BeginTabBar("serverTabBar"))
{
if (ImGui.BeginTabItem("Character Management"))
{
if (selectedServer.SecretKeys.Any() || useOauth)
{
UiSharedService.ColorTextWrapped("Characters listed here will automatically connect to the selected Mare service with the settings as provided below." +
" Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", ImGuiColors.DalamudYellow);
int i = 0;
_uiShared.DrawUpdateOAuthUIDsButton(selectedServer);
foreach (var item in selectedServer.Authentications.ToList())
{
using var charaId = ImRaii.PushId("selectedChara" + i);
var worldIdx = (ushort)item.WorldId;
var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value);
if (!data.TryGetValue(worldIdx, out string? worldPreview))
{
worldPreview = data.First().Value;
}
var friendlyName = string.Empty;
string friendlyNameTranslation = string.Empty;
Dictionary<int, SecretKey> keys = [];
if (!useOauth)
{
var secretKeyIdx = item.SecretKeyIdx;
keys = selectedServer.SecretKeys;
if (!keys.TryGetValue(secretKeyIdx, out var secretKey))
{
secretKey = new();
}
friendlyName = secretKey.FriendlyName;
friendlyNameTranslation = "Secret Key";
}
else
{
friendlyName = item.UID;
friendlyNameTranslation = "UID";
}
bool thisIsYou = false;
if (string.Equals(_dalamudUtilService.GetPlayerName(), item.CharacterName, StringComparison.OrdinalIgnoreCase)
&& _dalamudUtilService.GetWorldId() == worldIdx)
{
thisIsYou = true;
}
if (ImGui.TreeNode($"chara", (thisIsYou ? "[CURRENT] " : "") + $"Character: {item.CharacterName}, World: {worldPreview}, {friendlyNameTranslation}: {friendlyName}"))
{
var charaName = item.CharacterName;
if (ImGui.InputText("Character Name", ref charaName, 64))
{
item.CharacterName = charaName;
_serverConfigurationManager.Save();
}
_uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value,
(w) =>
{
if (item.WorldId != w.Key)
{
item.WorldId = w.Key;
_serverConfigurationManager.Save();
}
}, EqualityComparer<KeyValuePair<ushort, string>>.Default.Equals(data.FirstOrDefault(f => f.Key == worldIdx), default) ? data.First() : data.First(f => f.Key == worldIdx));
if (!useOauth)
{
_uiShared.DrawCombo("Secret Key##" + item.CharacterName + i, keys, (w) => w.Value.FriendlyName,
(w) =>
{
if (w.Key != item.SecretKeyIdx)
{
item.SecretKeyIdx = w.Key;
_serverConfigurationManager.Save();
}
}, EqualityComparer<KeyValuePair<int, SecretKey>>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx));
}
else
{
_uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri);
}
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed())
_serverConfigurationManager.RemoveCharacterFromServer(idx, item);
UiSharedService.AttachToolTip("Hold CTRL to delete this entry.");
ImGui.TreePop();
}
i++;
}
ImGui.Separator();
if (!selectedServer.Authentications.Exists(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal)
&& c.WorldId == _uiShared.WorldId))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character"))
{
_serverConfigurationManager.AddCurrentCharacterToServer(idx);
}
ImGui.SameLine();
}
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character"))
{
_serverConfigurationManager.AddEmptyCharacterToServer(idx);
}
}
else
{
UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", ImGuiColors.DalamudYellow);
}
ImGui.EndTabItem();
}
if (!useOauth && ImGui.BeginTabItem("Secret Key Management"))
{
foreach (var item in selectedServer.SecretKeys.ToList())
{
using var id = ImRaii.PushId("key" + item.Key);
var friendlyName = item.Value.FriendlyName;
if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255))
{
item.Value.FriendlyName = friendlyName;
_serverConfigurationManager.Save();
}
var key = item.Value.Key;
if (ImGui.InputText("Secret Key", ref key, 64))
{
item.Value.Key = key;
_serverConfigurationManager.Save();
}
if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key))
{
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed())
{
selectedServer.SecretKeys.Remove(item.Key);
_serverConfigurationManager.Save();
}
UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry");
}
else
{
UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", ImGuiColors.DalamudYellow);
}
if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault())
ImGui.Separator();
}
ImGui.Separator();
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key"))
{
selectedServer.SecretKeys.Add(selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, new SecretKey()
{
FriendlyName = "New Secret Key",
});
_serverConfigurationManager.Save();
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Service Settings"))
{
var serverName = selectedServer.ServerName;
var serverUri = selectedServer.ServerUri;
var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase);
var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None;
if (ImGui.InputText("Service URI", ref serverUri, 255, flags))
{
selectedServer.ServerUri = serverUri;
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the URI of the main service.");
}
if (ImGui.InputText("Service Name", ref serverName, 255, flags))
{
selectedServer.ServerName = serverName;
_serverConfigurationManager.Save();
}
if (isMain)
{
_uiShared.DrawHelpText("You cannot edit the name of the main service.");
}
if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth))
{
selectedServer.UseOAuth2 = useOauth;
_serverConfigurationManager.Save();
}
_uiShared.DrawHelpText("Use Discord OAuth2 Authentication to identify with this server instead of secret keys");
if (useOauth)
{
_uiShared.DrawOAuth(selectedServer);
}
if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer)
{
ImGui.Separator();
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed())
{
_serverConfigurationManager.DeleteServer(selectedServer);
}
_uiShared.DrawHelpText("Hold CTRL to delete this service");
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Permission Settings"))
{
_uiShared.BigText("Default Permission Settings");
if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected)
{
UiSharedService.TextWrapped("Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells.");
UiSharedService.TextWrapped("Note: The default permissions settings here are sent and stored on the connected service.");
ImGuiHelpers.ScaledDummy(5f);
var perms = _apiController.DefaultPermissions!;
bool individualIsSticky = perms.IndividualIsSticky;
bool disableIndividualSounds = perms.DisableIndividualSounds;
bool disableIndividualAnimations = perms.DisableIndividualAnimations;
bool disableIndividualVFX = perms.DisableIndividualVFX;
if (ImGui.Checkbox("Individually set permissions become preferred permissions", ref individualIsSticky))
{
perms.IndividualIsSticky = individualIsSticky;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " +
"(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " +
"if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + Environment.NewLine + Environment.NewLine +
"This setting means:" + Environment.NewLine +
" - All new individual pairs get their permissions defaulted to preferred permissions." + Environment.NewLine +
" - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + Environment.NewLine + Environment.NewLine +
"It is possible to remove or set the preferred permission state for any pair at any time." + Environment.NewLine + Environment.NewLine +
"If unsure, leave this setting off.");
ImGuiHelpers.ScaledDummy(3f);
if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds))
{
perms.DisableIndividualSounds = disableIndividualSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations))
{
perms.DisableIndividualAnimations = disableIndividualAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs.");
if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX))
{
perms.DisableIndividualVFX = disableIndividualVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs.");
ImGuiHelpers.ScaledDummy(5f);
bool disableGroundSounds = perms.DisableGroupSounds;
bool disableGroupAnimations = perms.DisableGroupAnimations;
bool disableGroupVFX = perms.DisableGroupVFX;
if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds))
{
perms.DisableGroupSounds = disableGroundSounds;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable sound sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations))
{
perms.DisableGroupAnimations = disableGroupAnimations;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable animation sync for all non-sticky pairs in newly joined syncshells.");
if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX))
{
perms.DisableGroupVFX = disableGroupVFX;
_ = _apiController.UserUpdateDefaultPermissions(perms);
}
_uiShared.DrawHelpText("This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells.");
}
else
{
UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " +
"You need to connect to this service to change the default permissions since they are stored on the service.", ImGuiColors.DalamudYellow);
}
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private int _lastSelectedServerIndex = -1;
private void DrawSettingsContent()
{

View File

@@ -19,6 +19,7 @@ using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -713,7 +714,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return true;
}
public int DrawServiceSelection(bool selectOnChange = false)
public int DrawServiceSelection(bool selectOnChange = false, bool showConnect = true)
{
string[] comboEntries = _serverConfigurationManager.GetServerNames();
@@ -753,13 +754,16 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.EndCombo();
}
if (showConnect)
{
ImGui.SameLine();
var text = "Connect";
if (_serverSelectionIndex == _serverConfigurationManager.CurrentServerIndex) text = "Reconnect";
if (IconTextButton(FontAwesomeIcon.Link, text))
{
_serverConfigurationManager.SelectServer(_serverSelectionIndex);
_ = _apiController.CreateConnections();
_ = _apiController.CreateConnectionsAsync();
}
}
if (ImGui.TreeNode("Add Custom Service"))
@@ -776,6 +780,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{
ServerName = _customServerName,
ServerUri = _customServerUri,
UseOAuth2 = true
});
_customServerName = string.Empty;
_customServerUri = string.Empty;
@@ -855,4 +860,177 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
UidFont.Dispose();
GameFont.Dispose();
}
private Task<Uri?>? _discordOAuthCheck;
private CancellationTokenSource _discordOAuthGetCts = new();
private Task<string?>? _discordOAuthGetCode;
private Task<Dictionary<string, string>>? _discordOAuthUIDs;
public void DrawUpdateOAuthUIDsButton(ServerStorage selectedServer)
{
using (ImRaii.Disabled(selectedServer.OAuthToken == null))
{
if ((_discordOAuthUIDs == null || _discordOAuthUIDs.IsCompleted)
&& IconTextButton(FontAwesomeIcon.ArrowsSpin, "Update UIDs from Service")
&& selectedServer.OAuthToken != null)
{
_discordOAuthUIDs = _serverConfigurationManager.GetUIDsWithDiscordToken(selectedServer.ServerUri, selectedServer.OAuthToken);
}
}
}
public void DrawUIDComboForAuthentication(int indexOffset, Authentication item, string serverUri)
{
using (ImRaii.Disabled(_discordOAuthUIDs == null))
{
DrawCombo("UID##" + item.CharacterName + serverUri + indexOffset, _discordOAuthUIDs?.Result ?? new(StringComparer.Ordinal) { { item.UID ?? string.Empty, string.Empty } },
(v) =>
{
if (!string.IsNullOrEmpty(v.Value))
{
return $"{v.Key} ({v.Value})";
}
if (string.IsNullOrEmpty(v.Key))
return "No UID set";
return $"{v.Key}";
},
(v) =>
{
if (!string.Equals(v.Key, item.UID, StringComparison.Ordinal))
{
item.UID = v.Key;
_serverConfigurationManager.Save();
}
},
_discordOAuthUIDs?.Result?.FirstOrDefault(f => string.Equals(f.Key, item.UID, StringComparison.Ordinal)) ?? default);
}
if (_discordOAuthUIDs == null)
{
AttachToolTip("Use the button above to update your UIDs from the service before you can assign UIDs to characters.");
}
}
public void DrawOAuth(ServerStorage selectedServer)
{
var oauthToken = selectedServer.OAuthToken;
using var _ = ImRaii.PushIndent(10f);
if (oauthToken == null)
{
if (_discordOAuthCheck == null)
{
if (IconTextButton(FontAwesomeIcon.QuestionCircle, "Check if Server supports Discord OAuth2"))
{
_discordOAuthCheck = _serverConfigurationManager.CheckDiscordOAuth(selectedServer.ServerUri);
}
}
else
{
if (!_discordOAuthCheck.IsCompleted)
{
ColorTextWrapped($"Checking OAuth2 compatibility with {selectedServer.ServerUri}", ImGuiColors.DalamudYellow);
}
else
{
if (_discordOAuthCheck.Result != null)
{
ColorTextWrapped("Server is compatible with Discord OAuth2", ImGuiColors.HealerGreen);
}
else
{
ColorTextWrapped("Server is not compatible with Discord OAuth2", ImGuiColors.DalamudRed);
}
}
}
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
{
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
{
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
}
else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
{
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
{
_discordOAuthGetCts = _discordOAuthGetCts.CancelRecreate();
_discordOAuthGetCode = null;
}
}
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
{
TextWrapped("Discord OAuth is completed, status: ");
ImGui.SameLine();
if (_discordOAuthGetCode.Result != null)
{
selectedServer.OAuthToken = _discordOAuthGetCode.Result;
_discordOAuthGetCode = null;
_serverConfigurationManager.Save();
ColorTextWrapped("Success", ImGuiColors.HealerGreen);
}
else
{
ColorTextWrapped("Failed, please check /xllog for more information", ImGuiColors.DalamudRed);
}
}
}
}
if (oauthToken != null)
{
ColorTextWrapped($"OAuth2 is enabled, linked to: Discord User {_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)}", ImGuiColors.HealerGreen);
if ((_discordOAuthUIDs == null || _discordOAuthUIDs.IsCompleted)
&& IconTextButton(FontAwesomeIcon.Question, "Check Discord Connection"))
{
_discordOAuthUIDs = _serverConfigurationManager.GetUIDsWithDiscordToken(selectedServer.ServerUri, oauthToken);
}
else if (_discordOAuthUIDs != null)
{
if (!_discordOAuthUIDs.IsCompleted)
{
ColorTextWrapped("Checking UIDs on Server", ImGuiColors.DalamudYellow);
}
else
{
var foundUids = _discordOAuthUIDs.Result?.Count ?? 0;
var primaryUid = _discordOAuthUIDs.Result?.FirstOrDefault() ?? new KeyValuePair<string, string>(string.Empty, string.Empty);
var vanity = string.IsNullOrEmpty(primaryUid.Value) ? "-" : primaryUid.Value;
if (foundUids > 0)
{
ColorTextWrapped($"Found {foundUids} associated UIDs on the server, Primary UID: {primaryUid.Key} (Vanity UID: {vanity})",
ImGuiColors.HealerGreen);
}
else
{
ColorTextWrapped($"Found no UIDs associated to this linked OAuth2 account", ImGuiColors.DalamudRed);
}
}
}
DrawUnlinkOAuthButton(selectedServer);
}
}
public void DrawUnlinkOAuthButton(ServerStorage selectedServer)
{
using (ImRaii.Disabled(!CtrlPressed()))
{
if (IconTextButton(FontAwesomeIcon.Trash, "Unlink OAuth2 Connection") && UiSharedService.CtrlPressed())
{
selectedServer.OAuthToken = null;
_serverConfigurationManager.Save();
RestOAuthTasksState();
}
}
DrawHelpText("Hold CTRL to unlink the current OAuth2 connection.");
}
internal void RestOAuthTasksState()
{
_discordOAuthCheck = null;
_discordOAuthGetCts = _discordOAuthGetCts.CancelRecreate();
_discordOAuthGetCode = null;
_discordOAuthUIDs = null;
}
}

View File

@@ -39,7 +39,7 @@ public partial class ApiController
{
CheckConnection();
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
await CreateConnections().ConfigureAwait(false);
await CreateConnectionsAsync().ConfigureAwait(false);
}
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs(CensusDataDto? censusDataDto)

View File

@@ -55,11 +55,11 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
Mediator.Subscribe<HubClosedMessage>(this, (msg) => MareHubOnClosed(msg.Exception));
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = MareHubOnReconnected());
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = MareHubOnReconnectedAsync());
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(msg.Exception));
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePause(msg.UserData));
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.UserData));
Mediator.Subscribe<CensusUpdateMessage>(this, (msg) => _lastCensus = msg);
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = Pause(msg.UserData));
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = PauseAsync(msg.UserData));
ServerState = ServerState.Offline;
@@ -105,7 +105,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
return await _mareHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
}
public async Task CreateConnections()
public async Task CreateConnectionsAsync()
{
if (!_serverManager.ShownCensusPopup)
{
@@ -122,11 +122,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
{
Logger.LogInformation("Not recreating Connection, paused");
_connectionDto = null;
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
if (!_serverManager.CurrentServer.UseOAuth2)
{
var secretKey = _serverManager.GetSecretKey(out bool multi);
if (multi)
{
@@ -134,7 +136,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
_connectionDto = null;
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Mare.",
NotificationType.Error));
await StopConnection(ServerState.MultiChara).ConfigureAwait(false);
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
@@ -143,12 +145,45 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
{
Logger.LogWarning("No secret key set for current character");
_connectionDto = null;
await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false);
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
}
else
{
var oauth2 = _serverManager.GetOAuth2(out bool multi);
if (multi)
{
Logger.LogWarning("Multiple secret keys for current character");
_connectionDto = null;
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Mare.",
NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
if (!oauth2.HasValue)
{
Logger.LogWarning("No UID/OAuth set for current character");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
if (!await _tokenProvider.TryUpdateOAuth2LoginTokenAsync().ConfigureAwait(false))
{
Logger.LogWarning("OAuth2 login token could not be updated");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
return;
}
}
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
Logger.LogInformation("Recreating Connection");
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
@@ -162,7 +197,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
{
AuthFailureMessage = string.Empty;
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
ServerState = ServerState.Connecting;
try
@@ -208,7 +243,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
$"This client version is incompatible and will not be able to connect. Please update your Mare Synchronos client.",
NotificationType.Error));
}
await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false);
await StopConnectionAsync(ServerState.VersionMisMatch).ConfigureAwait(false);
return;
}
@@ -232,8 +267,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
NotificationType.Error, TimeSpan.FromSeconds(15)));
}
await LoadIninitialPairs().ConfigureAwait(false);
await LoadOnlinePairs().ConfigureAwait(false);
await LoadIninitialPairsAsync().ConfigureAwait(false);
await LoadOnlinePairsAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -246,7 +281,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await StopConnection(ServerState.Unauthorized).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Unauthorized).ConfigureAwait(false);
return;
}
@@ -257,7 +292,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
catch (InvalidOperationException ex)
{
Logger.LogWarning(ex, "InvalidOperationException on connection");
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
return;
}
catch (Exception ex)
@@ -270,7 +305,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
}
}
public Task CyclePause(UserData userData)
public Task CyclePauseAsync(UserData userData)
{
CancellationTokenSource cts = new();
cts.CancelAfter(TimeSpan.FromSeconds(5));
@@ -293,7 +328,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
return Task.CompletedTask;
}
public async Task Pause(UserData userData)
public async Task PauseAsync(UserData userData)
{
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
var perm = pair.UserPair!.OwnPermissions;
@@ -301,9 +336,9 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
}
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDto(true);
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDtoAsync(true);
public async Task<ConnectionDto> GetConnectionDto(bool publishConnected = true)
public async Task<ConnectionDto> GetConnectionDtoAsync(bool publishConnected)
{
var dto = await _mareHub!.InvokeAsync<ConnectionDto>(nameof(GetConnectionDto)).ConfigureAwait(false);
if (publishConnected) Mediator.Publish(new ConnectedMessage(dto));
@@ -315,18 +350,18 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
base.Dispose(disposing);
_healthCheckTokenSource?.Cancel();
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
_ = Task.Run(async () => await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
_connectionCancellationTokenSource?.Cancel();
}
private async Task ClientHealthCheck(CancellationToken ct)
private async Task ClientHealthCheckAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested && _mareHub != null)
{
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
Logger.LogDebug("Checking Client Health State");
bool requireReconnect = await RefreshToken(ct).ConfigureAwait(false);
bool requireReconnect = await RefreshTokenAsync(ct).ConfigureAwait(false);
if (requireReconnect) break;
@@ -336,12 +371,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
private void DalamudUtilOnLogIn()
{
_ = Task.Run(() => CreateConnections());
_ = Task.Run(() => CreateConnectionsAsync());
}
private void DalamudUtilOnLogOut()
{
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
_ = Task.Run(async () => await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
ServerState = ServerState.Offline;
}
@@ -378,12 +413,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
_healthCheckTokenSource?.Cancel();
_healthCheckTokenSource?.Dispose();
_healthCheckTokenSource = new CancellationTokenSource();
_ = ClientHealthCheck(_healthCheckTokenSource.Token);
_ = ClientHealthCheckAsync(_healthCheckTokenSource.Token);
_initialized = true;
}
private async Task LoadIninitialPairs()
private async Task LoadIninitialPairsAsync()
{
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
{
@@ -398,7 +433,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
}
}
private async Task LoadOnlinePairs()
private async Task LoadOnlinePairsAsync()
{
CensusDataDto? dto = null;
if (_serverManager.SendCensusData && _lastCensus != null)
@@ -430,27 +465,27 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
}
}
private async Task MareHubOnReconnected()
private async Task MareHubOnReconnectedAsync()
{
ServerState = ServerState.Reconnecting;
try
{
InitializeApiHooks();
_connectionDto = await GetConnectionDto(publishConnected: false).ConfigureAwait(false);
_connectionDto = await GetConnectionDtoAsync(publishConnected: false).ConfigureAwait(false);
if (_connectionDto.ServerVersion != IMareHub.ApiVersion)
{
await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false);
await StopConnectionAsync(ServerState.VersionMisMatch).ConfigureAwait(false);
return;
}
ServerState = ServerState.Connected;
await LoadIninitialPairs().ConfigureAwait(false);
await LoadOnlinePairs().ConfigureAwait(false);
await LoadIninitialPairsAsync().ConfigureAwait(false);
await LoadOnlinePairsAsync().ConfigureAwait(false);
Mediator.Publish(new ConnectedMessage(_connectionDto));
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Failure to obtain data after reconnection");
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
}
}
@@ -465,7 +500,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
}
private async Task<bool> RefreshToken(CancellationToken ct)
private async Task<bool> RefreshTokenAsync(CancellationToken ct)
{
Logger.LogDebug("Checking token");
@@ -478,28 +513,28 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
Logger.LogDebug("Reconnecting due to updated token");
_doNotNotifyOnNextInfo = true;
await CreateConnections().ConfigureAwait(false);
await CreateConnectionsAsync().ConfigureAwait(false);
requireReconnect = true;
}
}
catch (MareAuthFailureException ex)
{
AuthFailureMessage = ex.Reason;
await StopConnection(ServerState.Unauthorized).ConfigureAwait(false);
await StopConnectionAsync(ServerState.Unauthorized).ConfigureAwait(false);
requireReconnect = true;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not refresh token, forcing reconnect");
_doNotNotifyOnNextInfo = true;
await CreateConnections().ConfigureAwait(false);
await CreateConnectionsAsync().ConfigureAwait(false);
requireReconnect = true;
}
return requireReconnect;
}
private async Task StopConnection(ServerState state)
private async Task StopConnectionAsync(ServerState state)
{
ServerState = ServerState.Disconnecting;

View File

@@ -1,9 +1,9 @@
namespace MareSynchronos.WebAPI.SignalR;
public record JwtIdentifier(string ApiUrl, string CharaHash, string SecretKey)
public record JwtIdentifier(string ApiUrl, string CharaHash, string UID, string SecretKeyOrOAuth)
{
public override string ToString()
{
return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", HasSecretKey: " + !string.IsNullOrEmpty(SecretKey) + "}";
return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", UID: " + UID + ", HasSecretKeyOrOAuth: " + !string.IsNullOrEmpty(SecretKeyOrOAuth) + "}";
}
}

View File

@@ -51,7 +51,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
_httpClient.Dispose();
}
public async Task<string> GetNewToken(bool isRenewal, JwtIdentifier identifier, CancellationToken token)
public async Task<string> GetNewToken(bool isRenewal, JwtIdentifier identifier, CancellationToken ct)
{
Uri tokenUri;
string response = string.Empty;
@@ -63,16 +63,34 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
{
_logger.LogDebug("GetNewToken: Requesting");
if (!_serverManager.CurrentServer.UseOAuth2)
{
tokenUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
var secretKey = _serverManager.GetSecretKey(out _)!;
var auth = secretKey.GetHash256();
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(new[]
{
_logger.LogInformation("Sending SecretKey Request to server with auth {auth}", string.Join("", identifier.SecretKeyOrOAuth.Take(10)));
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("auth", auth),
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
}), token).ConfigureAwait(false);
]), ct).ConfigureAwait(false);
}
else
{
tokenUri = MareAuth.AuthWithOauthFullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, tokenUri.ToString());
request.Content = new FormUrlEncodedContent([
new KeyValuePair<string, string>("uid", identifier.UID),
new KeyValuePair<string, string>("charaIdent", identifier.CharaHash)
]);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", identifier.SecretKeyOrOAuth);
_logger.LogInformation("Sending OAuth Request to server with auth {auth}", string.Join("", identifier.SecretKeyOrOAuth.Take(10)));
result = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
}
}
else
{
@@ -83,7 +101,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
HttpRequestMessage request = new(HttpMethod.Get, tokenUri.ToString());
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenCache[identifier]);
result = await _httpClient.SendAsync(request, token).ConfigureAwait(false);
result = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
}
response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -102,7 +120,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting to Mare manually.",
NotificationType.Error));
else
Mediator.Publish(new NotificationMessage("Error generating token", "Your authentication token could not be generated. Check Mares main UI to see the error message.",
Mediator.Publish(new NotificationMessage("Error generating token", "Your authentication token could not be generated. Check Mares Main UI to see the error message.",
NotificationType.Error));
Mediator.Publish(new DisconnectedMessage());
throw new MareAuthFailureException(response);
@@ -144,9 +162,20 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
return _lastJwtIdentifier;
}
if (_serverManager.CurrentServer.UseOAuth2)
{
var oauthInfo = _serverManager.GetOAuth2(out _)!;
jwtIdentifier = new(_serverManager.CurrentApiUrl,
playerIdentifier,
oauthInfo.Value.UID, oauthInfo.Value.OAuthToken);
}
else
{
jwtIdentifier = new(_serverManager.CurrentApiUrl,
playerIdentifier,
string.Empty,
_serverManager.GetSecretKey(out _)!);
}
_lastJwtIdentifier = jwtIdentifier;
}
catch (Exception ex)
@@ -205,4 +234,39 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
_logger.LogTrace("GetOrUpdate: Getting new token");
return await GetNewToken(renewal, jwtIdentifier, ct).ConfigureAwait(false);
}
public async Task<bool> TryUpdateOAuth2LoginTokenAsync()
{
var oauth2 = _serverManager.GetOAuth2(out _);
if (oauth2 == null) return false;
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(oauth2.Value.OAuthToken);
if (jwt.ValidTo == DateTime.MinValue || jwt.ValidTo.Subtract(TimeSpan.FromDays(7)) > DateTime.Now)
return true;
if (jwt.ValidTo < DateTime.UtcNow)
return false;
var tokenUri = MareAuth.RenewOAuthTokenFullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, tokenUri.ToString());
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", oauth2.Value.OAuthToken);
_logger.LogInformation("Sending Request to server with auth {auth}", string.Join("", oauth2.Value.OAuthToken.Take(10)));
var result = await _httpClient.SendAsync(request).ConfigureAwait(false);
if (!result.IsSuccessStatusCode)
{
_logger.LogWarning("Could not renew OAuth2 Login token, error code {error}", result.StatusCode);
return false;
}
var newToken = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
_serverManager.CurrentServer.OAuthToken = newToken;
_serverManager.Save();
return true;
}
}

View File

@@ -13,4 +13,6 @@ public enum ServerState
RateLimited,
NoSecretKey,
MultiChara,
OAuthMisconfigured,
OAuthLoginTokenStale
}