using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto; using LightlessSync.API.SignalR; using LightlessSyncServer.Services; using LightlessSyncServer.Utils; using LightlessSyncShared; using LightlessSyncShared.Data; using LightlessSyncShared.Metrics; using LightlessSyncShared.Models; using LightlessSyncShared.Services; using LightlessSyncShared.Utils.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis.Extensions.Core.Abstractions; using System.Collections.Concurrent; namespace LightlessSyncServer.Hubs; [Authorize(Policy = "Authenticated")] public partial class LightlessHub : Hub, ILightlessHub { private static readonly ConcurrentDictionary _userConnections = new(StringComparer.Ordinal); private readonly LightlessMetrics _lightlessMetrics; private readonly SystemInfoService _systemInfoService; private readonly IHttpContextAccessor _contextAccessor; private readonly LightlessHubLogger _logger; private readonly string _shardName; private readonly int _maxExistingGroupsByUser; private readonly int _maxJoinedGroupsByUser; private readonly int _maxGroupUserCount; private readonly IRedisDatabase _redis; private readonly OnlineSyncedPairCacheService _onlineSyncedPairCacheService; private readonly LightlessCensus _lightlessCensus; private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService; private readonly Uri _fileServerAddress; private readonly Version _expectedClientVersion; private readonly Lazy _dbContextLazy; private LightlessDbContext DbContext => _dbContextLazy.Value; private readonly int _maxCharaDataByUser; private readonly int _maxCharaDataByUserVanity; public LightlessHub(LightlessMetrics lightlessMetrics, IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, GPoseLobbyDistributionService gPoseLobbyDistributionService) { _lightlessMetrics = lightlessMetrics; _systemInfoService = systemInfoService; _shardName = configuration.GetValue(nameof(ServerConfiguration.ShardName)); _maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3); _maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6); _maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100); _fileServerAddress = configuration.GetValue(nameof(ServerConfiguration.CdnFullUrl)); _expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0)); _maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10); _maxCharaDataByUserVanity = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUserVanity), 50); _contextAccessor = contextAccessor; _redis = redisDb; _onlineSyncedPairCacheService = onlineSyncedPairCacheService; _lightlessCensus = lightlessCensus; _gPoseLobbyDistributionService = gPoseLobbyDistributionService; _logger = new LightlessHubLogger(this, logger); _dbContextLazy = new Lazy(() => lightlessDbContextFactory.CreateDbContext()); } protected override void Dispose(bool disposing) { if (disposing) { if (_dbContextLazy.IsValueCreated) DbContext.Dispose(); } base.Dispose(disposing); } [Authorize(Policy = "Identified")] public async Task GetConnectionDto() { _logger.LogCallInfo(); _lightlessMetrics.IncCounter(MetricsAPI.CounterInitializedConnections); await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false); var dbUser = await DbContext.Users.SingleAsync(f => f.UID == UserUID).ConfigureAwait(false); dbUser.LastLoggedIn = DateTime.UtcNow; await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Lightless Sync \"" + _shardName + "\", Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false); var defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID).ConfigureAwait(false); if (defaultPermissions == null) { defaultPermissions = new UserDefaultPreferredPermission() { UserUID = UserUID, }; DbContext.UserDefaultPreferredPermissions.Add(defaultPermissions); } await DbContext.SaveChangesAsync().ConfigureAwait(false); return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias)) { CurrentClientVersion = _expectedClientVersion, ServerVersion = ILightlessHub.ApiVersion, IsAdmin = dbUser.IsAdmin, IsModerator = dbUser.IsModerator, ServerInfo = new ServerInfo() { MaxGroupsCreatedByUser = _maxExistingGroupsByUser, ShardName = _shardName, MaxGroupsJoinedByUser = _maxJoinedGroupsByUser, MaxGroupUserCount = _maxGroupUserCount, FileServerAddress = _fileServerAddress, MaxCharaData = _maxCharaDataByUser, MaxCharaDataVanity = _maxCharaDataByUserVanity, }, DefaultPreferredPermissions = new DefaultPermissionsDto() { DisableGroupAnimations = defaultPermissions.DisableGroupAnimations, DisableGroupSounds = defaultPermissions.DisableGroupSounds, DisableGroupVFX = defaultPermissions.DisableGroupVFX, DisableIndividualAnimations = defaultPermissions.DisableIndividualAnimations, DisableIndividualSounds = defaultPermissions.DisableIndividualSounds, DisableIndividualVFX = defaultPermissions.DisableIndividualVFX, IndividualIsSticky = defaultPermissions.IndividualIsSticky, }, }; } [Authorize(Policy = "Authenticated")] public async Task CheckClientHealth() { await UpdateUserOnRedis().ConfigureAwait(false); return false; } [Authorize(Policy = "Authenticated")] public override async Task OnConnectedAsync() { if (_userConnections.TryGetValue(UserUID, out var oldId)) { _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "UpdatingId", oldId, Context.ConnectionId)); _userConnections[UserUID] = Context.ConnectionId; } else { _lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); try { _logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); await _onlineSyncedPairCacheService.InitPlayer(UserUID).ConfigureAwait(false); await UpdateUserOnRedis().ConfigureAwait(false); _userConnections[UserUID] = Context.ConnectionId; } catch { _userConnections.Remove(UserUID, out _); } } await base.OnConnectedAsync().ConfigureAwait(false); } [Authorize(Policy = "Authenticated")] public override async Task OnDisconnectedAsync(Exception exception) { if (_userConnections.TryGetValue(UserUID, out var connectionId) && string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal)) { _lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); try { await GposeLobbyLeave().ConfigureAwait(false); await _onlineSyncedPairCacheService.DisposePlayer(UserUID).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); if (exception != null) _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace)); await RemoveUserFromRedis().ConfigureAwait(false); _lightlessCensus.ClearStatistics(UserUID); await SendOfflineToAllPairedUsers().ConfigureAwait(false); DbContext.RemoveRange(DbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == UserUID)); await DbContext.SaveChangesAsync().ConfigureAwait(false); } catch { } finally { _userConnections.Remove(UserUID, out _); } } else { _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "ObsoleteId", UserUID, Context.ConnectionId)); } await base.OnDisconnectedAsync(exception).ConfigureAwait(false); } }