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; public class ServerConfigurationManager { private readonly ServerConfigService _configService; private readonly DalamudUtilService _dalamudUtil; private readonly MareConfigService _mareConfigService; private readonly ILogger _logger; private readonly MareMediator _mareMediator; private readonly NotesConfigService _notesConfig; private readonly ServerTagConfigService _serverTagConfig; public ServerConfigurationManager(ILogger logger, ServerConfigService configService, ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil, MareConfigService mareConfigService, MareMediator mareMediator) { _logger = logger; _configService = configService; _serverTagConfig = serverTagConfig; _notesConfig = notesConfig; _dalamudUtil = dalamudUtil; _mareConfigService = mareConfigService; _mareMediator = mareMediator; EnsureMainExists(); } public string CurrentApiUrl => CurrentServer.ServerUri; public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex]; public bool SendCensusData { get { return _configService.Current.SendCensusData; } set { _configService.Current.SendCensusData = value; _configService.Save(); } } public bool ShownCensusPopup { get { return _configService.Current.ShownCensusPopup; } set { _configService.Current.ShownCensusPopup = value; _configService.Save(); } } public int CurrentServerIndex { set { _configService.Current.CurrentServer = value; _configService.Save(); } get { if (_configService.Current.CurrentServer < 0) { _configService.Current.CurrentServer = 0; _configService.Save(); } return _configService.Current.CurrentServer; } } 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; 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(); if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any()) { currentServer.Authentications.Add(new Authentication() { CharacterName = charaName, WorldId = worldId, SecretKeyIdx = currentServer.SecretKeys.Last().Key, }); Save(); } var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && 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 (currentServer.SecretKeys.TryGetValue(auth.Single().SecretKeyIdx, out var secretKey)) { _logger.LogTrace("GetSecretKey accessed, returning {key} ({keyValue}) for {chara} on {world}", secretKey.FriendlyName, string.Join("", secretKey.Key.Take(10)), charaName, worldId); return secretKey.Key; } _logger.LogTrace("GetSecretKey accessed, returning null because no fitting key found for {chara} on {world} for idx {idx}.", charaName, worldId, auth.Single().SecretKeyIdx); return null; } public string[] GetServerApiUrls() { return _configService.Current.ServerStorage.Select(v => v.ServerUri).ToArray(); } public ServerStorage GetServerByIndex(int idx) { try { return _configService.Current.ServerStorage[idx]; } catch { _configService.Current.CurrentServer = 0; EnsureMainExists(); return CurrentServer!; } } public string GetDiscordUserFromToken(ServerStorage server) { JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); if (server.OAuthToken == null) return string.Empty; try { var token = handler.ReadJwtToken(server.OAuthToken); return token.Claims.First(f => string.Equals(f.Type, "discord_user", StringComparison.Ordinal)).Value!; } catch (Exception ex) { _logger.LogWarning(ex, "Could not read jwt, resetting it"); server.OAuthToken = null; Save(); return string.Empty; } } public string[] GetServerNames() { return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray(); } public bool HasValidConfig() { return CurrentServer != null && CurrentServer.Authentications.Count > 0; } public void Save() { var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; _logger.LogDebug("{caller} Calling config save", caller); _configService.Save(); } public void SelectServer(int idx) { _configService.Current.CurrentServer = idx; CurrentServer!.FullPause = false; Save(); } internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1) { if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex; var server = GetServerByIndex(serverSelectionIndex); if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal) && c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult())) return; server.Authentications.Add(new Authentication() { CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(), SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1, }); Save(); } internal void AddEmptyCharacterToServer(int serverSelectionIndex) { var server = GetServerByIndex(serverSelectionIndex); server.Authentications.Add(new Authentication() { SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1, }); Save(); } internal void AddOpenPairTag(string tag) { CurrentServerTagStorage().OpenPairTags.Add(tag); _serverTagConfig.Save(); } internal void AddServer(ServerStorage serverStorage) { _configService.Current.ServerStorage.Add(serverStorage); Save(); } internal void AddTag(string tag) { CurrentServerTagStorage().ServerAvailablePairTags.Add(tag); _serverTagConfig.Save(); _mareMediator.Publish(new RefreshUiMessage()); } internal void AddTagForUid(string uid, string tagName) { if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { tags.Add(tagName); _mareMediator.Publish(new RefreshUiMessage()); } else { CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName]; } _serverTagConfig.Save(); } internal bool ContainsOpenPairTag(string tag) { return CurrentServerTagStorage().OpenPairTags.Contains(tag); } internal bool ContainsTag(string uid, string tag) { if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { return tags.Contains(tag, StringComparer.Ordinal); } return false; } internal void DeleteServer(ServerStorage selectedServer) { if (Array.IndexOf(_configService.Current.ServerStorage.ToArray(), selectedServer) < _configService.Current.CurrentServer) { _configService.Current.CurrentServer--; } _configService.Current.ServerStorage.Remove(selectedServer); Save(); } internal string? GetNoteForGid(string gID) { if (CurrentNotesStorage().GidServerComments.TryGetValue(gID, out var note)) { if (string.IsNullOrEmpty(note)) return null; return note; } return null; } internal string? GetNoteForUid(string uid) { if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note)) { if (string.IsNullOrEmpty(note)) return null; return note; } return null; } internal HashSet GetServerAvailablePairTags() { return CurrentServerTagStorage().ServerAvailablePairTags; } internal Dictionary> GetUidServerPairedUserTags() { return CurrentServerTagStorage().UidServerPairedUserTags; } internal HashSet GetUidsForTag(string tag) { return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal); } internal bool HasTags(string uid) { if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { return tags.Any(); } return false; } internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item) { var server = GetServerByIndex(serverSelectionIndex); server.Authentications.Remove(item); Save(); } internal void RemoveOpenPairTag(string tag) { CurrentServerTagStorage().OpenPairTags.Remove(tag); _serverTagConfig.Save(); } internal void RemoveTag(string tag) { CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag); foreach (var uid in GetUidsForTag(tag)) { RemoveTagForUid(uid, tag, save: false); } _serverTagConfig.Save(); _mareMediator.Publish(new RefreshUiMessage()); } internal void RemoveTagForUid(string uid, string tagName, bool save = true) { if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { tags.Remove(tagName); if (save) { _serverTagConfig.Save(); _mareMediator.Publish(new RefreshUiMessage()); } } } internal void RenameTag(string oldName, string newName) { CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName); CurrentServerTagStorage().ServerAvailablePairTags.Add(newName); foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value)) { if (existingTags.Remove(oldName)) existingTags.Add(newName); } } internal void SaveNotes() { _notesConfig.Save(); } internal void SetNoteForGid(string gid, string note, bool save = true) { if (string.IsNullOrEmpty(gid)) return; CurrentNotesStorage().GidServerComments[gid] = note; if (save) _notesConfig.Save(); } internal void SetNoteForUid(string uid, string note, bool save = true) { if (string.IsNullOrEmpty(uid)) return; CurrentNotesStorage().UidServerComments[uid] = note; if (save) _notesConfig.Save(); } internal void AutoPopulateNoteForUid(string uid, string note) { if (!_mareConfigService.Current.AutoPopulateEmptyNotesFromCharaName || GetNoteForUid(uid) != null) return; SetNoteForUid(uid, note, save: true); } private ServerNotesStorage CurrentNotesStorage() { TryCreateCurrentNotesStorage(); return _notesConfig.Current.ServerNotes[CurrentApiUrl]; } private ServerTagStorage CurrentServerTagStorage() { TryCreateCurrentServerTagStorage(); return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl]; } private void EnsureMainExists() { 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, UseOAuth2 = true }); } Save(); } private void TryCreateCurrentNotesStorage() { if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl)) { _notesConfig.Current.ServerNotes[CurrentApiUrl] = new(); } } private void TryCreateCurrentServerTagStorage() { if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl)) { _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new(); } } public async Task> 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>(responseStream).ConfigureAwait(false) ?? []; } catch (Exception ex) { _logger.LogWarning(ex, "Failure getting UIDs"); return []; } } public async Task 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(oauthCheckUri).ConfigureAwait(false); return response; } catch (Exception ex) { _logger.LogWarning(ex, "Failure checking for Discord Auth"); return null; } } public async Task 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; } }