diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs index fafc432..6344f40 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs @@ -323,7 +323,9 @@ public partial class LightlessHub GID = user.Gid, Synced = user.Synced, OwnPermissions = ownperm, - OtherPermissions = otherperm + OtherPermissions = otherperm, + OtherUserIsAdmin = u.IsAdmin, + OtherUserIsModerator = u.IsModerator }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); @@ -336,7 +338,9 @@ public partial class LightlessHub resultList.Max(p => p.Synced), resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), resultList[0].OwnPermissions, - resultList[0].OtherPermissions); + resultList[0].OtherPermissions, + resultList[0].OtherUserIsAdmin, + resultList[0].OtherUserIsModerator); } private async Task> GetAllPairInfo(string uid) @@ -408,7 +412,9 @@ public partial class LightlessHub GID = user.Gid, Synced = user.Synced, OwnPermissions = ownperm, - OtherPermissions = otherperm + OtherPermissions = otherperm, + OtherUserIsAdmin = u.IsAdmin, + OtherUserIsModerator = u.IsModerator }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); @@ -419,7 +425,10 @@ public partial class LightlessHub g.Max(p => p.Synced), g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), g.First().OwnPermissions, - g.First().OtherPermissions); + g.First().OtherPermissions, + g.First().OtherUserIsAdmin, + g.First().OtherUserIsModerator + ); }, StringComparer.Ordinal); } @@ -484,5 +493,14 @@ public partial class LightlessHub return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false); } - public record UserInfo(string Alias, bool IndividuallyPaired, bool IsSynced, List GIDs, UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions); + public record UserInfo( + string Alias, + bool IndividuallyPaired, + bool IsSynced, + List GIDs, + UserPermissionSet? OwnPermissions, + UserPermissionSet? OtherPermissions, + bool IsAdmin, + bool IsModerator + ); } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index d174224..1e157d1 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -1,6 +1,8 @@ -using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; @@ -322,6 +324,11 @@ public partial class LightlessHub return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); } + private static bool IsHex(char c) => + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + [Authorize(Policy = "Identified")] public async Task GroupJoinFinalize(GroupJoinDto dto) { @@ -331,12 +338,14 @@ public partial class LightlessHub var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); + var hashedPw = dto.Password.Length == 64 && dto.Password.All(IsHex) + ? dto.Password + : StringUtils.Sha256String(dto.Password); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -527,6 +536,88 @@ public partial class LightlessHub return true; } + [Authorize(Policy = "Identified")] + public async Task GroupJoinHashed(GroupJoinHashedDto dto) + { + var aliasOrGid = dto.Group.GID.Trim(); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var group = await DbContext.Groups.Include(g => g.Owner) + .AsNoTracking() + .SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid) + .ConfigureAwait(false); + + var groupGid = group?.GID ?? string.Empty; + + var existingPair = await DbContext.GroupPairs + .AsNoTracking() + .SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID) + .ConfigureAwait(false); + + var isBanned = await DbContext.GroupBans + .AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID) + .ConfigureAwait(false); + + var oneTimeInvite = await DbContext.GroupTempInvites + .SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == dto.HashedPassword) + .ConfigureAwait(false); + + var existingUserCount = await DbContext.GroupPairs + .AsNoTracking() + .CountAsync(g => g.GroupGID == groupGid) + .ConfigureAwait(false); + + var joinedGroups = await DbContext.GroupPairs + .CountAsync(g => g.GroupUserUID == UserUID) + .ConfigureAwait(false); + + if (group == null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Syncshell not found."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (!string.Equals(group.HashedPassword, dto.HashedPassword, StringComparison.Ordinal) && oneTimeInvite == null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Incorrect or expired password."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (existingPair != null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You are already a member of this syncshell."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (existingUserCount >= _maxGroupUserCount) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "This syncshell is full."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (!group.InvitesEnabled) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Invites to this syncshell are currently disabled."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (joinedGroups >= _maxJoinedGroupsByUser) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You have reached the maximum number of syncshells you can join."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (isBanned) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You are banned from this syncshell."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); + } + + [Authorize(Policy = "Identified")] public async Task GroupLeave(GroupDto dto) { @@ -742,4 +833,70 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } + + [Authorize(Policy = "Identified")] + public async Task SetGroupBroadcastStatus(GroupBroadcastRequestDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + if (string.IsNullOrEmpty(dto.HashedCID)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("missing CID in syncshell broadcast request", "User", UserUID, "GID", dto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Internal error: missing CID."); + return false; + } + + var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false); + if (!isOwner) + { + _logger.LogCallWarning(LightlessHubLogger.Args("Unauthorized syncshell broadcast change", "User", UserUID, "GID", dto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner of the syncshell to broadcast it."); + return false; + } + + return true; + } + + [Authorize(Policy = "Identified")] + public async Task> GetBroadcastedGroups(List broadcastEntries) + { + _logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID))); + + var results = new List(); + var gidsToValidate = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in broadcastEntries) + { + if (string.IsNullOrWhiteSpace(entry.HashedCID) || string.IsNullOrWhiteSpace(entry.GID)) + continue; + + var redisKey = $"broadcast:{entry.HashedCID}"; + var redisEntry = await _redis.GetAsync(redisKey).ConfigureAwait(false); + + if (redisEntry?.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase)) + gidsToValidate.Add(entry.GID); + } + + if (gidsToValidate.Count == 0) + return results; + + var groups = await DbContext.Groups + .AsNoTracking() + .Where(g => gidsToValidate.Contains(g.GID) && g.InvitesEnabled) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var group in groups) + { + results.Add(new GroupJoinDto( + Group: new GroupData(group.GID, group.Alias), + Password: group.HashedPassword, + GroupUserPreferredPermissions: new GroupUserPreferredPermissions() + )); + } + + return results; + } + + } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs index 0ea7d14..27c72eb 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs @@ -1,9 +1,7 @@ -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; @@ -13,6 +11,10 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using StackExchange.Redis; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; namespace LightlessSyncServer.Hubs; @@ -137,6 +139,322 @@ public partial class LightlessHub } } + [Authorize(Policy = "Identified")] + public async Task TryPairWithContentId(string otherCid, string myCid) + { + if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid)) + return; + + if (string.Equals(otherCid, myCid, StringComparison.Ordinal)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can't pair with yourself.").ConfigureAwait(false); + return; + } + + var throttleKey = $"pairing:rate:{UserUID}"; + var existingThrottle = await _redis.GetAsync(throttleKey).ConfigureAwait(false); + if (existingThrottle != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You're sending requests too quickly. Please wait a moment.").ConfigureAwait(false); + return; + } + await _redis.AddAsync(throttleKey, "true", TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + var reverseKey = $"pairing:{otherCid}:{myCid}"; + var forwardKey = $"pairing:{myCid}:{otherCid}"; + + var json = await _redis.GetAsync(reverseKey).ConfigureAwait(false); + if (json != null) + { + await _redis.RemoveAsync(reverseKey).ConfigureAwait(false); + + try + { + var payload = JsonSerializer.Deserialize(json); + if (payload?.UID == null) + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid payload", reverseKey)); + return; + } + + var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); + var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); + + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleAsync(u => u.UID == payload.UID).ConfigureAwait(false); + + var pairData = await GetPairInfo(UserUID, payload.UID).ConfigureAwait(false); + var permissions = await DbContext.Permissions.SingleAsync(p => + p.UserUID == UserUID && p.OtherUserUID == payload.UID).ConfigureAwait(false); + + var ownPerm = permissions.ToUserPermissions(setSticky: true); + var otherPerm = pairData?.OtherPermissions.ToUserPermissions() ?? new UserPermissions(); + + var individualPairStatus = pairData?.IsSynced == true + ? IndividualPairStatus.Bidirectional + : IndividualPairStatus.OneSided; + + var dtoA = new UserPairDto(otherUser.ToUserData(), individualPairStatus, ownPerm, otherPerm); + var dtoB = new UserPairDto(user.ToUserData(), individualPairStatus, otherPerm, ownPerm); + + await Clients.User(UserUID).Client_UserAddClientPair(dtoA).ConfigureAwait(false); + await Clients.User(payload.UID).Client_UserAddClientPair(dtoB).ConfigureAwait(false); + + await Clients.User(payload.UID) + .Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(user.ToUserData(), permissions.ToUserPermissions())) + .ConfigureAwait(false); + + await Clients.User(payload.UID) + .Client_UpdateUserIndividualPairStatusDto(new(user.ToUserData(), individualPairStatus)) + .ConfigureAwait(false); + + await Clients.User(UserUID) + .Client_UpdateUserIndividualPairStatusDto(new(otherUser.ToUserData(), individualPairStatus)) + .ConfigureAwait(false); + + if (!ownPerm.IsPaused() && !otherPerm.IsPaused()) + { + var ident_sender = await GetUserIdent(UserUID).ConfigureAwait(false); + var ident_receiver = await GetUserIdent(payload.UID).ConfigureAwait(false); + + if (ident_sender != null && ident_receiver != null) + { + await Clients.User(UserUID).Client_UserSendOnline(new(otherUser.ToUserData(), ident_receiver)).ConfigureAwait(false); + await Clients.User(payload.UID).Client_UserSendOnline(new(user.ToUserData(), ident_sender)).ConfigureAwait(false); + } + } + + if (sender || receiver) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"paired with {payload.UID}.").ConfigureAwait(false); + await Clients.User(payload.UID).Client_ReceiveServerMessage(MessageSeverity.Information, $"paired with {UserUID}.").ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args("pair established", UserUID, payload.UID)); + } + + await _redis.RemoveAsync(forwardKey).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to process pairing", reverseKey, ex.Message)); + } + } + else + { + var payload = new PairingPayload + { + UID = UserUID, + Timestamp = DateTime.UtcNow + }; + + var payloadJson = JsonSerializer.Serialize(payload); + await _redis.AddAsync(forwardKey, payloadJson, TimeSpan.FromMinutes(5)).ConfigureAwait(false); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); + } + } + + private class PairingPayload + { + public string UID { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } + } + + public class BroadcastRedisEntry + { + public string HashedCID { get; set; } = string.Empty; + public string? GID { get; set; } + } + + [Authorize(Policy = "Identified")] + public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + { + var db = _redis.Database; + var broadcastKey = $"broadcast:{hashedCid}"; + + if (enabled) + { + string? gid = null; + + if (groupDto is not null) + { + groupDto.HashedCID = hashedCid; + + var valid = await SetGroupBroadcastStatus(groupDto).ConfigureAwait(false); + if (!valid) + return; + + gid = groupDto.GID; + } + + var entry = new BroadcastRedisEntry + { + HashedCID = hashedCid, + GID = gid, + }; + + var json = JsonSerializer.Serialize(entry); + await db.StringSetAsync(broadcastKey, json, TimeSpan.FromMinutes(5)).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid)); + } + else + { + var value = await db.StringGetAsync(broadcastKey).ConfigureAwait(false); + if (value.IsNullOrEmpty) + return; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(value!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast entry during removal", "CID", hashedCid, "Value", value, "Error", ex)); + return; + } + + if (entry is null || entry.HashedCID != hashedCid) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Stored", entry?.HashedCID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + return; + } + + await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID)); + } + } + + + [Authorize(Policy = "Identified")] + public async Task IsUserBroadcasting(string hashedCid) + { + var db = _redis.Database; + var key = $"broadcast:{hashedCid}"; + + var result = await db.StringGetWithExpiryAsync(key).ConfigureAwait(false); + if (result.Expiry is null || result.Expiry <= TimeSpan.Zero || result.Value.IsNullOrEmpty) + return null; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(result.Value!); + } + catch + { + return null; + } + + var dto = new BroadcastStatusInfoDto + { + HashedCID = entry?.HashedCID ?? hashedCid, + IsBroadcasting = true, + TTL = result.Expiry, + GID = entry?.GID + }; + + _logger.LogCallInfo(LightlessHubLogger.Args("checked broadcast status", hashedCid, "TTL", result.Expiry, "GID", dto.GID)); + return dto; + } + + [Authorize(Policy = "Identified")] + public async Task GetBroadcastTtl(string hashedCid) + { + var db = _redis.Database; + var key = $"broadcast:{hashedCid}"; + + var value = await db.StringGetAsync(key).ConfigureAwait(false); + if (value.IsNullOrEmpty) + return null; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(value!); + } + catch + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid broadcast entry format", "CID", hashedCid)); + return null; + } + + if (entry?.HashedCID != hashedCid) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry?.HashedCID)); + return null; + } + + var ttl = await db.KeyTimeToLiveAsync(key).ConfigureAwait(false); + if (ttl is null || ttl <= TimeSpan.Zero) + return null; + + _logger.LogCallInfo(LightlessHubLogger.Args("checked broadcast ttl", UserUID, "CID", hashedCid, "TTL", ttl, "GID", entry.GID)); + return ttl; + } + + + private const int MaxBatchSize = 30; + + [Authorize(Policy = "Identified")] + public async Task AreUsersBroadcasting(List hashedCids) + { + var db = _redis.Database; + if (hashedCids.Count > MaxBatchSize) + hashedCids = hashedCids.Take(MaxBatchSize).ToList(); + + var tasks = new Dictionary>(hashedCids.Count); + foreach (var cid in hashedCids) + { + var key = $"broadcast:{cid}"; + tasks[cid] = db.StringGetWithExpiryAsync(key); + } + + await Task.WhenAll(tasks.Values).ConfigureAwait(false); + + var results = new Dictionary(StringComparer.Ordinal); + + foreach (var (cid, task) in tasks) + { + var result = task.Result; + var raw = result.Value; + TimeSpan? ttl = result.Expiry; + + BroadcastRedisEntry? entry = null; + string? gid = null; + bool isBroadcasting = false; + + if (!raw.IsNullOrEmpty && ttl > TimeSpan.Zero) + { + isBroadcasting = true; + + try + { + entry = JsonSerializer.Deserialize(raw!); + gid = entry?.GID; + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("deserialization failed", "CID", cid, "Raw", raw.ToString(), "Error", ex)); + } + } + + results[cid] = new BroadcastStatusInfoDto + { + HashedCID = entry?.HashedCID ?? cid, + IsBroadcasting = isBroadcasting, + TTL = ttl, + GID = gid, + }; + } + + _logger.LogCallInfo(LightlessHubLogger.Args("batch checked broadcast", "Count", hashedCids.Count)); + return new BroadcastStatusBatchDto { Results = results }; + } + + [Authorize(Policy = "Identified")] public async Task UserDelete() { @@ -175,7 +493,7 @@ public partial class LightlessHub var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false); return pairs.Select(p => { - return new UserFullPairDto(new UserData(p.Key, p.Value.Alias), + return new UserFullPairDto(new UserData(p.Key, p.Value.Alias, p.Value.IsAdmin, p.Value.IsModerator), p.Value.ToIndividualPairStatus(), p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(), p.Value.OwnPermissions.ToUserPermissions(setSticky: true), diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs index adf1fe0..17ca1e3 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs @@ -24,6 +24,7 @@ public partial class LightlessHub : Hub, ILightlessHub private static readonly ConcurrentDictionary _userConnections = new(StringComparer.Ordinal); private readonly LightlessMetrics _lightlessMetrics; private readonly SystemInfoService _systemInfoService; + private readonly PairService _pairService; private readonly IHttpContextAccessor _contextAccessor; private readonly LightlessHubLogger _logger; private readonly string _shardName; @@ -45,7 +46,7 @@ public partial class LightlessHub : Hub, ILightlessHub IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, - GPoseLobbyDistributionService gPoseLobbyDistributionService) + GPoseLobbyDistributionService gPoseLobbyDistributionService, PairService pairService) { _lightlessMetrics = lightlessMetrics; _systemInfoService = systemInfoService; @@ -64,6 +65,7 @@ public partial class LightlessHub : Hub, ILightlessHub _gPoseLobbyDistributionService = gPoseLobbyDistributionService; _logger = new LightlessHubLogger(this, logger); _dbContextLazy = new Lazy(() => lightlessDbContextFactory.CreateDbContext()); + _pairService = pairService; } protected override void Dispose(bool disposing) diff --git a/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs b/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs new file mode 100644 index 0000000..854472f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs @@ -0,0 +1,108 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +public class PairService +{ + private readonly IDbContextFactory _dbFactory; + private readonly ILogger _logger; + + public PairService(IDbContextFactory dbFactory, ILogger logger) + { + _dbFactory = dbFactory; + _logger = logger; + } + + public async Task TryAddPairAsync(string userUid, string otherUid) + { + if (userUid == otherUid || string.IsNullOrWhiteSpace(userUid) || string.IsNullOrWhiteSpace(otherUid)) + return false; + + await using var db = await _dbFactory.CreateDbContextAsync(); + + var user = await db.Users.SingleOrDefaultAsync(u => u.UID == userUid); + var other = await db.Users.SingleOrDefaultAsync(u => u.UID == otherUid); + + if (user == null || other == null) + return false; + + bool modified = false; + + if (!await db.ClientPairs.AnyAsync(p => p.UserUID == userUid && p.OtherUserUID == otherUid)) + { + db.ClientPairs.Add(new ClientPair + { + UserUID = userUid, + OtherUserUID = otherUid + }); + modified = true; + } + + if (!await db.ClientPairs.AnyAsync(p => p.UserUID == otherUid && p.OtherUserUID == userUid)) + { + db.ClientPairs.Add(new ClientPair + { + UserUID = otherUid, + OtherUserUID = userUid + }); + modified = true; + } + + if (!await db.Permissions.AnyAsync(p => p.UserUID == userUid && p.OtherUserUID == otherUid)) + { + var defaultPerms = await db.UserDefaultPreferredPermissions + .SingleOrDefaultAsync(p => p.UserUID == userUid); + + if (defaultPerms != null) + { + db.Permissions.Add(new UserPermissionSet + { + UserUID = userUid, + OtherUserUID = otherUid, + DisableAnimations = defaultPerms.DisableIndividualAnimations, + DisableSounds = defaultPerms.DisableIndividualSounds, + DisableVFX = defaultPerms.DisableIndividualVFX, + IsPaused = false, + Sticky = true, + }); + modified = true; + } + } + + if (!await db.Permissions.AnyAsync(p => p.UserUID == otherUid && p.OtherUserUID == userUid)) + { + var defaultPerms = await db.UserDefaultPreferredPermissions + .SingleOrDefaultAsync(p => p.UserUID == otherUid); + + if (defaultPerms != null) + { + db.Permissions.Add(new UserPermissionSet + { + UserUID = otherUid, + OtherUserUID = userUid, + DisableAnimations = defaultPerms.DisableIndividualAnimations, + DisableSounds = defaultPerms.DisableIndividualSounds, + DisableVFX = defaultPerms.DisableIndividualVFX, + IsPaused = false, + Sticky = true, + }); + modified = true; + } + } + + if (modified) + { + await db.SaveChangesAsync(); + _logger.LogInformation("Mutual pair established between {UserUID} and {OtherUID}", userUid, otherUid); + } + else + { + _logger.LogInformation("Pair already exists between {UserUID} and {OtherUID}", userUid, otherUid); + } + + return modified; + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Startup.cs b/LightlessSyncServer/LightlessSyncServer/Startup.cs index d9bab3f..5b0b672 100644 --- a/LightlessSyncServer/LightlessSyncServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncServer/Startup.cs @@ -105,6 +105,7 @@ public class Startup services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); services.AddHostedService(); + services.AddScoped(); } services.AddSingleton();