From 47a94cb79f7a7290e8ae32f95b25c87dac8b5783 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:20:26 +0900 Subject: [PATCH] shivering my timbers --- LightlessAPI | 2 +- .../Hubs/LightlessHub.User.cs | 139 +++++++++++++++++- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 44fbe10..fbbd311 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 44fbe1045872fcae4df45e43625a9ff1a79bc2ef +Subproject commit fbbd311f18c53055f839cc0d2669e53cdefc1456 diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index cf64d96..efa8c09 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -72,7 +72,7 @@ public partial class LightlessHub var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false); var permissions = existingData?.OwnPermissions; - if (permissions == null || !permissions.Sticky) + if (permissions == null || !permissions.Sticky) { var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); @@ -523,6 +523,52 @@ public partial class LightlessHub } } + private async Task<(BroadcastRedisEntry? Entry, TimeSpan? Expiry)> TryGetBroadcastEntryAsync(string hashedCid) + { + var key = _broadcastConfiguration.BuildRedisKey(hashedCid); + RedisValueWithExpiry value; + + try + { + value = await _redis.Database.StringGetWithExpiryAsync(key).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileLookupFailed", "CID", hashedCid, "Error", ex)); + return (null, null); + } + + if (value.Value.IsNullOrEmpty || value.Expiry is null || value.Expiry <= TimeSpan.Zero) + { + return (null, value.Expiry); + } + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(value.Value!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileDeserializeFailed", "CID", hashedCid, "Raw", value.Value.ToString(), "Error", ex)); + return (null, value.Expiry); + } + + if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileEntryMismatch", "CID", hashedCid, "EntryCID", entry?.HashedCID ?? "null")); + return (null, value.Expiry); + } + + return (entry, value.Expiry); + } + + private static bool HasActiveBroadcast(BroadcastRedisEntry? entry, TimeSpan? expiry) => + entry?.HasOwner() == true && expiry.HasValue && expiry.Value > TimeSpan.Zero; + + private static bool IsActiveBroadcastForUser(BroadcastRedisEntry? entry, TimeSpan? expiry, string userUid) => + HasActiveBroadcast(entry, expiry) && entry!.OwnedBy(userUid); + private static bool IsValidHashedCid(string? cid) { if (string.IsNullOrWhiteSpace(cid)) @@ -792,6 +838,97 @@ public partial class LightlessHub return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription); } + [Authorize(Policy = "Identified")] + public async Task UserGetLightfinderProfile(string hashedCid) + { + _logger.LogCallInfo(LightlessHubLogger.Args("LightfinderProfile", hashedCid)); + + if (!_broadcastConfiguration.EnableBroadcasting) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Lightfinder is currently disabled.").ConfigureAwait(false); + return null; + } + + if (!IsValidHashedCid(hashedCid)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileInvalidCid", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Invalid Lightfinder target.").ConfigureAwait(false); + return null; + } + + var viewerCid = UserCharaIdent; + if (!IsValidHashedCid(viewerCid)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false); + return null; + } + + var (viewerEntry, viewerExpiry) = await TryGetBroadcastEntryAsync(viewerCid).ConfigureAwait(false); + if (!IsActiveBroadcastForUser(viewerEntry, viewerExpiry, UserUID)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false); + return null; + } + + var (targetEntry, targetExpiry) = await TryGetBroadcastEntryAsync(hashedCid).ConfigureAwait(false); + if (!HasActiveBroadcast(targetEntry, targetExpiry)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false); + return null; + } + + if (string.IsNullOrEmpty(targetEntry!.OwnerUID)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false); + return null; + } + + var targetUser = await DbContext.Users.AsNoTracking() + .SingleOrDefaultAsync(u => u.UID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken) + .ConfigureAwait(false); + + if (targetUser == null) + { + _logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileMissingUser", hashedCid, "OwnerUID", targetEntry.OwnerUID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Unable to load the players profile at this time.").ConfigureAwait(false); + return null; + } + + var displayAlias = string.IsNullOrWhiteSpace(targetUser.Alias) + ? "LightfinderUser" + : targetUser.Alias; + + var userData = new UserData( + UID: hashedCid, + Alias: displayAlias, + IsAdmin: false, + IsModerator: false, + HasVanity: false, + TextColorHex: targetUser.TextColorHex, + TextGlowColorHex: targetUser.TextGlowColorHex); + + var profile = await DbContext.UserProfileData.AsNoTracking() + .SingleOrDefaultAsync(u => u.UserUID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken) + .ConfigureAwait(false); + + if (profile == null) + { + return new UserProfileDto(userData, false, null, null, null); + } + + if (profile.FlaggedForReport) + { + return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation"); + } + + if (profile.ProfileDisabled) + { + return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled"); + } + + return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription); + } + [Authorize(Policy = "Identified")] public async Task UserPushData(UserCharaDataMessageDto dto) {