Compare commits

..

2 Commits

Author SHA1 Message Date
cake
dd4cb73b9b Change lightfinder permissions for groups 2025-11-18 00:27:25 +01:00
cake
ab9cdeb682 Upped hashing 2025-11-17 19:37:23 +01:00
34 changed files with 177 additions and 1736 deletions

View File

@@ -44,14 +44,12 @@ public class GeoIPService : IHostedService
if (_dbReader!.TryCity(ip, out var response)) if (_dbReader!.TryCity(ip, out var response))
{ {
string? continent = response?.Continent.Code; string? continent = response?.Continent.Code;
string? countryIso = response?.Country.IsoCode;
if (!string.IsNullOrEmpty(continent) && if (!string.IsNullOrEmpty(continent) &&
string.Equals(continent, "NA", StringComparison.Ordinal) string.Equals(continent, "NA", StringComparison.Ordinal)
&& response?.Location.Longitude != null) && response?.Location.Longitude != null)
{ {
if (response.Location.Longitude < -102 && if (response.Location.Longitude < -102)
!string.Equals(countryIso, "CA", StringComparison.OrdinalIgnoreCase))
{ {
continent = "NA-W"; continent = "NA-W";
} }
@@ -59,8 +57,6 @@ public class GeoIPService : IHostedService
{ {
continent = "NA-E"; continent = "NA-E";
} }
_logger.LogDebug("Connecting {countryIso} to {continent}", countryIso, continent);
} }
return continent ?? "*"; return continent ?? "*";

View File

@@ -1,9 +1,13 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSyncServer.Models; using LightlessSyncServer.Models;
using LightlessSyncServer.Services;
using LightlessSyncServer.Utils; using LightlessSyncServer.Utils;
using LightlessSyncShared.Models; using LightlessSyncShared.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -14,7 +18,7 @@ namespace LightlessSyncServer.Hubs;
public partial class LightlessHub public partial class LightlessHub
{ {
private const int MaxChatMessageLength = 200; private const int MaxChatMessageLength = 400;
private const int ChatRateLimitMessages = 7; private const int ChatRateLimitMessages = 7;
private static readonly TimeSpan ChatRateLimitWindow = TimeSpan.FromMinutes(1); private static readonly TimeSpan ChatRateLimitWindow = TimeSpan.FromMinutes(1);
private static readonly ConcurrentDictionary<string, ChatRateLimitState> ChatRateLimiters = new(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, ChatRateLimitState> ChatRateLimiters = new(StringComparer.Ordinal);
@@ -63,7 +67,7 @@ public partial class LightlessHub
descriptor, descriptor,
displayName, displayName,
g.GID, g.GID,
string.Equals(g.OwnerUID, userUid, StringComparison.Ordinal)); g.OwnerUID == userUid);
}) })
.OrderBy(info => info.DisplayName, StringComparer.OrdinalIgnoreCase) .OrderBy(info => info.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
@@ -402,7 +406,7 @@ public partial class LightlessHub
return; return;
} }
/* if (!string.IsNullOrEmpty(messageEntry.SenderUserUid)) if (!string.IsNullOrEmpty(messageEntry.SenderUserUid))
{ {
var targetAlreadyPending = await DbContext.ReportedChatMessages var targetAlreadyPending = await DbContext.ReportedChatMessages
.AsNoTracking() .AsNoTracking()
@@ -414,7 +418,7 @@ public partial class LightlessHub
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "This user already has a report pending review.").ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "This user already has a report pending review.").ConfigureAwait(false);
return; return;
} }
} */ }
var snapshotEntries = _chatChannelService.GetRecentMessages(messageEntry.Channel, 25); var snapshotEntries = _chatChannelService.GetRecentMessages(messageEntry.Channel, 25);
var snapshotItems = snapshotEntries var snapshotItems = snapshotEntries

View File

@@ -264,7 +264,7 @@ public partial class LightlessHub
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(),
newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal), 1)) newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal)))
.ConfigureAwait(false); .ConfigureAwait(false);
_logger.LogCallInfo(LightlessHubLogger.Args(gid)); _logger.LogCallInfo(LightlessHubLogger.Args(gid));
@@ -454,14 +454,9 @@ public partial class LightlessHub
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false);
var totalUserCount = await DbContext.GroupPairs
.AsNoTracking()
.CountAsync(u => u.GroupGID == group.GID, RequestAbortedToken)
.ConfigureAwait(false);
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(),
group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(),
groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal), totalUserCount)).ConfigureAwait(false); groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false);
var self = DbContext.Users.Single(u => u.UID == UserUID); var self = DbContext.Users.Single(u => u.UID == UserUID);
@@ -683,38 +678,30 @@ public partial class LightlessHub
{ {
_logger.LogCallInfo(LightlessHubLogger.Args(dto, days, execute)); _logger.LogCallInfo(LightlessHubLogger.Args(dto, days, execute));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID) var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
.ConfigureAwait(false);
if (!hasRights) return -1; if (!hasRights) return -1;
if (!execute) var allGroupUsers = await DbContext.GroupPairs.Include(p => p.GroupUser).Include(p => p.Group)
{
var count = await _pruneService.CountPrunableUsersAsync(dto.Group.GID, days, RequestAbortedToken).ConfigureAwait(false);
return count;
}
var allGroupUsers = await DbContext.GroupPairs
.Include(p => p.GroupUser)
.Include(p => p.Group)
.Where(g => g.GroupGID == dto.Group.GID) .Where(g => g.GroupGID == dto.Group.GID)
.ToListAsync(RequestAbortedToken).ConfigureAwait(false); .ToListAsync().ConfigureAwait(false);
var usersToPrune = allGroupUsers.Where(p => !p.IsPinned && !p.IsModerator
&& !string.Equals(p.GroupUserUID, UserUID, StringComparison.Ordinal)
&& !string.Equals(p.Group.OwnerUID, p.GroupUserUID, StringComparison.Ordinal)
&& p.GroupUser.LastLoggedIn.AddDays(days) < DateTime.UtcNow);
var prunedPairs = await _pruneService.ExecutePruneAsync(dto.Group.GID, days, RequestAbortedToken).ConfigureAwait(false); if (!execute) return usersToPrune.Count();
var remainingUserIds = allGroupUsers DbContext.GroupPairs.RemoveRange(usersToPrune);
.Where(p => !prunedPairs.Any(x => string.Equals(x.GroupUserUID, p.GroupUserUID, StringComparison.Ordinal)))
.Select(p => p.GroupUserUID)
.Distinct(StringComparer.Ordinal)
.ToList();
foreach (var pair in prunedPairs) foreach (var pair in usersToPrune)
{ {
await Clients.Users(remainingUserIds) await Clients.Users(allGroupUsers.Where(p => !usersToPrune.Contains(p)).Select(g => g.GroupUserUID))
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())) .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
.ConfigureAwait(false);
} }
return prunedPairs.Count; await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
return usersToPrune.Count();
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -852,9 +839,8 @@ public partial class LightlessHub
{ {
GroupGID = dto.Group.GID, GroupGID = dto.Group.GID,
Group = group, Group = group,
ProfileDisabled = dto.IsDisabled ?? false, ProfileDisabled = false,
IsNSFW = dto.IsNsfw ?? false, IsNSFW = dto.IsNsfw ?? false,
}; };
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage); groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
@@ -924,44 +910,6 @@ public partial class LightlessHub
await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false);
} }
[Authorize(Policy = "Identified")]
public async Task<GroupPruneSettingsDto> GroupGetPruneSettings(GroupDto dto)
{
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID)
.ConfigureAwait(false);
if (!hasRights || group == null)
return null;
return new GroupPruneSettingsDto(
Group: group.ToGroupData(),
AutoPruneEnabled: group.AutoPruneEnabled,
AutoPruneDays: group.AutoPruneDays
);
}
[Authorize(Policy = "Identified")]
public async Task GroupSetPruneSettings(GroupPruneSettingsDto dto)
{
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID)
.ConfigureAwait(false);
if (!hasRights || group == null)
return;
// if days == 0, auto prune is OFF
var days = dto.AutoPruneDays;
var enabled = dto.AutoPruneEnabled && days > 0;
group.AutoPruneEnabled = enabled;
group.AutoPruneDays = enabled ? days : 0;
await DbContext.SaveChangesAsync(RequestAbortedToken)
.ConfigureAwait(false);
}
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
public async Task<List<GroupFullInfoDto>> GroupsGetAll() public async Task<List<GroupFullInfoDto>> GroupsGetAll()
{ {
@@ -984,8 +932,6 @@ public partial class LightlessHub
.Where(x => x.GroupGID == gp.GroupGID && (x.IsPinned || x.IsModerator)) .Where(x => x.GroupGID == gp.GroupGID && (x.IsPinned || x.IsModerator))
.Select(x => new { x.GroupUserUID, EnumValue = x.ToEnum() }) .Select(x => new { x.GroupUserUID, EnumValue = x.ToEnum() })
.ToList(), .ToList(),
UserCount = DbContext.GroupPairs
.Count(x => x.GroupGID == gp.GroupGID),
}) })
.AsNoTracking() .AsNoTracking()
.ToListAsync() .ToListAsync()
@@ -998,6 +944,7 @@ public partial class LightlessHub
var groupInfoDict = r.GroupInfos var groupInfoDict = r.GroupInfos
.ToDictionary(x => x.GroupUserUID, x => x.EnumValue, StringComparer.Ordinal); .ToDictionary(x => x.GroupUserUID, x => x.EnumValue, StringComparer.Ordinal);
_logger.LogCallInfo(LightlessHubLogger.Args(r)); _logger.LogCallInfo(LightlessHubLogger.Args(r));
return new GroupFullInfoDto( return new GroupFullInfoDto(
@@ -1006,8 +953,7 @@ public partial class LightlessHub
r.GroupPair.Group.ToEnum(), r.GroupPair.Group.ToEnum(),
r.PreferredPermission.ToEnum(), r.PreferredPermission.ToEnum(),
r.GroupPair.ToEnum(), r.GroupPair.ToEnum(),
groupInfoDict, groupInfoDict
r.UserCount
); );
}),]; }),];
return List; return List;
@@ -1049,11 +995,11 @@ public partial class LightlessHub
return false; return false;
} }
var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false); var (isOwner, _) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
if (!isOwner) if (!isOwner)
{ {
_logger.LogCallWarning(LightlessHubLogger.Args("Unauthorized syncshell broadcast change", "User", UserUID, "GID", dto.GID)); _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."); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner or moderator of the syncshell to broadcast it.");
return false; return false;
} }

View File

@@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;

View File

@@ -1,6 +1,7 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
using LightlessSync.API.Dto.Chat;
using LightlessSync.API.SignalR; using LightlessSync.API.SignalR;
using LightlessSyncServer.Services; using LightlessSyncServer.Services;
using LightlessSyncServer.Configuration; using LightlessSyncServer.Configuration;
@@ -16,7 +17,8 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using LightlessSyncServer.Services.Interfaces; using System.Collections.Generic;
using System.Linq;
namespace LightlessSyncServer.Hubs; namespace LightlessSyncServer.Hubs;
@@ -27,7 +29,6 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
private readonly LightlessMetrics _lightlessMetrics; private readonly LightlessMetrics _lightlessMetrics;
private readonly SystemInfoService _systemInfoService; private readonly SystemInfoService _systemInfoService;
private readonly PairService _pairService; private readonly PairService _pairService;
private readonly IPruneService _pruneService;
private readonly IHttpContextAccessor _contextAccessor; private readonly IHttpContextAccessor _contextAccessor;
private readonly LightlessHubLogger _logger; private readonly LightlessHubLogger _logger;
private readonly string _shardName; private readonly string _shardName;
@@ -54,7 +55,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor, IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
GPoseLobbyDistributionService gPoseLobbyDistributionService, IBroadcastConfiguration broadcastConfiguration, PairService pairService, GPoseLobbyDistributionService gPoseLobbyDistributionService, IBroadcastConfiguration broadcastConfiguration, PairService pairService,
ChatChannelService chatChannelService, IPruneService pruneService) ChatChannelService chatChannelService)
{ {
_lightlessMetrics = lightlessMetrics; _lightlessMetrics = lightlessMetrics;
_systemInfoService = systemInfoService; _systemInfoService = systemInfoService;
@@ -76,7 +77,6 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
_broadcastConfiguration = broadcastConfiguration; _broadcastConfiguration = broadcastConfiguration;
_pairService = pairService; _pairService = pairService;
_chatChannelService = chatChannelService; _chatChannelService = chatChannelService;
_pruneService = pruneService;
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@@ -21,6 +21,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" /> <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="Blake3" Version="2.0.0" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8"> <PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,10 +0,0 @@
using LightlessSyncShared.Models;
namespace LightlessSyncServer.Services.Interfaces
{
public interface IPruneService
{
Task<int> CountPrunableUsersAsync(string groupGid, int days, CancellationToken ct);
Task<IReadOnlyList<GroupPair>> ExecutePruneAsync(string groupGid, int days, CancellationToken ct);
}
}

View File

@@ -1,60 +0,0 @@
using LightlessSyncServer.Services.Interfaces;
using LightlessSyncShared.Data;
using LightlessSyncShared.Models;
using Microsoft.EntityFrameworkCore;
namespace LightlessSyncServer.Services
{
public class PruneService(LightlessDbContext dbContext) : IPruneService
{
private readonly LightlessDbContext _dbContext = dbContext;
public async Task<int> CountPrunableUsersAsync(string groupGid, int days, CancellationToken ct)
{
var allGroupUsers = await _dbContext.GroupPairs
.Include(p => p.GroupUser)
.Include(p => p.Group)
.Where(g => g.GroupGID == groupGid)
.ToListAsync(ct).ConfigureAwait(false);
var inactivitySpan = GetInactivitySpan(days);
var now = DateTime.UtcNow;
var usersToPrune = allGroupUsers.Where(p =>
!p.IsPinned &&
!p.IsModerator &&
!string.Equals(p.Group.OwnerUID, p.GroupUserUID, StringComparison.Ordinal) &&
p.GroupUser.LastLoggedIn < now - inactivitySpan);
return usersToPrune.Count();
}
public async Task<IReadOnlyList<GroupPair>> ExecutePruneAsync(string groupGid, int days, CancellationToken ct)
{
var allGroupUsers = await _dbContext.GroupPairs
.Include(p => p.GroupUser)
.Include(p => p.Group)
.Where(g => g.GroupGID == groupGid)
.ToListAsync(ct).ConfigureAwait(false);
var inactivitySpan = GetInactivitySpan(days);
var now = DateTime.UtcNow;
var usersToPrune = allGroupUsers.Where(p =>
!p.IsPinned &&
!p.IsModerator &&
!string.Equals(p.Group.OwnerUID, p.GroupUserUID, StringComparison.Ordinal) &&
p.GroupUser.LastLoggedIn < now - inactivitySpan)
.ToList();
_dbContext.GroupPairs.RemoveRange(usersToPrune);
await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
return usersToPrune;
}
private static TimeSpan GetInactivitySpan(int days) => days == 0
? TimeSpan.FromMinutes(15)
: TimeSpan.FromDays(days);
}
}

View File

@@ -72,11 +72,9 @@ public sealed class SystemInfoService : BackgroundService
await _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto).ConfigureAwait(false); await _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto).ConfigureAwait(false);
using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false); using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var groupsWithAutoPrune = db.Groups.AsNoTracking().Count(g => g.AutoPruneEnabled && g.AutoPruneDays > 0);
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers); _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers);
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeLightFinderConnections, countLightFinderUsers); _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeLightFinderConnections, countLightFinderUsers);
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroupAutoPrunesEnabled, groupsWithAutoPrune);
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count()); _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count());
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Count(p => p.IsPaused)); _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Count(p => p.IsPaused));
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count()); _lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count());

View File

@@ -1,13 +1,9 @@
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.SignalR; using LightlessSync.API.SignalR;
using LightlessSyncServer.Configuration;
using LightlessSyncServer.Controllers; using LightlessSyncServer.Controllers;
using LightlessSyncServer.Configuration;
using LightlessSyncServer.Hubs; using LightlessSyncServer.Hubs;
using LightlessSyncServer.Services; using LightlessSyncServer.Services;
using LightlessSyncServer.Services.Interfaces;
using LightlessSyncServer.Worker;
using LightlessSyncShared.Data; using LightlessSyncShared.Data;
using LightlessSyncShared.Metrics; using LightlessSyncShared.Metrics;
using LightlessSyncShared.RequirementHandlers; using LightlessSyncShared.RequirementHandlers;
@@ -22,7 +18,6 @@ using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Prometheus; using Prometheus;
using StackExchange.Redis; using StackExchange.Redis;
@@ -114,12 +109,9 @@ public class Startup
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>()); services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
services.AddHostedService<ClientPairPermissionsCleanupService>(); services.AddHostedService<ClientPairPermissionsCleanupService>();
services.AddScoped<PairService>(); services.AddScoped<PairService>();
services.AddScoped<IPruneService, PruneService>();
} }
services.AddSingleton<GPoseLobbyDistributionService>(); services.AddSingleton<GPoseLobbyDistributionService>();
services.AddHostedService<AutoPruneWorker>();
services.AddHostedService(provider => provider.GetService<GPoseLobbyDistributionService>()); services.AddHostedService(provider => provider.GetService<GPoseLobbyDistributionService>());
} }
@@ -128,8 +120,6 @@ public class Startup
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>(); services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
services.AddSingleton<ConcurrencyFilter>(); services.AddSingleton<ConcurrencyFilter>();
var msgpackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithResolver(ContractlessStandardResolver.Instance);
var signalRServiceBuilder = services.AddSignalR(hubOptions => var signalRServiceBuilder = services.AddSignalR(hubOptions =>
{ {
hubOptions.MaximumReceiveMessageSize = long.MaxValue; hubOptions.MaximumReceiveMessageSize = long.MaxValue;
@@ -141,10 +131,21 @@ public class Startup
hubOptions.AddFilter<ConcurrencyFilter>(); hubOptions.AddFilter<ConcurrencyFilter>();
}).AddMessagePackProtocol(opt => }).AddMessagePackProtocol(opt =>
{ {
opt.SerializerOptions = msgpackOptions; var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
// replace enum resolver
DynamicEnumAsStringResolver.Instance,
DynamicGenericResolver.Instance,
DynamicUnionResolver.Instance,
DynamicObjectResolver.Instance,
PrimitiveObjectResolver.Instance,
// final fallback(last priority)
StandardResolver.Instance);
var dummy = new GroupPruneSettingsDto(new GroupData("TEST-GID", null), true, 14); opt.SerializerOptions = MessagePackSerializerOptions.Standard
MessagePackSerializer.Serialize(dummy, msgpackOptions); .WithCompression(MessagePackCompression.Lz4Block)
.WithResolver(resolver);
}); });
@@ -165,7 +166,7 @@ public class Startup
KeyPrefix = "", KeyPrefix = "",
Hosts = new RedisHost[] Hosts = new RedisHost[]
{ {
new(){ Host = address, Port = port }, new RedisHost(){ Host = address, Port = port },
}, },
AllowAdmin = true, AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout, ConnectTimeout = options.ConnectTimeout,
@@ -291,12 +292,11 @@ public class Startup
MetricsAPI.CounterUserPairCacheMiss, MetricsAPI.CounterUserPairCacheMiss,
MetricsAPI.CounterUserPairCacheNewEntries, MetricsAPI.CounterUserPairCacheNewEntries,
MetricsAPI.CounterUserPairCacheUpdatedEntries, MetricsAPI.CounterUserPairCacheUpdatedEntries,
}, }, new List<string>
[ {
MetricsAPI.GaugeAuthorizedConnections, MetricsAPI.GaugeAuthorizedConnections,
MetricsAPI.GaugeLightFinderConnections, MetricsAPI.GaugeLightFinderConnections,
MetricsAPI.GaugeLightFinderGroups, MetricsAPI.GaugeLightFinderGroups,
MetricsAPI.GaugeGroupAutoPrunesEnabled,
MetricsAPI.GaugeConnections, MetricsAPI.GaugeConnections,
MetricsAPI.GaugePairs, MetricsAPI.GaugePairs,
MetricsAPI.GaugePairsPaused, MetricsAPI.GaugePairsPaused,
@@ -312,7 +312,7 @@ public class Startup
MetricsAPI.GaugeGposeLobbyUsers, MetricsAPI.GaugeGposeLobbyUsers,
MetricsAPI.GaugeHubConcurrency, MetricsAPI.GaugeHubConcurrency,
MetricsAPI.GaugeHubQueuedConcurrency, MetricsAPI.GaugeHubQueuedConcurrency,
])); }));
} }
private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection lightlessConfig, bool isMainServer) private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection lightlessConfig, bool isMainServer)

View File

@@ -22,7 +22,6 @@ public static class Extensions
if (dto.Tags != null) profile.Tags = dto.Tags; if (dto.Tags != null) profile.Tags = dto.Tags;
if (dto.Description != null) profile.Description = dto.Description; if (dto.Description != null) profile.Description = dto.Description;
if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value; if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value;
if (dto.IsDisabled.HasValue) profile.ProfileDisabled = dto.IsDisabled.Value;
} }
public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto, string? base64PictureString = null, string? base64BannerString = null) public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto, string? base64PictureString = null, string? base64BannerString = null)

View File

@@ -1,68 +0,0 @@
using LightlessSync.API.Dto.Group;
using LightlessSyncServer.Hubs;
using LightlessSyncServer.Services.Interfaces;
using LightlessSyncServer.Utils;
using LightlessSyncShared.Data;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace LightlessSyncServer.Worker
{
public class AutoPruneWorker(IServiceProvider services, ILogger<AutoPruneWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var scope = services.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var db = scope.ServiceProvider.GetRequiredService<LightlessDbContext>();
var pruneService = scope.ServiceProvider.GetRequiredService<IPruneService>();
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<LightlessHub>>();
var groups = await db.Groups
.Where(g => g.AutoPruneEnabled && g.AutoPruneDays > 0)
.ToListAsync(stoppingToken).ConfigureAwait(false);
foreach (var group in groups)
{
var allGroupUsers = await db.GroupPairs
.Include(p => p.GroupUser)
.Include(p => p.Group)
.Where(p => p.GroupGID == group.GID)
.ToListAsync(stoppingToken).ConfigureAwait(false);
var prunedPairs = await pruneService.ExecutePruneAsync(group.GID, group.AutoPruneDays, stoppingToken).ConfigureAwait(false);
if (prunedPairs.Count == 0)
continue;
var remainingUserIds = allGroupUsers
.Where(p => !prunedPairs.Any(x => string.Equals(x.GroupUserUID, p.GroupUserUID, StringComparison.Ordinal)))
.Select(p => p.GroupUserUID)
.Distinct(StringComparer.Ordinal)
.ToList();
foreach (var pair in prunedPairs)
{
await hubContext.Clients.Users(remainingUserIds).SendAsync("Client_GroupPairLeft", new GroupPairDto(group.ToGroupData(), pair.GroupUser.ToUserData()), stoppingToken).ConfigureAwait(false);
}
logger.LogInformation("Auto-pruned {Count} users from group {GroupId}", prunedPairs.Count, group.GID);
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error in auto-prune worker");
}
//Run task each hour to check for pruning
await Task.Delay(TimeSpan.FromHours(1), stoppingToken).ConfigureAwait(false);
}
}
}
}

View File

@@ -7,7 +7,6 @@
"Default": "Information", "Default": "Information",
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information", "Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"LightlessSyncServer.Authentication": "Warning", "LightlessSyncServer.Authentication": "Warning",
"System.IO.IOException": "Warning" "System.IO.IOException": "Warning"
}, },

View File

@@ -162,7 +162,7 @@ internal class DiscordBot : IHostedService
try try
{ {
await Task.Delay(TimeSpan.FromMinutes(1), token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromMinutes(10), token).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@@ -94,7 +94,7 @@ public partial class LightlessWizardModule
bool canAddVanityId = !db.Users.Any(u => u.UID == modal.DesiredVanityUID || u.Alias == modal.DesiredVanityUID); bool canAddVanityId = !db.Users.Any(u => u.UID == modal.DesiredVanityUID || u.Alias == modal.DesiredVanityUID);
var forbiddenWords = new[] { "null", "nil" }; var forbiddenWords = new[] { "null", "nil" };
Regex rgx = new(@"^[_\-a-zA-Z0-9\?]{3,15}$", RegexOptions.ECMAScript); Regex rgx = new(@"^[_\-a-zA-Z0-9]{3,15}$", RegexOptions.ECMAScript);
if (!rgx.Match(desiredVanityUid).Success) if (!rgx.Match(desiredVanityUid).Success)
{ {
eb.WithColor(Color.Red); eb.WithColor(Color.Red);

View File

@@ -11,7 +11,6 @@ public class MetricsAPI
public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted"; public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted";
public const string GaugeLightFinderConnections = "lightless_lightfinder_connections"; public const string GaugeLightFinderConnections = "lightless_lightfinder_connections";
public const string GaugeLightFinderGroups = "lightless_lightfinder_groups"; public const string GaugeLightFinderGroups = "lightless_lightfinder_groups";
public const string GaugeGroupAutoPrunesEnabled = "lightless_group_autoprunes_enabled";
public const string GaugePairs = "lightless_pairs"; public const string GaugePairs = "lightless_pairs";
public const string GaugePairsPaused = "lightless_pairs_paused"; public const string GaugePairsPaused = "lightless_pairs_paused";
public const string GaugeFilesTotal = "lightless_files"; public const string GaugeFilesTotal = "lightless_files";

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LightlessSyncServer.Migrations
{
/// <inheritdoc />
public partial class AddAutoPruneInGroup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "auto_prune_days",
table: "groups",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "auto_prune_enabled",
table: "groups",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "auto_prune_days",
table: "groups");
migrationBuilder.DropColumn(
name: "auto_prune_enabled",
table: "groups");
}
}
}

View File

@@ -430,14 +430,6 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("character varying(50)") .HasColumnType("character varying(50)")
.HasColumnName("alias"); .HasColumnName("alias");
b.Property<int>("AutoPruneDays")
.HasColumnType("integer")
.HasColumnName("auto_prune_days");
b.Property<bool>("AutoPruneEnabled")
.HasColumnType("boolean")
.HasColumnName("auto_prune_enabled");
b.Property<DateTime>("CreatedDate") b.Property<DateTime>("CreatedDate")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")

View File

@@ -12,8 +12,6 @@ public class Group
[MaxLength(50)] [MaxLength(50)]
public string Alias { get; set; } public string Alias { get; set; }
public GroupProfile? Profile { get; set; } public GroupProfile? Profile { get; set; }
public bool AutoPruneEnabled { get; set; } = false;
public int AutoPruneDays { get; set; } = 0;
public bool InvitesEnabled { get; set; } public bool InvitesEnabled { get; set; }
public string HashedPassword { get; set; } public string HashedPassword { get; set; }
public bool PreferDisableSounds { get; set; } public bool PreferDisableSounds { get; set; }

View File

@@ -1,4 +1,5 @@
using K4os.Compression.LZ4.Legacy; using Blake3;
using K4os.Compression.LZ4.Legacy;
using LightlessSync.API.Dto.Files; using LightlessSync.API.Dto.Files;
using LightlessSync.API.Routes; using LightlessSync.API.Routes;
using LightlessSync.API.SignalR; using LightlessSync.API.SignalR;
@@ -208,11 +209,14 @@ public class ServerFilesController : ControllerBase
[RequestSizeLimit(200 * 1024 * 1024)] [RequestSizeLimit(200 * 1024 * 1024)]
public async Task<IActionResult> UploadFile(string hash, CancellationToken requestAborted) public async Task<IActionResult> UploadFile(string hash, CancellationToken requestAborted)
{ {
using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); await using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
_logger.LogInformation("{user}|{file}: Uploading", LightlessUser, hash); _logger.LogInformation("{user}|{file}: Uploading", LightlessUser, hash);
if (hash.Length == 40)
{
hash = hash.ToUpperInvariant(); hash = hash.ToUpperInvariant();
}
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
if (existingFile != null) return Ok(); if (existingFile != null) return Ok();
@@ -263,10 +267,14 @@ public class ServerFilesController : ControllerBase
[RequestSizeLimit(200 * 1024 * 1024)] [RequestSizeLimit(200 * 1024 * 1024)]
public async Task<IActionResult> UploadFileMunged(string hash, CancellationToken requestAborted) public async Task<IActionResult> UploadFileMunged(string hash, CancellationToken requestAborted)
{ {
using var dbContext = await _lightlessDbContext.CreateDbContextAsync(); await using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
_logger.LogInformation("{user}|{file}: Uploading munged", LightlessUser, hash); _logger.LogInformation("{user}|{file}: Uploading munged", LightlessUser, hash);
if (hash.Length == 40)
{
hash = hash.ToUpperInvariant(); hash = hash.ToUpperInvariant();
}
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash); var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
if (existingFile != null) return Ok(); if (existingFile != null) return Ok();
@@ -319,20 +327,26 @@ public class ServerFilesController : ControllerBase
private async Task StoreData(string hash, LightlessDbContext dbContext, MemoryStream compressedFileStream) private async Task StoreData(string hash, LightlessDbContext dbContext, MemoryStream compressedFileStream)
{ {
var decompressedData = LZ4Wrapper.Unwrap(compressedFileStream.ToArray()); var decompressedData = LZ4Wrapper.Unwrap(compressedFileStream.ToArray());
// reset streams
compressedFileStream.Seek(0, SeekOrigin.Begin); compressedFileStream.Seek(0, SeekOrigin.Begin);
// compute hash to verify bool valid;
var hashString = BitConverter.ToString(SHA1.HashData(decompressedData))
.Replace("-", "", StringComparison.Ordinal).ToUpperInvariant();
if (!string.Equals(hashString, hash, StringComparison.Ordinal))
throw new InvalidOperationException($"{LightlessUser}|{hash}: Hash does not match file, computed: {hashString}, expected: {hash}");
// save file if (hash.Length == 40)
var path = FilePathUtil.GetFilePath(_basePath, hash); {
using var fileStream = new FileStream(path, FileMode.Create); var sha1Hex = Convert.ToHexString(SHA1.HashData(decompressedData));
await compressedFileStream.CopyToAsync(fileStream).ConfigureAwait(false); valid = string.Equals(sha1Hex, hash, StringComparison.OrdinalIgnoreCase);
_logger.LogDebug("{user}|{file}: Uploaded file saved to {path}", LightlessUser, hash, path); }
else
{
var blakeHash = Hasher.Hash(decompressedData);
var blakeHex = Convert.ToHexString(blakeHash.AsSpan());
valid = string.Equals(blakeHex, hash, StringComparison.OrdinalIgnoreCase);
}
if (!valid)
throw new InvalidOperationException(
$"{LightlessUser}|{hash}: Hash does not match file, computed mismatch."
);
// update on db // update on db
await dbContext.Files.AddAsync(new FileCache() await dbContext.Files.AddAsync(new FileCache()

View File

@@ -18,6 +18,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blake3" Version="2.0.0" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8"> <PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>