From 7b0ac34623636682226889120af649962df3e914 Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sun, 19 Mar 2023 18:57:55 +0100 Subject: [PATCH] add mare profiles --- Docker/run/compose/mare-standalone.yml | 3 + .../Hubs/MareHub.ClientStubs.cs | 2 + .../Hubs/MareHub.Functions.cs | 173 ++--- .../MareSynchronosServer/Hubs/MareHub.User.cs | 510 +++++++++----- .../MareSynchronosServer.csproj | 3 +- .../Discord/DiscordBot.cs | 265 +++++-- .../Discord/DiscordBotServices.cs | 16 +- .../MareSynchronosServices/DummyHub.cs | 25 + .../MareSynchronosServices.csproj | 1 + .../MareSynchronosServices/Startup.cs | 54 +- .../Data/MareDbContext.cs | 22 +- .../MareSynchronosShared.csproj | 2 + ...20230319015307_UserProfileData.Designer.cs | 584 ++++++++++++++++ .../20230319015307_UserProfileData.cs | 40 ++ ...30319114005_UserProfileReports.Designer.cs | 650 ++++++++++++++++++ .../20230319114005_UserProfileReports.cs | 92 +++ .../Migrations/MareDbContextModelSnapshot.cs | 100 ++- .../Models/UserProfileData.cs | 20 + .../Models/UserProfileDataReport.cs | 24 + .../Utils/MareConfigurationBase.cs | 8 +- .../Utils/ServerConfiguration.cs | 20 +- .../Utils/ServicesConfiguration.cs | 7 +- .../Utils/SharedDbFunctions.cs | 69 +- .../MareSynchronosStaticFilesServer.csproj | 2 - 24 files changed, 2313 insertions(+), 379 deletions(-) create mode 100644 MareSynchronosServer/MareSynchronosServices/DummyHub.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs create mode 100644 MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs diff --git a/Docker/run/compose/mare-standalone.yml b/Docker/run/compose/mare-standalone.yml index 4c89bb0..a49e82f 100644 --- a/Docker/run/compose/mare-standalone.yml +++ b/Docker/run/compose/mare-standalone.yml @@ -2,6 +2,8 @@ services: postgres: image: postgres:latest restart: always + ports: + - 5432:5432/tcp environment: POSTGRES_DB: mare POSTGRES_USER: mare @@ -48,6 +50,7 @@ services: environment: MareSynchronos__DiscordBotToken: "${DEV_MARE_DISCORDTOKEN}" MareSynchronos__DiscordChannelForMessages: "${DEV_MARE_DISCORDCHANNEL}" + MareSynchronos__DiscordChannelForReports: "${DEV_MARE_DISCORDCHANNEL}" DOTNET_USE_POLLING_FILE_WATCHER: 1 volumes: - ../config/standalone/services-standalone.json:/opt/MareSynchronosServices/appsettings.json diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs index 2e3fa18..cdfa2f2 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs @@ -43,6 +43,8 @@ namespace MareSynchronosServer.Hubs public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs index cf75d67..2e1957e 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -12,26 +12,53 @@ namespace MareSynchronosServer.Hubs; public partial class MareHub { - private async Task UpdateUserOnRedis() - { - await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); - } + public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims"); - private async Task RemoveUserFromRedis() - { - await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); - } + public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims"); - private async Task GetUserIdent(string uid) + private async Task DeleteUser(User user) { - if (uid.IsNullOrEmpty()) return string.Empty; - return await _redis.GetAsync("UID:" + uid).ConfigureAwait(false); - } + var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false); + var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == user.UID).ConfigureAwait(false); + var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false); + var userProfileData = await _dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); - private async Task> GetOnlineUsers(List uids) - { - var result = await _redis.GetAllAsync(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false); - return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal); + if (lodestone != null) + { + _dbContext.Remove(lodestone); + } + + if (userProfileData != null) + { + _dbContext.Remove(userProfileData); + } + + while (_dbContext.Files.Any(f => f.Uploader == user)) + { + 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 == user.UID).AsNoTracking().ToListAsync().ConfigureAwait(false); + foreach (var pair in otherPairData) + { + await Clients.User(pair.UserUID).Client_UserRemoveClientPair(new(user.ToUserData())).ConfigureAwait(false); + } + + foreach (var pair in groupPairs) + { + await UserLeaveGroup(new GroupDto(new GroupData(pair.GroupGID)), user.UID).ConfigureAwait(false); + } + + _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); + + _dbContext.ClientPairs.RemoveRange(otherPairData); + _dbContext.Users.Remove(user); + _dbContext.Auth.Remove(auth); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } private async Task> GetAllPairedClientsWithPauseState(string? uid = null) @@ -79,43 +106,21 @@ public partial class MareHub return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList(); } - private async Task> SendOnlineToAllPairedUsers() + private async Task> GetOnlineUsers(List uids) { - var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); - var self = await _dbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); - await Clients.Users(usersToSendDataTo).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); - - return usersToSendDataTo; + var result = await _redis.GetAllAsync(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false); + return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal); } - private async Task> SendOfflineToAllPairedUsers() + private async Task GetUserIdent(string uid) { - var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); - var self = await _dbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); - await Clients.Users(usersToSendDataTo).Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false); - - return usersToSendDataTo; + if (uid.IsNullOrEmpty()) return string.Empty; + return await _redis.GetAsync("UID:" + uid).ConfigureAwait(false); } - public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims"); - public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims"); - - private async Task UserGroupLeave(GroupPair groupUserPair, List allUserPairs, string userIdent, string? uid = null) + private async Task RemoveUserFromRedis() { - uid ??= UserUID; - var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); - if (userPair != null) - { - if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return; - if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return; - } - - var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false); - if (!string.IsNullOrEmpty(groupUserIdent)) - { - await Clients.User(uid).Client_UserSendOffline(new(new(groupUserPair.GroupUserUID))).ConfigureAwait(false); - await Clients.User(groupUserPair.GroupUserUID).Client_UserSendOffline(new(new(uid))).ConfigureAwait(false); - } + await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } private async Task SendGroupDeletedToAll(List groupUsers) @@ -134,15 +139,22 @@ public partial class MareHub } } - private async Task<(bool IsValid, GroupPair ReferredPair)> TryValidateUserInGroup(string gid, string? uid = null) + private async Task> SendOfflineToAllPairedUsers() { - uid ??= UserUID; + var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var self = await _dbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + await Clients.Users(usersToSendDataTo).Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false); - var groupPair = await _dbContext.GroupPairs.Include(c => c.GroupUser) - .SingleOrDefaultAsync(g => g.GroupGID == gid && (g.GroupUserUID == uid || g.GroupUser.Alias == uid)).ConfigureAwait(false); - if (groupPair == null) return (false, null); + return usersToSendDataTo; + } - return (true, groupPair); + private async Task> SendOnlineToAllPairedUsers() + { + var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var self = await _dbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + await Clients.Users(usersToSendDataTo).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false); + + return usersToSendDataTo; } private async Task<(bool IsValid, Group ReferredGroup)> TryValidateGroupModeratorOrOwner(string gid) @@ -166,43 +178,38 @@ public partial class MareHub return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group); } - private async Task DeleteUser(User user) + private async Task<(bool IsValid, GroupPair ReferredPair)> TryValidateUserInGroup(string gid, string? uid = null) { - var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false); - var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == user.UID).ConfigureAwait(false); - var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == user.UID).ConfigureAwait(false); - var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false); + uid ??= UserUID; - if (lodestone != null) + var groupPair = await _dbContext.GroupPairs.Include(c => c.GroupUser) + .SingleOrDefaultAsync(g => g.GroupGID == gid && (g.GroupUserUID == uid || g.GroupUser.Alias == uid)).ConfigureAwait(false); + if (groupPair == null) return (false, null); + + return (true, groupPair); + } + + private async Task UpdateUserOnRedis() + { + await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + + private async Task UserGroupLeave(GroupPair groupUserPair, List allUserPairs, string userIdent, string? uid = null) + { + uid ??= UserUID; + var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair != null) { - _dbContext.Remove(lodestone); + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return; + if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return; } - while (_dbContext.Files.Any(f => f.Uploader == user)) + var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) { - await Task.Delay(1000).ConfigureAwait(false); + await Clients.User(uid).Client_UserSendOffline(new(new(groupUserPair.GroupUserUID))).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID).Client_UserSendOffline(new(new(uid))).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 == user.UID).AsNoTracking().ToListAsync().ConfigureAwait(false); - foreach (var pair in otherPairData) - { - await Clients.User(pair.UserUID).Client_UserRemoveClientPair(new(user.ToUserData())).ConfigureAwait(false); - } - - foreach (var pair in groupPairs) - { - await UserLeaveGroup(new GroupDto(new GroupData(pair.GroupGID)), user.UID).ConfigureAwait(false); - } - - _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); - - _dbContext.ClientPairs.RemoveRange(otherPairData); - _dbContext.Users.Remove(user); - _dbContext.Auth.Remove(auth); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); } private async Task UserLeaveGroup(GroupDto dto, string userUid) @@ -272,4 +279,4 @@ public partial class MareHub await UserGroupLeave(groupUserPair, allUserPairs, ident, userUid).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs index f1289f7..d31c88a 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs @@ -14,140 +14,8 @@ namespace MareSynchronosServer.Hubs; public partial class MareHub { - [Authorize(Policy = "Identified")] - public async Task UserDelete() - { - _logger.LogCallInfo(); - - var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); - var secondaryUsers = await _dbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); - foreach (var user in secondaryUsers) - { - await DeleteUser(user).ConfigureAwait(false); - } - - await DeleteUser(userEntry).ConfigureAwait(false); - } - - [Authorize(Policy = "Identified")] - public async Task> UserGetOnlinePairs() - { - _logger.LogCallInfo(); - - var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); - var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); - - return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList(); - } - - [Authorize(Policy = "Identified")] - public async Task> UserGetPairedClients() - { - _logger.LogCallInfo(); - - 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 == UserUID - select new - { - userToOther.OtherUser.Alias, - userToOther.IsPaused, - OtherIsPaused = otherEntry != null && otherEntry.IsPaused, - userToOther.OtherUserUID, - IsSynced = otherEntry != null, - DisableOwnAnimations = userToOther.DisableAnimations, - DisableOwnSounds = userToOther.DisableSounds, - DisableOtherAnimations = otherEntry == null ? false : otherEntry.DisableAnimations, - DisableOtherSounds = otherEntry == null ? false : otherEntry.DisableSounds - }; - - var results = await query.AsNoTracking().ToListAsync().ConfigureAwait(false); - - return results.Select(c => - { - var ownPerm = UserPermissions.Paired; - ownPerm.SetPaused(c.IsPaused); - ownPerm.SetDisableAnimations(c.DisableOwnAnimations); - ownPerm.SetDisableSounds(c.DisableOwnSounds); - var otherPerm = UserPermissions.NoneSet; - otherPerm.SetPaired(c.IsSynced); - otherPerm.SetPaused(c.OtherIsPaused); - otherPerm.SetDisableAnimations(c.DisableOtherAnimations); - otherPerm.SetDisableSounds(c.DisableOtherSounds); - return new UserPairDto(new(c.OtherUserUID, c.Alias), ownPerm, otherPerm); - }).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(); - private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk" }; - [Authorize(Policy = "Identified")] - public async Task UserPushData(UserCharaDataMessageDto dto) - { - _logger.LogCallInfo(MareHubLogger.Args(dto.CharaData.FileReplacements.Count)); - - bool hadInvalidData = false; - List invalidGamePaths = new(); - List invalidFileSwapPaths = new(); - foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value)) - { - var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList(); - invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); - replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).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 (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath); - if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths); - if (!validHash) invalidFileSwapPaths.Add(replacement.Hash); - } - } - - if (hadInvalidData) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "One or more of your supplied mods were rejected from the server. Consult /xllog for more information.").ConfigureAwait(false); - throw new HubException("Invalid data provided, contact the appropriate mod creator to resolve those issues" - + Environment.NewLine - + string.Join(Environment.NewLine, invalidGamePaths.Select(p => "Invalid Game Path: " + p)) - + Environment.NewLine - + string.Join(Environment.NewLine, invalidFileSwapPaths.Select(p => "Invalid FileSwap Path: " + p))); - } - - var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); - var idents = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); - - var recipients = allPairedUsers.Where(f => dto.Recipients.Select(r => r.UID).Contains(f, StringComparer.Ordinal)).ToList(); - - _logger.LogCallInfo(MareHubLogger.Args(idents.Count, recipients.Count())); - - await Clients.Users(recipients).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false); - - _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); - _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipients.Count()); - } - [Authorize(Policy = "Identified")] public async Task UserAddPair(UserDto dto) { @@ -214,6 +82,255 @@ public partial class MareHub } } + [Authorize(Policy = "Identified")] + public async Task UserDelete() + { + _logger.LogCallInfo(); + + var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var secondaryUsers = await _dbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); + foreach (var user in secondaryUsers) + { + await DeleteUser(user).ConfigureAwait(false); + } + + await DeleteUser(userEntry).ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] + public async Task> UserGetOnlinePairs() + { + _logger.LogCallInfo(); + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task> UserGetPairedClients() + { + _logger.LogCallInfo(); + + 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 == UserUID + select new + { + userToOther.OtherUser.Alias, + userToOther.IsPaused, + OtherIsPaused = otherEntry != null && otherEntry.IsPaused, + userToOther.OtherUserUID, + IsSynced = otherEntry != null, + DisableOwnAnimations = userToOther.DisableAnimations, + DisableOwnSounds = userToOther.DisableSounds, + DisableOtherAnimations = otherEntry == null ? false : otherEntry.DisableAnimations, + DisableOtherSounds = otherEntry == null ? false : otherEntry.DisableSounds + }; + + var results = await query.AsNoTracking().ToListAsync().ConfigureAwait(false); + + return results.Select(c => + { + var ownPerm = UserPermissions.Paired; + ownPerm.SetPaused(c.IsPaused); + ownPerm.SetDisableAnimations(c.DisableOwnAnimations); + ownPerm.SetDisableSounds(c.DisableOwnSounds); + var otherPerm = UserPermissions.NoneSet; + otherPerm.SetPaired(c.IsSynced); + otherPerm.SetPaused(c.OtherIsPaused); + otherPerm.SetDisableAnimations(c.DisableOtherAnimations); + otherPerm.SetDisableSounds(c.DisableOtherSounds); + return new UserPairDto(new(c.OtherUserUID, c.Alias), ownPerm, otherPerm); + }).ToList(); + } + + [Authorize(Policy = "Identified")] + public async Task UserGetProfile(UserDto user) + { + _logger.LogCallInfo(MareHubLogger.Args(user)); + + var allUserPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + + if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal)) + { + return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); + } + + var data = await _dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false); + if (data == null) return new UserProfileDto(user.User, false, null, null, null); + + if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); + if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled"); + + return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription); + } + + [Authorize(Policy = "Identified")] + public async Task UserPushData(UserCharaDataMessageDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto.CharaData.FileReplacements.Count)); + + bool hadInvalidData = false; + List invalidGamePaths = new(); + List invalidFileSwapPaths = new(); + foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value)) + { + var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList(); + invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).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 (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath); + if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths); + if (!validHash) invalidFileSwapPaths.Add(replacement.Hash); + } + } + + if (hadInvalidData) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "One or more of your supplied mods were rejected from the server. Consult /xllog for more information.").ConfigureAwait(false); + throw new HubException("Invalid data provided, contact the appropriate mod creator to resolve those issues" + + Environment.NewLine + + string.Join(Environment.NewLine, invalidGamePaths.Select(p => "Invalid Game Path: " + p)) + + Environment.NewLine + + string.Join(Environment.NewLine, invalidFileSwapPaths.Select(p => "Invalid FileSwap Path: " + p))); + } + + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var idents = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + var recipients = allPairedUsers.Where(f => dto.Recipients.Select(r => r.UID).Contains(f, StringComparer.Ordinal)).ToList(); + + _logger.LogCallInfo(MareHubLogger.Args(idents.Count, recipients.Count())); + + await Clients.Users(recipients).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false); + + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipients.Count()); + } + + [Authorize(Policy = "Identified")] + public async Task UserRemovePair(UserDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return; + + // check if client pair even exists + ClientPair callerPair = + await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); + if (callerPair == null) return; + + bool callerHadPaused = callerPair.IsPaused; + + // delete from database, send update info to users pair list + _dbContext.ClientPairs.Remove(callerPair); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + + await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false); + + // check if opposite entry exists + var oppositeClientPair = OppositeEntry(dto.User.UID); + if (oppositeClientPair == null) return; + + // check if other user is online, if no then there is no need to do anything further + var otherIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); + if (otherIdent == null) return; + + // get own ident and + await Clients.User(dto.User.UID) + .Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID), + UserPermissions.NoneSet)).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, dto.User.UID, 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) + { + await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).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) + { + await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherIdent)).ConfigureAwait(false); + await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); + } + } + + [Authorize(Policy = "Identified")] + public async Task UserReportProfile(UserProfileReportDto dto) + { + _logger.LogCallInfo(MareHubLogger.Args(dto)); + + UserProfileDataReport report = await _dbContext.UserProfileReports.SingleOrDefaultAsync(u => u.ReportedUserUID == dto.User.UID && u.ReportingUserUID == UserUID).ConfigureAwait(false); + if (report != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You already reported this profile and it's pending validation").ConfigureAwait(false); + return; + } + + UserProfileData profile = await _dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); + if (profile == null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "This user has no profile").ConfigureAwait(false); + return; + } + + UserProfileDataReport reportToAdd = new() + { + ReportDate = DateTime.UtcNow, + ReportingUserUID = UserUID, + ReportReason = dto.ProfileReport, + ReportedUserUID = dto.User.UID, + }; + + profile.FlaggedForReport = true; + + await _dbContext.UserProfileReports.AddAsync(reportToAdd).ConfigureAwait(false); + + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.User(dto.User.UID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Your Mare profile has been reported and disabled for admin validation").ConfigureAwait(false); + + var allPairedUsers = await GetAllPairedUnpausedUsers(dto.User.UID).ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); + + await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + await Clients.Users(dto.User.UID).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + } + [Authorize(Policy = "Identified")] public async Task UserSetPairPermissions(UserPermissionsDto dto) { @@ -262,67 +379,94 @@ public partial class MareHub } [Authorize(Policy = "Identified")] - public async Task UserRemovePair(UserDto dto) + public async Task UserSetProfile(UserProfileDto dto) { _logger.LogCallInfo(MareHubLogger.Args(dto)); - if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return; + if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); - // check if client pair even exists - ClientPair callerPair = - await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); - if (callerPair == null) return; + var existingData = await _dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); - bool callerHadPaused = callerPair.IsPaused; + if (existingData.FlaggedForReport) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false); + return; + } + + if (existingData.ProfileDisabled) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); + return; + } + + if (!string.IsNullOrEmpty(dto.ProfilePictureBase64)) + { + byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64); + using MemoryStream ms = new(imageData); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); + return; + } + using var image = Image.Load(imageData); + + if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false); + return; + } + } + + if (existingData != null) + { + if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase)) + { + existingData.Base64ProfileImage = null; + } + else if (dto.ProfilePictureBase64 != null) + { + existingData.Base64ProfileImage = dto.ProfilePictureBase64; + } + + if (dto.IsNSFW != null) + { + existingData.IsNSFW = dto.IsNSFW.Value; + } + + if (dto.Description != null) + { + existingData.UserDescription = dto.Description; + } + } + else + { + UserProfileData userProfileData = new() + { + UserUID = dto.User.UID, + Base64ProfileImage = dto.ProfilePictureBase64 ?? null, + UserDescription = dto.Description ?? null, + IsNSFW = dto.IsNSFW ?? false + }; + + await _dbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false); + } - // delete from database, send update info to users pair list - _dbContext.ClientPairs.Remove(callerPair); await _dbContext.SaveChangesAsync().ConfigureAwait(false); - _logger.LogCallInfo(MareHubLogger.Args(dto, "Success")); + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); - await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false); - - // check if opposite entry exists - var oppositeClientPair = OppositeEntry(dto.User.UID); - if (oppositeClientPair == null) return; - - // check if other user is online, if no then there is no need to do anything further - var otherIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); - if (otherIdent == null) return; - - // get own ident and - await Clients.User(dto.User.UID) - .Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID), - UserPermissions.NoneSet)).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, dto.User.UID, 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) - { - await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false); - await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).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) - { - await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherIdent)).ConfigureAwait(false); - await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false); - } + await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); + await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); } + [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GamePathRegex(); + + [GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex HashRegex(); + private ClientPair OppositeEntry(string otherUID) => _dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == UserUID); -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index 267fb08..a526451 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -28,10 +28,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs index ded9cb0..68f0417 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs @@ -2,32 +2,42 @@ using Discord.Interactions; using Discord.Rest; using Discord.WebSocket; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto.User; +using MareSynchronos.API.SignalR; +using MareSynchronosServer.Hubs; using MareSynchronosShared.Data; using MareSynchronosShared.Services; using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; +using System.Text; namespace MareSynchronosServices.Discord; internal class DiscordBot : IHostedService { private readonly DiscordBotServices _botServices; - private readonly IServiceProvider _services; private readonly IConfigurationService _configurationService; - private readonly ILogger _logger; private readonly IConnectionMultiplexer _connectionMultiplexer; private readonly DiscordSocketClient _discordClient; + private readonly ILogger _logger; + private readonly IHubContext _mareHubContext; + private readonly IServiceProvider _services; + private InteractionService _interactionModule; + private CancellationTokenSource? _processReportQueueCts; private CancellationTokenSource? _updateStatusCts; private CancellationTokenSource? _vanityUpdateCts; - private InteractionService _interactionModule; public DiscordBot(DiscordBotServices botServices, IServiceProvider services, IConfigurationService configuration, + IHubContext mareHubContext, ILogger logger, IConnectionMultiplexer connectionMultiplexer) { _botServices = botServices; _services = services; _configurationService = configuration; + _mareHubContext = mareHubContext; _logger = logger; _connectionMultiplexer = connectionMultiplexer; _discordClient = new(new DiscordSocketConfig() @@ -38,12 +48,131 @@ internal class DiscordBot : IHostedService _discordClient.Log += Log; } + public async Task StartAsync(CancellationToken cancellationToken) + { + var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty); + if (!string.IsNullOrEmpty(token)) + { + _interactionModule = new InteractionService(_discordClient); + await _interactionModule.AddModuleAsync(typeof(MareModule), _services).ConfigureAwait(false); + + await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await _discordClient.StartAsync().ConfigureAwait(false); + + _discordClient.Ready += DiscordClient_Ready; + _discordClient.ButtonExecuted += ButtonExecutedHandler; + _discordClient.InteractionCreated += async (x) => + { + var ctx = new SocketInteractionContext(_discordClient, x); + await _interactionModule.ExecuteCommandAsync(ctx, _services); + }; + + await _botServices.Start(); + _ = UpdateStatusAsync(); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty))) + { + _discordClient.ButtonExecuted -= ButtonExecutedHandler; + + await _botServices.Stop(); + _processReportQueueCts?.Cancel(); + _updateStatusCts?.Cancel(); + _vanityUpdateCts?.Cancel(); + + await _discordClient.LogoutAsync().ConfigureAwait(false); + await _discordClient.StopAsync().ConfigureAwait(false); + } + } + + private async Task ButtonExecutedHandler(SocketMessageComponent arg) + { + var id = arg.Data.CustomId; + if (!id.StartsWith("mare-report-button", StringComparison.Ordinal)) return; + + var userId = arg.User.Id; + using var scope = _services.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + var user = await dbContext.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == userId).ConfigureAwait(false); + + if (user == null || (!user.User.IsModerator && !user.User.IsAdmin)) + { + EmbedBuilder eb = new(); + eb.WithTitle($"Cannot resolve report"); + eb.WithDescription($"<@{userId}>: You have no rights to resolve this report"); + await arg.RespondAsync(embed: eb.Build()).ConfigureAwait(false); + return; + } + + id = id.Remove(0, "mare-report-button-".Length); + var split = id.Split('-', StringSplitOptions.RemoveEmptyEntries); + + var profile = await dbContext.UserProfileData.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); + + var embed = arg.Message.Embeds.First(); + + var builder = embed.ToEmbedBuilder(); + var otherPairs = await dbContext.ClientPairs.Where(p => p.UserUID == split[1]).Select(p => p.OtherUserUID).ToListAsync().ConfigureAwait(false); + switch (split[0]) + { + case "dismiss": + builder.AddField("Resolution", $"Dismissed by <@{userId}>"); + builder.WithColor(Color.Green); + profile.FlaggedForReport = false; + await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), + MessageSeverity.Warning, "The Mare profile report against you has been evaluated and your profile re-enabled.") + .ConfigureAwait(false); + break; + + case "banprofile": + builder.AddField("Resolution", $"Profile has been banned by <@{userId}>"); + builder.WithColor(Color.Red); + profile.Base64ProfileImage = null; + profile.UserDescription = null; + profile.ProfileDisabled = true; + profile.FlaggedForReport = false; + await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), + MessageSeverity.Warning, "The Mare profile report against you has been evaluated and the profile functionality permanently disabled.") + .ConfigureAwait(false); + break; + + case "banuser": + builder.AddField("Resolution", $"User has been banned by <@{userId}>"); + builder.WithColor(Color.DarkRed); + var offendingUser = await dbContext.Auth.SingleAsync(u => u.UserUID == split[1]).ConfigureAwait(false); + offendingUser.IsBanned = true; + profile.Base64ProfileImage = null; + profile.UserDescription = null; + profile.ProfileDisabled = true; + await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_ReceiveServerMessage), + MessageSeverity.Warning, "The Mare profile report against you has been evaluated and your account permanently banned.") + .ConfigureAwait(false); + break; + } + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + await _mareHubContext.Clients.Users(otherPairs).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); + await _mareHubContext.Clients.User(split[1]).SendAsync(nameof(IMareHub.Client_UserUpdateProfile), new UserDto(new(split[1]))).ConfigureAwait(false); + + await arg.Message.ModifyAsync(msg => + { + msg.Content = arg.Message.Content; + msg.Components = null; + msg.Embed = new Optional(builder.Build()); + }).ConfigureAwait(false); + } + private async Task DiscordClient_Ready() { var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); await _interactionModule.RegisterCommandsToGuildAsync(guild.Id, true).ConfigureAwait(false); _ = RemoveUsersNotInVanityRole(); + _ = ProcessReportsQueue(); } private Task Log(LogMessage msg) @@ -53,15 +182,107 @@ internal class DiscordBot : IHostedService return Task.CompletedTask; } + private async Task ProcessReportsQueue() + { + var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); + + _processReportQueueCts?.Cancel(); + _processReportQueueCts?.Dispose(); + _processReportQueueCts = new(); + var token = _processReportQueueCts.Token; + while (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (_discordClient.ConnectionState != ConnectionState.Connected) continue; + var reportChannelId = _configurationService.GetValue(nameof(ServicesConfiguration.DiscordChannelForReports)); + if (reportChannelId == null) continue; + + try + { + using (var scope = _services.CreateScope()) + { + _logger.LogInformation("Checking for Profile Reports"); + var dbContext = scope.ServiceProvider.GetRequiredService(); + if (!dbContext.UserProfileReports.Any()) + { + continue; + } + + var reports = await dbContext.UserProfileReports.ToListAsync().ConfigureAwait(false); + var restChannel = await guild.GetTextChannelAsync(reportChannelId.Value).ConfigureAwait(false); + + foreach (var report in reports) + { + var reportedUser = await dbContext.Users.SingleAsync(u => u.UID == report.ReportedUserUID).ConfigureAwait(false); + var reportedUserLodestone = await dbContext.LodeStoneAuth.SingleOrDefaultAsync(u => u.User.UID == report.ReportedUserUID).ConfigureAwait(false); + var reportingUser = await dbContext.Users.SingleAsync(u => u.UID == report.ReportingUserUID).ConfigureAwait(false); + var reportingUserLodestone = await dbContext.LodeStoneAuth.SingleOrDefaultAsync(u => u.User.UID == report.ReportingUserUID).ConfigureAwait(false); + var reportedUserProfile = await dbContext.UserProfileData.SingleAsync(u => u.UserUID == report.ReportedUserUID).ConfigureAwait(false); + EmbedBuilder eb = new(); + eb.WithTitle("Mare Synchronos Profile Report"); + + StringBuilder reportedUserSb = new(); + StringBuilder reportingUserSb = new(); + reportedUserSb.Append(reportedUser.UID); + reportingUserSb.Append(reportingUser.UID); + if (reportedUserLodestone != null) + { + reportedUserSb.AppendLine($" (<@{reportedUserLodestone.DiscordId}>)"); + } + if (reportingUserLodestone != null) + { + reportingUserSb.AppendLine($" (<@{reportingUserLodestone.DiscordId}>)"); + } + eb.AddField("Reported User", reportedUserSb.ToString()); + eb.AddField("Reporting User", reportingUserSb.ToString()); + eb.AddField("Report Date (UTC)", report.ReportDate); + eb.AddField("Report Reason", report.ReportReason ?? "-"); + eb.AddField("Reported User Profile Description", string.IsNullOrEmpty(reportedUserProfile.UserDescription) ? "-" : reportedUserProfile.UserDescription); + eb.AddField("Reported User Profile Is NSFW", reportedUserProfile.IsNSFW); + + var cb = new ComponentBuilder(); + cb.WithButton("Dismiss Report", customId: $"mare-report-button-dismiss-{reportedUser.UID}", style: ButtonStyle.Primary); + cb.WithButton("Ban profile", customId: $"mare-report-button-banprofile-{reportedUser.UID}", style: ButtonStyle.Secondary); + cb.WithButton("Ban user", customId: $"mare-report-button-banuser-{reportedUser.UID}", style: ButtonStyle.Danger); + + if (!string.IsNullOrEmpty(reportedUserProfile.Base64ProfileImage)) + { + var fileName = reportedUser.UID + "_profile_" + Guid.NewGuid().ToString("N") + ".png"; + eb.WithImageUrl($"attachment://{fileName}"); + using MemoryStream ms = new(Convert.FromBase64String(reportedUserProfile.Base64ProfileImage)); + await restChannel.SendFileAsync(ms, fileName, "User Report", embed: eb.Build(), components: cb.Build(), isSpoiler: true).ConfigureAwait(false); + } + else + { + var msg = await restChannel.SendMessageAsync(embed: eb.Build(), components: cb.Build()).ConfigureAwait(false); + } + + dbContext.Remove(report); + } + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to process reports"); + } + } + } + private async Task RemoveUsersNotInVanityRole() { + _vanityUpdateCts?.Cancel(); + _vanityUpdateCts?.Dispose(); _vanityUpdateCts = new(); + var token = _vanityUpdateCts.Token; var guild = (await _discordClient.Rest.GetGuildsAsync()).First(); var commands = await guild.GetApplicationCommandsAsync(); var appId = await _discordClient.GetApplicationInfoAsync().ConfigureAwait(false); var vanityCommandId = commands.First(c => c.ApplicationId == appId.Id && c.Name == "setvanityuid").Id; - while (!_vanityUpdateCts.IsCancellationRequested) + while (!token.IsCancellationRequested) { try { @@ -179,40 +400,4 @@ internal class DiscordBot : IHostedService await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); } } - - public async Task StartAsync(CancellationToken cancellationToken) - { - var token = _configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty); - if (!string.IsNullOrEmpty(token)) - { - _interactionModule = new InteractionService(_discordClient); - await _interactionModule.AddModuleAsync(typeof(MareModule), _services).ConfigureAwait(false); - - await _discordClient.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); - await _discordClient.StartAsync().ConfigureAwait(false); - - _discordClient.Ready += DiscordClient_Ready; - _discordClient.InteractionCreated += async (x) => - { - var ctx = new SocketInteractionContext(_discordClient, x); - await _interactionModule.ExecuteCommandAsync(ctx, _services); - }; - - await _botServices.Start(); - _ = UpdateStatusAsync(); - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - if (!string.IsNullOrEmpty(_configurationService.GetValueOrDefault(nameof(ServicesConfiguration.DiscordBotToken), string.Empty))) - { - await _botServices.Stop(); - _updateStatusCts?.Cancel(); - _vanityUpdateCts?.Cancel(); - - await _discordClient.LogoutAsync().ConfigureAwait(false); - await _discordClient.StopAsync().ConfigureAwait(false); - } - } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs index 0d99a53..7c8cc94 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBotServices.cs @@ -5,16 +5,12 @@ namespace MareSynchronosServices.Discord; public class DiscordBotServices { - public ConcurrentQueue>> VerificationQueue { get; } = new(); - public ConcurrentDictionary LastVanityChange = new(); - public ConcurrentDictionary LastVanityGidChange = new(); + public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; public ConcurrentDictionary DiscordLodestoneMapping = new(); public ConcurrentDictionary DiscordRelinkLodestoneMapping = new(); - public readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; + public ConcurrentDictionary LastVanityChange = new(); + public ConcurrentDictionary LastVanityGidChange = new(); private readonly IServiceProvider _serviceProvider; - - public ILogger Logger { get; init; } - public MareMetrics Metrics { get; init; } private CancellationTokenSource? verificationTaskCts; public DiscordBotServices(IServiceProvider serviceProvider, ILogger logger, MareMetrics metrics) @@ -24,6 +20,10 @@ public class DiscordBotServices Metrics = metrics; } + public ILogger Logger { get; init; } + public MareMetrics Metrics { get; init; } + public ConcurrentQueue>> VerificationQueue { get; } = new(); + public Task Start() { _ = ProcessVerificationQueue(); @@ -58,4 +58,4 @@ public class DiscordBotServices await Task.Delay(TimeSpan.FromSeconds(2), verificationTaskCts.Token).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/DummyHub.cs b/MareSynchronosServer/MareSynchronosServices/DummyHub.cs new file mode 100644 index 0000000..afcb665 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServices/DummyHub.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; + +// this is a very hacky way to attach this file server to the main mare hub signalr instance via redis +// signalr publishes the namespace and hubname into the redis backend so this needs to be equal to the original +// but I don't need to reimplement the hub completely as I only exclusively use it for internal connection calling +// from the queue service so I keep the namespace and name of the class the same so it can connect to the same channel +// if anyone finds a better way to do this let me know + +#pragma warning disable IDE0130 // Namespace does not match folder structure +#pragma warning disable MA0048 // File name must match type name +namespace MareSynchronosServer.Hubs; +public class MareHub : Hub +{ + public override Task OnConnectedAsync() + { + throw new NotSupportedException(); + } + + public override Task OnDisconnectedAsync(Exception exception) + { + throw new NotSupportedException(); + } +} +#pragma warning restore IDE0130 // Namespace does not match folder structure +#pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj index 04eaa9c..ea2d846 100644 --- a/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj +++ b/MareSynchronosServer/MareSynchronosServices/MareSynchronosServices.csproj @@ -32,6 +32,7 @@ + diff --git a/MareSynchronosServer/MareSynchronosServices/Startup.cs b/MareSynchronosServer/MareSynchronosServices/Startup.cs index e1336ea..a0a9b20 100644 --- a/MareSynchronosServer/MareSynchronosServices/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServices/Startup.cs @@ -8,6 +8,9 @@ using Grpc.Net.Client.Configuration; using MareSynchronosShared.Protos; using MareSynchronosShared.Services; using StackExchange.Redis; +using MessagePack.Resolvers; +using MessagePack; +using Microsoft.AspNetCore.Authorization; namespace MareSynchronosServices; @@ -20,6 +23,20 @@ public class Startup public IConfiguration Configuration { get; } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + var config = app.ApplicationServices.GetRequiredService>(); + + var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4982)); + metricServer.Start(); + + app.UseRouting(); + app.UseEndpoints(e => + { + e.MapHub("/dummyhub"); + }); + } + public void ConfigureServices(IServiceCollection services) { var mareConfig = Configuration.GetSection("MareSynchronos"); @@ -61,6 +78,35 @@ public class Startup }; }); + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + }).AddMessagePackProtocol(opt => + { + var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, + BuiltinResolver.Instance, + AttributeFormatterResolver.Instance, + // replace enum resolver + DynamicEnumAsStringResolver.Instance, + DynamicGenericResolver.Instance, + DynamicUnionResolver.Instance, + DynamicObjectResolver.Instance, + PrimitiveObjectResolver.Instance, + // final fallback(last priority) + StandardResolver.Instance); + + opt.SerializerOptions = MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4Block) + .WithResolver(resolver); + }); + + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(MareConfigurationBase.RedisConnectionString), string.Empty); + signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); + services.Configure(Configuration.GetRequiredSection("MareSynchronos")); services.Configure(Configuration.GetRequiredSection("MareSynchronos")); services.Configure(Configuration.GetRequiredSection("MareSynchronos")); @@ -75,12 +121,4 @@ public class Startup services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); services.AddHostedService(p => (MareConfigurationServiceClient)p.GetService>()); } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - var config = app.ApplicationServices.GetRequiredService>(); - - var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4982)); - metricServer.Start(); - } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs index bf92cf9..7203e0d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -31,19 +31,20 @@ public class MareDbContext : DbContext { } - public DbSet Users { get; set; } - public DbSet Files { get; set; } - public DbSet ClientPairs { get; set; } - public DbSet ForbiddenUploadEntries { get; set; } - public DbSet BannedUsers { get; set; } public DbSet Auth { get; set; } - public DbSet LodeStoneAuth { get; set; } public DbSet BannedRegistrations { get; set; } - public DbSet Groups { get; set; } - public DbSet GroupPairs { get; set; } + public DbSet BannedUsers { get; set; } + public DbSet ClientPairs { get; set; } + public DbSet Files { get; set; } + public DbSet ForbiddenUploadEntries { get; set; } public DbSet GroupBans { get; set; } + public DbSet GroupPairs { get; set; } + public DbSet Groups { get; set; } public DbSet GroupTempInvites { get; set; } - + public DbSet LodeStoneAuth { get; set; } + public DbSet UserProfileData { get; set; } + public DbSet UserProfileReports { get; set; } + public DbSet Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -73,5 +74,8 @@ public class MareDbContext : DbContext modelBuilder.Entity().HasKey(u => new { u.GroupGID, u.Invite }); modelBuilder.Entity().HasIndex(c => c.GroupGID); modelBuilder.Entity().HasIndex(c => c.Invite); + modelBuilder.Entity().ToTable("user_profile_data"); + modelBuilder.Entity().HasKey(c => c.UserUID); + modelBuilder.Entity().ToTable("user_profile_data_reports"); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj index d88f50f..d102ab2 100644 --- a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj +++ b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj @@ -32,6 +32,8 @@ + + diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs new file mode 100644 index 0000000..ea2bac1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.Designer.cs @@ -0,0 +1,584 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230319015307_UserProfileData")] + partial class UserProfileData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs new file mode 100644 index 0000000..6b4bb1a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319015307_UserProfileData.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class UserProfileData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user_profile_data", + columns: table => new + { + user_uid = table.Column(type: "character varying(10)", nullable: false), + base64profile_image = table.Column(type: "text", nullable: true), + user_description = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_profile_data", x => x.user_uid); + table.ForeignKey( + name: "fk_user_profile_data_users_user_uid", + column: x => x.user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_profile_data"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs new file mode 100644 index 0000000..4fd766a --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.Designer.cs @@ -0,0 +1,650 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20230319114005_UserProfileReports")] + partial class UserProfileReports + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id1"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id2"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id3"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id8"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => + { + b.HasOne("MareSynchronosShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_temp_id5"); + + b.HasOne("MareSynchronosShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_temp_id6"); + + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_temp_id"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id7"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupTempInvite", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs new file mode 100644 index 0000000..660af57 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20230319114005_UserProfileReports.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class UserProfileReports : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "flagged_for_report", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "is_nsfw", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "profile_disabled", + table: "user_profile_data", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "user_profile_data_reports", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + report_date = table.Column(type: "timestamp with time zone", nullable: false), + reported_user_uid = table.Column(type: "character varying(10)", nullable: true), + reporting_user_uid = table.Column(type: "character varying(10)", nullable: true), + report_reason = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_profile_data_reports", x => x.id); + table.ForeignKey( + name: "fk_user_profile_data_reports_users_reported_user_uid", + column: x => x.reported_user_uid, + principalTable: "users", + principalColumn: "uid"); + table.ForeignKey( + name: "fk_user_profile_data_reports_users_reporting_user_uid", + column: x => x.reporting_user_uid, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateIndex( + name: "ix_user_profile_data_reports_reported_user_uid", + table: "user_profile_data_reports", + column: "reported_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_user_profile_data_reports_reporting_user_uid", + table: "user_profile_data_reports", + column: "reporting_user_uid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_profile_data_reports"); + + migrationBuilder.DropColumn( + name: "flagged_for_report", + table: "user_profile_data"); + + migrationBuilder.DropColumn( + name: "is_nsfw", + table: "user_profile_data"); + + migrationBuilder.DropColumn( + name: "profile_disabled", + table: "user_profile_data"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs index d5608df..7e89dc0 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace MareSynchronosServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("ProductVersion", "7.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -415,6 +415,75 @@ namespace MareSynchronosServer.Migrations b.ToTable("users", (string)null); }); + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ReportDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("report_date"); + + b.Property("ReportReason") + .HasColumnType("text") + .HasColumnName("report_reason"); + + b.Property("ReportedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reported_user_uid"); + + b.Property("ReportingUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("reporting_user_uid"); + + b.HasKey("Id") + .HasName("pk_user_profile_data_reports"); + + b.HasIndex("ReportedUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reported_user_uid"); + + b.HasIndex("ReportingUserUID") + .HasDatabaseName("ix_user_profile_data_reports_reporting_user_uid"); + + b.ToTable("user_profile_data_reports", (string)null); + }); + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => { b.HasOne("MareSynchronosShared.Models.User", "PrimaryUser") @@ -543,6 +612,35 @@ namespace MareSynchronosServer.Migrations b.Navigation("User"); }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.UserProfileDataReport", b => + { + b.HasOne("MareSynchronosShared.Models.User", "ReportedUser") + .WithMany() + .HasForeignKey("ReportedUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reported_user_uid"); + + b.HasOne("MareSynchronosShared.Models.User", "ReportingUser") + .WithMany() + .HasForeignKey("ReportingUserUID") + .HasConstraintName("fk_user_profile_data_reports_users_reporting_user_uid"); + + b.Navigation("ReportedUser"); + + b.Navigation("ReportingUser"); + }); #pragma warning restore 612, 618 } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs new file mode 100644 index 0000000..9d8e848 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileData.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MareSynchronosShared.Models; + +public class UserProfileData +{ + public string Base64ProfileImage { get; set; } + public bool FlaggedForReport { get; set; } + public bool IsNSFW { get; set; } + public bool ProfileDisabled { get; set; } + public User User { get; set; } + + public string UserDescription { get; set; } + + [Required] + [Key] + [ForeignKey(nameof(User))] + public string UserUID { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs new file mode 100644 index 0000000..cd53b76 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/UserProfileDataReport.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MareSynchronosShared.Models; + +public class UserProfileDataReport +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public DateTime ReportDate { get; set; } + public User ReportedUser { get; set; } + + [ForeignKey(nameof(ReportedUser))] + public string ReportedUserUID { get; set; } + + public User ReportingUser { get; set; } + + [ForeignKey(nameof(ReportingUser))] + public string ReportingUserUID { get; set; } + + public string ReportReason { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationBase.cs b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationBase.cs index 2f9f6f8..844a396 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationBase.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationBase.cs @@ -6,11 +6,12 @@ namespace MareSynchronosShared.Utils; public class MareConfigurationBase : IMareConfiguration { - public Uri MainServerAddress { get; set; } public int DbContextPoolSize { get; set; } = 100; - public string ShardName { get; set; } = string.Empty; - public int MetricsPort { get; set; } public string Jwt { get; set; } = string.Empty; + public Uri MainServerAddress { get; set; } + public int MetricsPort { get; set; } + public string RedisConnectionString { get; set; } = string.Empty; + public string ShardName { get; set; } = string.Empty; public T GetValue(string key) { @@ -41,6 +42,7 @@ public class MareConfigurationBase : IMareConfiguration StringBuilder sb = new(); sb.AppendLine(base.ToString()); sb.AppendLine($"{nameof(MainServerAddress)} => {MainServerAddress}"); + sb.AppendLine($"{nameof(RedisConnectionString)} => {RedisConnectionString}"); sb.AppendLine($"{nameof(ShardName)} => {ShardName}"); sb.AppendLine($"{nameof(DbContextPoolSize)} => {DbContextPoolSize}"); return sb.ToString(); diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs index ac584c9..2b8d668 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/ServerConfiguration.cs @@ -4,23 +4,29 @@ namespace MareSynchronosShared.Utils; public class ServerConfiguration : MareConfigurationAuthBase { - public string RedisConnectionString { get; set; } = string.Empty; - public int RedisPool { get; set; } = 50; - [RemoteConfiguration] - public Version ExpectedClientVersion { get; set; } = new Version(0, 0, 0); [RemoteConfiguration] public Uri CdnFullUrl { get; set; } = null; + + [RemoteConfiguration] + public Version ExpectedClientVersion { get; set; } = new Version(0, 0, 0); + [RemoteConfiguration] public int MaxExistingGroupsByUser { get; set; } = 3; - [RemoteConfiguration] - public int MaxJoinedGroupsByUser { get; set; } = 6; + [RemoteConfiguration] public int MaxGroupUserCount { get; set; } = 100; + + [RemoteConfiguration] + public int MaxJoinedGroupsByUser { get; set; } = 6; + [RemoteConfiguration] public bool PurgeUnusedAccounts { get; set; } = false; + [RemoteConfiguration] public int PurgeUnusedAccountsPeriodInDays { get; set; } = 14; + public int RedisPool { get; set; } = 50; + public override string ToString() { StringBuilder sb = new(); @@ -36,4 +42,4 @@ public class ServerConfiguration : MareConfigurationAuthBase sb.AppendLine($"{nameof(PurgeUnusedAccountsPeriodInDays)} => {PurgeUnusedAccountsPeriodInDays}"); return sb.ToString(); } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/ServicesConfiguration.cs b/MareSynchronosServer/MareSynchronosShared/Utils/ServicesConfiguration.cs index 5768edc..8481c46 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/ServicesConfiguration.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/ServicesConfiguration.cs @@ -5,8 +5,9 @@ namespace MareSynchronosShared.Utils; public class ServicesConfiguration : MareConfigurationBase { public string DiscordBotToken { get; set; } = string.Empty; - public Uri MainServerGrpcAddress { get; set; } = null; public ulong? DiscordChannelForMessages { get; set; } = null; + public ulong? DiscordChannelForReports { get; set; } = null; + public Uri MainServerGrpcAddress { get; set; } = null; public override string ToString() { @@ -14,6 +15,8 @@ public class ServicesConfiguration : MareConfigurationBase sb.AppendLine(base.ToString()); sb.AppendLine($"{nameof(DiscordBotToken)} => {DiscordBotToken}"); sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}"); + sb.AppendLine($"{nameof(DiscordChannelForMessages)} => {DiscordChannelForMessages}"); + sb.AppendLine($"{nameof(DiscordChannelForReports)} => {DiscordChannelForReports}"); return sb.ToString(); } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs index 98c3e5d..5dcfae9 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs @@ -7,17 +7,54 @@ namespace MareSynchronosShared.Utils; public static class SharedDbFunctions { + public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List groupPairs, int maxGroupsByUser) + { + bool groupHasMigrated = false; + string newOwner = string.Empty; + foreach (var potentialNewOwner in groupPairs.OrderByDescending(p => p.IsModerator).ThenByDescending(p => p.IsPinned).ToList()) + { + groupHasMigrated = await TryMigrateGroup(context, group, potentialNewOwner.GroupUserUID, maxGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated) + { + newOwner = potentialNewOwner.GroupUserUID; + potentialNewOwner.IsPinned = true; + potentialNewOwner.IsModerator = false; + + await context.SaveChangesAsync().ConfigureAwait(false); + break; + } + } + + if (!groupHasMigrated) + { + context.GroupPairs.RemoveRange(groupPairs); + context.Groups.Remove(group); + + await context.SaveChangesAsync().ConfigureAwait(false); + } + + return (groupHasMigrated, newOwner); + } + public static async Task PurgeUser(ILogger _logger, User user, MareDbContext dbContext, int maxGroupsByUser) { _logger.LogInformation("Purging user: {uid}", user.UID); var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); + var userProfileData = await dbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false); + if (lodestone != null) { dbContext.Remove(lodestone); } + if (userProfileData != null) + { + dbContext.Remove(userProfileData); + } + var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); @@ -63,36 +100,6 @@ public static class SharedDbFunctions await dbContext.SaveChangesAsync().ConfigureAwait(false); } - public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List groupPairs, int maxGroupsByUser) - { - bool groupHasMigrated = false; - string newOwner = string.Empty; - foreach (var potentialNewOwner in groupPairs.OrderByDescending(p => p.IsModerator).ThenByDescending(p => p.IsPinned).ToList()) - { - groupHasMigrated = await TryMigrateGroup(context, group, potentialNewOwner.GroupUserUID, maxGroupsByUser).ConfigureAwait(false); - - if (groupHasMigrated) - { - newOwner = potentialNewOwner.GroupUserUID; - potentialNewOwner.IsPinned = true; - potentialNewOwner.IsModerator = false; - - await context.SaveChangesAsync().ConfigureAwait(false); - break; - } - } - - if (!groupHasMigrated) - { - context.GroupPairs.RemoveRange(groupPairs); - context.Groups.Remove(group); - - await context.SaveChangesAsync().ConfigureAwait(false); - } - - return (groupHasMigrated, newOwner); - } - private static async Task TryMigrateGroup(MareDbContext context, Group group, string potentialNewOwnerUid, int maxGroupsByUser) { var newOwnerOwnedGroups = await context.Groups.CountAsync(g => g.OwnerUID == potentialNewOwnerUid).ConfigureAwait(false); @@ -105,4 +112,4 @@ public static class SharedDbFunctions await context.SaveChangesAsync().ConfigureAwait(false); return true; } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj index 9962a4d..c4688b0 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj @@ -23,8 +23,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - -