using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using MareSynchronos.API; using MareSynchronosServer.Utils; using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; namespace MareSynchronosServer.Hubs; public partial class MareHub { [Authorize(Policy = "Identified")] public async Task UserDelete() { _logger.LogCallInfo(); string userid = AuthenticatedUserId; var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false); var charaIdent = _clientIdentService.GetCharacterIdentForUid(userid); var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false); var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false); var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false); var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == userid).ToListAsync().ConfigureAwait(false); if (lodestone != null) { _dbContext.Remove(lodestone); } while (_dbContext.Files.Any(f => f.Uploader == userEntry)) { await Task.Delay(1000).ConfigureAwait(false); } _dbContext.ClientPairs.RemoveRange(ownPairData); await _dbContext.SaveChangesAsync().ConfigureAwait(false); var otherPairData = await _dbContext.ClientPairs.Include(u => u.User) .Where(u => u.OtherUser.UID == userid).AsNoTracking().ToListAsync().ConfigureAwait(false); foreach (var pair in otherPairData) { await Clients.User(pair.User.UID).Client_UserUpdateClientPairs(new ClientPairDto() { OtherUID = userid, IsRemoved = true }).ConfigureAwait(false); } foreach (var pair in groupPairs) { await GroupLeave(pair.GroupGID).ConfigureAwait(false); } _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); _dbContext.ClientPairs.RemoveRange(otherPairData); _dbContext.Users.Remove(userEntry); _dbContext.Auth.Remove(auth); await _dbContext.SaveChangesAsync().ConfigureAwait(false); } [Authorize(Policy = "Identified")] public async Task> UserGetOnlineCharacters() { _logger.LogCallInfo(); var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); var usersToSendOnlineTo = await SendOnlineToAllPairedUsers(ownIdent).ConfigureAwait(false); return usersToSendOnlineTo.Select(e => _clientIdentService.GetCharacterIdentForUid(e)).Where(t => !string.IsNullOrEmpty(t)).Distinct(System.StringComparer.Ordinal).ToList(); } [Authorize(Policy = "Identified")] public async Task> UserGetPairedClients() { _logger.LogCallInfo(); string userid = AuthenticatedUserId; var query = from userToOther in _dbContext.ClientPairs join otherToUser in _dbContext.ClientPairs on new { user = userToOther.UserUID, other = userToOther.OtherUserUID } equals new { user = otherToUser.OtherUserUID, other = otherToUser.UserUID } into leftJoin from otherEntry in leftJoin.DefaultIfEmpty() where userToOther.UserUID == userid select new { userToOther.OtherUser.Alias, userToOther.IsPaused, OtherIsPaused = otherEntry != null && otherEntry.IsPaused, userToOther.OtherUserUID, IsSynced = otherEntry != null }; return (await query.AsNoTracking().ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto() { VanityUID = f.Alias, IsPaused = f.IsPaused, OtherUID = f.OtherUserUID, IsSynced = f.IsSynced, IsPausedFromOthers = f.OtherIsPaused }).ToList(); } [GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] private static partial Regex HashRegex(); [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] private static partial Regex GamePathRegex(); [Authorize(Policy = "Identified")] public async Task UserPushData(CharacterCacheDto characterCache, List visibleCharacterIds) { _logger.LogCallInfo(MareHubLogger.Args(visibleCharacterIds.Count)); bool hadInvalidData = false; foreach (var replacement in characterCache.FileReplacements.SelectMany(p => p.Value)) { var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToArray(); replacement.GamePaths = replacement.GamePaths.Where(p => GamePathRegex().IsMatch(p)).ToArray(); bool validGamePaths = replacement.GamePaths.Any(); bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash); bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath); if (!validGamePaths || !validHash || !validFileSwapPath) { _logger.LogCallWarning(MareHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath)); hadInvalidData = true; } } if (hadInvalidData) throw new HubException("Invalid data provided"); var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, f => _clientIdentService.GetCharacterIdentForUid(f), System.StringComparer.Ordinal) .Where(f => visibleCharacterIds.Contains(f.Value, System.StringComparer.Ordinal)); var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); _logger.LogCallInfo(MareHubLogger.Args(visibleCharacterIds.Count, allPairedUsersDict.Count())); await Clients.Users(allPairedUsersDict.Select(f => f.Key)).Client_UserReceiveCharacterData(characterCache, ownIdent).ConfigureAwait(false); _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, allPairedUsersDict.Count()); } [Authorize(Policy = "Identified")] public async Task UserAddPair(string uid) { _logger.LogCallInfo(MareHubLogger.Args(uid)); // don't allow adding yourself or nothing uid = uid.Trim(); if (string.Equals(uid, AuthenticatedUserId, System.StringComparison.Ordinal) || string.IsNullOrWhiteSpace(uid)) return; // grab other user, check if it exists and if a pair already exists var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); var existingEntry = await _dbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => p.User.UID == AuthenticatedUserId && p.OtherUserUID == uid).ConfigureAwait(false); if (otherUser == null || existingEntry != null) return; // grab self create new client pair and save var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); _logger.LogCallInfo(MareHubLogger.Args(uid, "Success")); ClientPair wl = new ClientPair() { IsPaused = false, OtherUser = otherUser, User = user }; await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); await _dbContext.SaveChangesAsync().ConfigureAwait(false); // get the opposite entry of the client pair var otherEntry = OppositeEntry(otherUser.UID); await Clients.User(user.UID).Client_UserUpdateClientPairs( new ClientPairDto() { VanityUID = otherUser.Alias, OtherUID = otherUser.UID, IsPaused = false, IsPausedFromOthers = otherEntry?.IsPaused ?? false, IsSynced = otherEntry != null }).ConfigureAwait(false); // if there's no opposite entry do nothing if (otherEntry == null) return; // check if other user is online var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID); if (otherIdent == null) return; // send push with update to other user if other user is online await Clients.User(otherUser.UID).Client_UserUpdateClientPairs( new ClientPairDto() { VanityUID = user.Alias, OtherUID = user.UID, IsPaused = otherEntry.IsPaused, IsPausedFromOthers = false, IsSynced = true }).ConfigureAwait(false); // get own ident and all pairs var userIdent = _clientIdentService.GetCharacterIdentForUid(user.UID); var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); // if the other user has paused the main user and there was no previous group connection don't send anything if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, System.StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection)) { await Clients.User(user.UID).Client_UserChangePairedPlayer(otherIdent, true).ConfigureAwait(false); await Clients.User(otherUser.UID).Client_UserChangePairedPlayer(userIdent, true).ConfigureAwait(false); } } [Authorize(Policy = "Identified")] public async Task UserChangePairPauseStatus(string otherUserUid, bool isPaused) { _logger.LogCallInfo(MareHubLogger.Args(otherUserUid, isPaused)); if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return; ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); if (pair == null) return; pair.IsPaused = isPaused; _dbContext.Update(pair); await _dbContext.SaveChangesAsync().ConfigureAwait(false); _logger.LogCallInfo(MareHubLogger.Args(otherUserUid, isPaused, "Success")); var otherEntry = OppositeEntry(otherUserUid); await Clients.User(AuthenticatedUserId).Client_UserUpdateClientPairs( new ClientPairDto() { OtherUID = otherUserUid, IsPaused = isPaused, IsPausedFromOthers = otherEntry?.IsPaused ?? false, IsSynced = otherEntry != null }).ConfigureAwait(false); if (otherEntry != null) { await Clients.User(otherUserUid).Client_UserUpdateClientPairs(new ClientPairDto() { OtherUID = AuthenticatedUserId, IsPaused = otherEntry.IsPaused, IsPausedFromOthers = isPaused, IsSynced = true }).ConfigureAwait(false); var selfCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); var otherCharaIdent = _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID); if (selfCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return; await Clients.User(AuthenticatedUserId).Client_UserChangePairedPlayer(otherCharaIdent, !isPaused).ConfigureAwait(false); await Clients.User(otherUserUid).Client_UserChangePairedPlayer(selfCharaIdent, !isPaused).ConfigureAwait(false); } } [Authorize(Policy = "Identified")] public async Task UserRemovePair(string otherUserUid) { _logger.LogCallInfo(MareHubLogger.Args(otherUserUid)); if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return; // check if client pair even exists ClientPair callerPair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); bool callerHadPaused = callerPair.IsPaused; if (callerPair == null) return; // delete from database, send update info to users pair list _dbContext.ClientPairs.Remove(callerPair); await _dbContext.SaveChangesAsync().ConfigureAwait(false); _logger.LogCallInfo(MareHubLogger.Args(otherUserUid, "Success")); await Clients.User(AuthenticatedUserId) .Client_UserUpdateClientPairs(new ClientPairDto() { OtherUID = otherUserUid, IsRemoved = true }).ConfigureAwait(false); // check if opposite entry exists var oppositeClientPair = OppositeEntry(otherUserUid); if (oppositeClientPair == null) return; // check if other user is online, if no then there is no need to do anything further var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUserUid); if (otherIdent == null) return; // get own ident and await Clients.User(otherUserUid).Client_UserUpdateClientPairs( new ClientPairDto() { OtherUID = AuthenticatedUserId, IsPausedFromOthers = false, IsSynced = false }).ConfigureAwait(false); // if the other user had paused the user the state will be offline for either, do nothing bool otherHadPaused = oppositeClientPair.IsPaused; if (!callerHadPaused && otherHadPaused) return; var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, otherUserUid, System.StringComparison.Ordinal)); var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection; // if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing if (!callerHadPaused && !otherHadPaused && !isPausedInGroup) return; // if neither user had paused each other and either is not in an unpaused group with each other, change state to offline if (!callerHadPaused && !otherHadPaused && isPausedInGroup) { var userIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); await Clients.User(AuthenticatedUserId).Client_UserChangePairedPlayer(otherIdent, false).ConfigureAwait(false); await Clients.User(otherUserUid).Client_UserChangePairedPlayer(userIdent, false).ConfigureAwait(false); } // if the caller had paused other but not the other has paused the caller and they are in an unpaused group together, change state to online if (callerHadPaused && !otherHadPaused && !isPausedInGroup) { var userIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); await Clients.User(AuthenticatedUserId).Client_UserChangePairedPlayer(otherIdent, true).ConfigureAwait(false); await Clients.User(otherUserUid).Client_UserChangePairedPlayer(userIdent, true).ConfigureAwait(false); } } private ClientPair OppositeEntry(string otherUID) => _dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId); }