From 2b05223a4b4dc8926b6cca80c092c792109cdf65 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:53:47 +0900 Subject: [PATCH] added methods to update vanity colors and submodule bump --- LightlessAPI | 2 +- .../Hubs/LightlessHub.Functions.cs | 23 +++++ .../Hubs/LightlessHub.User.cs | 93 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index eb04433..b85b54f 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit eb04433427d8b5144688004e436d833c6b63d39c +Subproject commit b85b54f560d3d4d901b3af1421337a4b22b0d067 diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index b9d5db8..83a2c15 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -6,6 +6,7 @@ using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; using LightlessSyncShared.Metrics; using Microsoft.AspNetCore.SignalR; +using System.Threading; namespace LightlessSyncServer.Hubs; @@ -97,6 +98,28 @@ public partial class LightlessHub await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } + private async Task EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default) + { + cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null + ? _contextAccessor.HttpContext.RequestAborted + : cancellationToken; + + var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false); + if (user == null) + { + _logger.LogCallWarning(LightlessHubLogger.Args("vanity check", uid, "missing user")); + return null; + } + + if (user.HasVanity != true) + { + _logger.LogCallWarning(LightlessHubLogger.Args("vanity check", uid, "no vanity")); + return null; + } + + return user; + } + private async Task SendGroupDeletedToAll(List groupUsers) { foreach (var pair in groupUsers) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index b9fc652..ddd0b1b 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -605,6 +605,62 @@ public partial class LightlessHub _lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipientUids.Count); } + [Authorize(Policy = "Identified")] + public async Task UserUpdateVanityColors(UserVanityColorsDto dto) + { + if (dto == null) + { + throw new HubException("Vanity color payload required"); + } + + _logger.LogCallInfo(LightlessHubLogger.Args(dto.TextColorHex, dto.TextGlowColorHex)); + + var cooldownKey = $"vanity:colors:{UserUID}"; + var existingCooldown = await _redis.GetAsync(cooldownKey).ConfigureAwait(false); + if (existingCooldown != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can update vanity colors once per minute.").ConfigureAwait(false); + return; + } + + var user = await EnsureUserHasVanity(UserUID).ConfigureAwait(false); + if (user == null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Vanity privileges are required to update colors.").ConfigureAwait(false); + return; + } + + if (!TryNormalizeColor(dto.TextColorHex, out var textColor, out var textColorError)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, textColorError).ConfigureAwait(false); + return; + } + + if (!TryNormalizeColor(dto.TextGlowColorHex, out var textGlowColor, out var textGlowError)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, textGlowError).ConfigureAwait(false); + return; + } + + var currentColor = user.TextColorHex ?? string.Empty; + var currentGlow = user.TextGlowColorHex ?? string.Empty; + + if (string.Equals(currentColor, textColor, StringComparison.Ordinal) && + string.Equals(currentGlow, textGlowColor, StringComparison.Ordinal)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Vanity colors are already set to these values.").ConfigureAwait(false); + return; + } + + user.TextColorHex = textColor; + user.TextGlowColorHex = textGlowColor; + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + await _redis.AddAsync(cooldownKey, "true", TimeSpan.FromMinutes(1)).ConfigureAwait(false); + + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Vanity colors updated.").ConfigureAwait(false); + } + [Authorize(Policy = "Identified")] public async Task UserRemovePair(UserDto dto) { @@ -741,6 +797,43 @@ public partial class LightlessHub await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); } + private static bool TryNormalizeColor(string? value, out string normalized, out string errorMessage) + { + if (string.IsNullOrWhiteSpace(value)) + { + normalized = string.Empty; + errorMessage = string.Empty; + return true; + } + + var trimmed = value.Trim(); + if (trimmed.StartsWith("#", StringComparison.Ordinal)) + { + trimmed = trimmed[1..]; + } + + if (trimmed.Length != 6 && trimmed.Length != 8) + { + normalized = string.Empty; + errorMessage = "Colors must contain 6 or 8 hexadecimal characters."; + return false; + } + + foreach (var ch in trimmed) + { + if (!Uri.IsHexDigit(ch)) + { + normalized = string.Empty; + errorMessage = "Colors may only contain hexadecimal characters."; + return false; + } + } + + normalized = "#" + trimmed.ToUpperInvariant(); + errorMessage = string.Empty; + return true; + } + [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] private static partial Regex GamePathRegex();