lightfinder!
This commit is contained in:
Submodule LightlessAPI updated: 4ce70bee83...fd4cd52d2e
@@ -323,7 +323,9 @@ public partial class LightlessHub
|
|||||||
GID = user.Gid,
|
GID = user.Gid,
|
||||||
Synced = user.Synced,
|
Synced = user.Synced,
|
||||||
OwnPermissions = ownperm,
|
OwnPermissions = ownperm,
|
||||||
OtherPermissions = otherperm
|
OtherPermissions = otherperm,
|
||||||
|
OtherUserIsAdmin = u.IsAdmin,
|
||||||
|
OtherUserIsModerator = u.IsModerator
|
||||||
};
|
};
|
||||||
|
|
||||||
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||||
@@ -336,7 +338,9 @@ public partial class LightlessHub
|
|||||||
resultList.Max(p => p.Synced),
|
resultList.Max(p => p.Synced),
|
||||||
resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
||||||
resultList[0].OwnPermissions,
|
resultList[0].OwnPermissions,
|
||||||
resultList[0].OtherPermissions);
|
resultList[0].OtherPermissions,
|
||||||
|
resultList[0].OtherUserIsAdmin,
|
||||||
|
resultList[0].OtherUserIsModerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, UserInfo>> GetAllPairInfo(string uid)
|
private async Task<Dictionary<string, UserInfo>> GetAllPairInfo(string uid)
|
||||||
@@ -408,7 +412,9 @@ public partial class LightlessHub
|
|||||||
GID = user.Gid,
|
GID = user.Gid,
|
||||||
Synced = user.Synced,
|
Synced = user.Synced,
|
||||||
OwnPermissions = ownperm,
|
OwnPermissions = ownperm,
|
||||||
OtherPermissions = otherperm
|
OtherPermissions = otherperm,
|
||||||
|
OtherUserIsAdmin = u.IsAdmin,
|
||||||
|
OtherUserIsModerator = u.IsModerator
|
||||||
};
|
};
|
||||||
|
|
||||||
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||||
@@ -419,7 +425,10 @@ public partial class LightlessHub
|
|||||||
g.Max(p => p.Synced),
|
g.Max(p => p.Synced),
|
||||||
g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(),
|
||||||
g.First().OwnPermissions,
|
g.First().OwnPermissions,
|
||||||
g.First().OtherPermissions);
|
g.First().OtherPermissions,
|
||||||
|
g.First().OtherUserIsAdmin,
|
||||||
|
g.First().OtherUserIsModerator
|
||||||
|
);
|
||||||
}, StringComparer.Ordinal);
|
}, StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,5 +493,14 @@ public partial class LightlessHub
|
|||||||
return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false);
|
return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UserInfo(string Alias, bool IndividuallyPaired, bool IsSynced, List<string> GIDs, UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions);
|
public record UserInfo(
|
||||||
|
string Alias,
|
||||||
|
bool IndividuallyPaired,
|
||||||
|
bool IsSynced,
|
||||||
|
List<string> GIDs,
|
||||||
|
UserPermissionSet? OwnPermissions,
|
||||||
|
UserPermissionSet? OtherPermissions,
|
||||||
|
bool IsAdmin,
|
||||||
|
bool IsModerator
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
using LightlessSyncShared.Utils;
|
using LightlessSyncShared.Utils;
|
||||||
@@ -319,6 +321,11 @@ public partial class LightlessHub
|
|||||||
return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true);
|
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")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<bool> GroupJoinFinalize(GroupJoinDto dto)
|
public async Task<bool> GroupJoinFinalize(GroupJoinDto dto)
|
||||||
{
|
{
|
||||||
@@ -329,7 +336,9 @@ public partial class LightlessHub
|
|||||||
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
|
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 groupGid = group?.GID ?? string.Empty;
|
||||||
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||||
var hashedPw = StringUtils.Sha256String(dto.Password);
|
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 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 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 isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
|
||||||
@@ -523,6 +532,88 @@ public partial class LightlessHub
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "Identified")]
|
||||||
|
public async Task<GroupJoinInfoDto> 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")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task GroupLeave(GroupDto dto)
|
public async Task GroupLeave(GroupDto dto)
|
||||||
{
|
{
|
||||||
@@ -669,4 +760,70 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "Identified")]
|
||||||
|
public async Task<bool> 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<List<GroupJoinDto>> GetBroadcastedGroups(List<BroadcastStatusInfoDto> broadcastEntries)
|
||||||
|
{
|
||||||
|
_logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID)));
|
||||||
|
|
||||||
|
var results = new List<GroupJoinDto>();
|
||||||
|
var gidsToValidate = new HashSet<string>(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<BroadcastRedisEntry>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
using System.Text;
|
using LightlessSync.API.Data;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using LightlessSync.API.Data;
|
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
@@ -13,6 +11,10 @@ using Microsoft.AspNetCore.SignalR;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace LightlessSyncServer.Hubs;
|
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<string>(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<string>(reverseKey).ConfigureAwait(false);
|
||||||
|
if (json != null)
|
||||||
|
{
|
||||||
|
await _redis.RemoveAsync(reverseKey).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var payload = JsonSerializer.Deserialize<PairingPayload>(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<BroadcastRedisEntry>(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<BroadcastStatusInfoDto?> 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<BroadcastRedisEntry>(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<TimeSpan?> 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<BroadcastRedisEntry>(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<BroadcastStatusBatchDto> AreUsersBroadcasting(List<string> hashedCids)
|
||||||
|
{
|
||||||
|
var db = _redis.Database;
|
||||||
|
if (hashedCids.Count > MaxBatchSize)
|
||||||
|
hashedCids = hashedCids.Take(MaxBatchSize).ToList();
|
||||||
|
|
||||||
|
var tasks = new Dictionary<string, Task<RedisValueWithExpiry>>(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<string, BroadcastStatusInfoDto>(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<BroadcastRedisEntry>(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")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task UserDelete()
|
public async Task UserDelete()
|
||||||
{
|
{
|
||||||
@@ -175,7 +493,7 @@ public partial class LightlessHub
|
|||||||
var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false);
|
var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false);
|
||||||
return pairs.Select(p =>
|
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.ToIndividualPairStatus(),
|
||||||
p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(),
|
p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(),
|
||||||
p.Value.OwnPermissions.ToUserPermissions(setSticky: true),
|
p.Value.OwnPermissions.ToUserPermissions(setSticky: true),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
private static readonly ConcurrentDictionary<string, string> _userConnections = new(StringComparer.Ordinal);
|
private static readonly ConcurrentDictionary<string, string> _userConnections = new(StringComparer.Ordinal);
|
||||||
private readonly LightlessMetrics _lightlessMetrics;
|
private readonly LightlessMetrics _lightlessMetrics;
|
||||||
private readonly SystemInfoService _systemInfoService;
|
private readonly SystemInfoService _systemInfoService;
|
||||||
|
private readonly PairService _pairService;
|
||||||
private readonly IHttpContextAccessor _contextAccessor;
|
private readonly IHttpContextAccessor _contextAccessor;
|
||||||
private readonly LightlessHubLogger _logger;
|
private readonly LightlessHubLogger _logger;
|
||||||
private readonly string _shardName;
|
private readonly string _shardName;
|
||||||
@@ -45,7 +46,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
||||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||||
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
|
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
|
||||||
GPoseLobbyDistributionService gPoseLobbyDistributionService)
|
GPoseLobbyDistributionService gPoseLobbyDistributionService, PairService pairService)
|
||||||
{
|
{
|
||||||
_lightlessMetrics = lightlessMetrics;
|
_lightlessMetrics = lightlessMetrics;
|
||||||
_systemInfoService = systemInfoService;
|
_systemInfoService = systemInfoService;
|
||||||
@@ -64,6 +65,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
||||||
_logger = new LightlessHubLogger(this, logger);
|
_logger = new LightlessHubLogger(this, logger);
|
||||||
_dbContextLazy = new Lazy<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
|
_dbContextLazy = new Lazy<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
|
||||||
|
_pairService = pairService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
108
LightlessSyncServer/LightlessSyncServer/Services/PairService.cs
Normal file
108
LightlessSyncServer/LightlessSyncServer/Services/PairService.cs
Normal file
@@ -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<LightlessDbContext> _dbFactory;
|
||||||
|
private readonly ILogger<PairService> _logger;
|
||||||
|
|
||||||
|
public PairService(IDbContextFactory<LightlessDbContext> dbFactory, ILogger<PairService> logger)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ public class Startup
|
|||||||
services.AddSingleton<CharaDataCleanupService>();
|
services.AddSingleton<CharaDataCleanupService>();
|
||||||
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
|
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
|
||||||
services.AddHostedService<ClientPairPermissionsCleanupService>();
|
services.AddHostedService<ClientPairPermissionsCleanupService>();
|
||||||
|
services.AddScoped<PairService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
services.AddSingleton<GPoseLobbyDistributionService>();
|
services.AddSingleton<GPoseLobbyDistributionService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user