Files
LightlessServer/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs
2025-12-17 03:46:23 +09:00

242 lines
11 KiB
C#

using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto;
using LightlessSync.API.SignalR;
using LightlessSyncServer.Services;
using LightlessSyncServer.Configuration;
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;
using LightlessSyncServer.Services.Interfaces;
namespace LightlessSyncServer.Hubs;
[Authorize(Policy = "Authenticated")]
public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
{
private static readonly ConcurrentDictionary<string, string> _userConnections = new(StringComparer.Ordinal);
private readonly LightlessMetrics _lightlessMetrics;
private readonly SystemInfoService _systemInfoService;
private readonly PairService _pairService;
private readonly IPruneService _pruneService;
private readonly IHttpContextAccessor _contextAccessor;
private readonly LightlessHubLogger _logger;
private readonly string _shardName;
private readonly int _maxExistingGroupsByUser;
private readonly IBroadcastConfiguration _broadcastConfiguration;
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<LightlessDbContext> _dbContextLazy;
private LightlessDbContext DbContext => _dbContextLazy.Value;
private readonly int _maxCharaDataByUser;
private readonly int _maxCharaDataByUserVanity;
private readonly ChatChannelService _chatChannelService;
private CancellationToken RequestAbortedToken => _contextAccessor.HttpContext?.RequestAborted ?? Context?.ConnectionAborted ?? CancellationToken.None;
public LightlessHub(LightlessMetrics lightlessMetrics,
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
GPoseLobbyDistributionService gPoseLobbyDistributionService, IBroadcastConfiguration broadcastConfiguration, PairService pairService,
ChatChannelService chatChannelService, IPruneService pruneService)
{
_lightlessMetrics = lightlessMetrics;
_systemInfoService = systemInfoService;
_shardName = configuration.GetValue<string>(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<Uri>(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<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
_broadcastConfiguration = broadcastConfiguration;
_pairService = pairService;
_chatChannelService = chatChannelService;
_pruneService = pruneService;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_dbContextLazy.IsValueCreated) DbContext.Dispose();
}
base.Dispose(disposing);
}
[Authorize(Policy = "Identified")]
public async Task<ConnectionDto> 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,
HasVanity = dbUser.HasVanity,
TextColorHex = dbUser.TextColorHex,
TextGlowColorHex = dbUser.TextGlowColorHex,
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<bool> 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
{
var ResultLabels = new List<string>
{
Continent,
Country,
};
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
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))
{
var ResultLabels = new List<string>
{
Continent,
Country,
};
_lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
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 ClearOwnedBroadcastLock().ConfigureAwait(false);
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
{
TryInvokeChatService(() => _chatChannelService.RemovePresence(UserUID), "removing chat presence on disconnect");
_userConnections.Remove(UserUID, out _);
}
}
else
{
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), "ObsoleteId", UserUID, Context.ConnectionId));
}
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
}