Compare commits

..

26 Commits

Author SHA1 Message Date
cake
6294b1d496 Reduced report chat messages to 1 min of polling 2025-12-16 07:11:35 +01:00
cake
143037165a Reduced amount of chats, commented out the pending report. 2025-12-16 07:06:17 +01:00
c731b265ff Merge pull request 'Auto pruning of Syncshell, Added group count in DTO of group. Changed database to add prune days. Metrics added for auto prune enabled syncshells.' (#36) from auto-prune-syncshell into master
Reviewed-on: #36
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-09 05:37:42 +01:00
cake
5feb91ed42 updated api 2025-12-09 05:32:32 +01:00
cake
918c5e7d5d Added disabled in the group profile dto. 2025-12-07 21:53:01 +01:00
cake
7748fa6eac Back to an hourly action 2025-12-07 02:21:48 +01:00
cake
16c55d634d Set minutes to 1 for testing. 2025-12-07 02:18:34 +01:00
cake
7c66d4c9cf Update lightless api 2025-12-06 23:39:31 +01:00
cake
e21f9c36a4 Revert DTO, Changed messagepackprotocol again. 2025-12-06 23:39:24 +01:00
cake
c9d3cb0d50 Updated submodule 2025-12-06 22:58:36 +01:00
cake
2dc7681904 Changed resolver a bit for messagepack 2025-12-06 21:47:57 +01:00
cake
fb6ff4fb0e Update 2025-12-06 20:16:09 +01:00
cake
903429e148 Merge branch 'auto-prune-syncshell' of https://git.lightless-sync.org/Lightless-Sync/LightlessServer into auto-prune-syncshell 2025-12-06 18:29:59 +01:00
cake
d7e5117e6b Update 2025-12-06 18:02:45 +01:00
cake
727f27c2d1 Fixed some problems. 2025-12-06 18:02:39 +01:00
3218e800d6 Merge branch 'master' into auto-prune-syncshell 2025-12-06 03:16:40 +01:00
cake
7e7a5808f4 Update submodule. 2025-12-05 22:01:58 +01:00
cake
091bfbbc29 Auto-pruning of syncshell, added metrics for pruning, return of count of users in fullgroupdto. 2025-12-05 22:01:48 +01:00
00bcbbf8f4 Western Canadians will now filter to NA-East where servers are located in Canada.
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #35
2025-11-20 19:33:53 +01:00
63211b2e8b Merge pull request 'Fix Fk' (#34) from fkfix into master
Reviewed-on: #34
2025-11-17 18:34:18 +01:00
defnotken
a1280d58bf Fix Fk 2025-11-17 09:34:33 -06:00
34f0223a85 revert revert regex 2025-11-13 15:50:19 +01:00
69f06f5868 Merge pull request 'revert regex' (#33) from revert-regex into master
Reviewed-on: #33
2025-11-13 15:22:20 +01:00
066f56e5a2 Merge branch 'master' into revert-regex 2025-11-13 15:22:05 +01:00
defnotken
287f72b6ad revert regex 2025-11-13 08:21:37 -06:00
ef13566b7a Merge pull request 'Fix chat stuff' (#32) from chat into master
Reviewed-on: #32
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-11-12 01:53:11 +01:00
32 changed files with 3092 additions and 151 deletions

View File

@@ -44,12 +44,14 @@ 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";
} }
@@ -57,6 +59,8 @@ 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,13 +1,9 @@
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;
@@ -18,7 +14,7 @@ namespace LightlessSyncServer.Hubs;
public partial class LightlessHub public partial class LightlessHub
{ {
private const int MaxChatMessageLength = 400; private const int MaxChatMessageLength = 200;
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);
@@ -67,7 +63,7 @@ public partial class LightlessHub
descriptor, descriptor,
displayName, displayName,
g.GID, g.GID,
g.OwnerUID == userUid); string.Equals(g.OwnerUID, userUid, StringComparison.Ordinal));
}) })
.OrderBy(info => info.DisplayName, StringComparer.OrdinalIgnoreCase) .OrderBy(info => info.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
@@ -406,7 +402,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()
@@ -418,7 +414,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))) newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal), 1))
.ConfigureAwait(false); .ConfigureAwait(false);
_logger.LogCallInfo(LightlessHubLogger.Args(gid)); _logger.LogCallInfo(LightlessHubLogger.Args(gid));
@@ -454,9 +454,14 @@ 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))).ConfigureAwait(false); groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal), totalUserCount)).ConfigureAwait(false);
var self = DbContext.Users.Single(u => u.UID == UserUID); var self = DbContext.Users.Single(u => u.UID == UserUID);
@@ -678,30 +683,38 @@ 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).ConfigureAwait(false); var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID)
.ConfigureAwait(false);
if (!hasRights) return -1; if (!hasRights) return -1;
var allGroupUsers = await DbContext.GroupPairs.Include(p => p.GroupUser).Include(p => p.Group) if (!execute)
.Where(g => g.GroupGID == dto.Group.GID)
.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);
if (!execute) return usersToPrune.Count();
DbContext.GroupPairs.RemoveRange(usersToPrune);
foreach (var pair in usersToPrune)
{ {
await Clients.Users(allGroupUsers.Where(p => !usersToPrune.Contains(p)).Select(g => g.GroupUserUID)) var count = await _pruneService.CountPrunableUsersAsync(dto.Group.GID, days, RequestAbortedToken).ConfigureAwait(false);
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); return count;
} }
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); var allGroupUsers = await DbContext.GroupPairs
.Include(p => p.GroupUser)
.Include(p => p.Group)
.Where(g => g.GroupGID == dto.Group.GID)
.ToListAsync(RequestAbortedToken).ConfigureAwait(false);
return usersToPrune.Count(); var prunedPairs = await _pruneService.ExecutePruneAsync(dto.Group.GID, days, RequestAbortedToken).ConfigureAwait(false);
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 Clients.Users(remainingUserIds)
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData()))
.ConfigureAwait(false);
}
return prunedPairs.Count;
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -839,8 +852,9 @@ public partial class LightlessHub
{ {
GroupGID = dto.Group.GID, GroupGID = dto.Group.GID,
Group = group, Group = group,
ProfileDisabled = false, ProfileDisabled = dto.IsDisabled ?? false,
IsNSFW = dto.IsNsfw ?? false, IsNSFW = dto.IsNsfw ?? false,
}; };
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage); groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
@@ -910,6 +924,44 @@ 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()
{ {
@@ -932,6 +984,8 @@ 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()
@@ -944,7 +998,6 @@ 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(
@@ -953,7 +1006,8 @@ 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;

View File

@@ -1,7 +1,6 @@
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;
@@ -17,8 +16,7 @@ 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 System.Collections.Generic; using LightlessSyncServer.Services.Interfaces;
using System.Linq;
namespace LightlessSyncServer.Hubs; namespace LightlessSyncServer.Hubs;
@@ -29,6 +27,7 @@ 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;
@@ -55,7 +54,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) ChatChannelService chatChannelService, IPruneService pruneService)
{ {
_lightlessMetrics = lightlessMetrics; _lightlessMetrics = lightlessMetrics;
_systemInfoService = systemInfoService; _systemInfoService = systemInfoService;
@@ -77,6 +76,7 @@ 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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,60 @@
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,9 +72,11 @@ 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,9 +1,13 @@
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.SignalR; using LightlessSync.API.SignalR;
using LightlessSyncServer.Controllers;
using LightlessSyncServer.Configuration; using LightlessSyncServer.Configuration;
using LightlessSyncServer.Controllers;
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;
@@ -18,6 +22,7 @@ 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;
@@ -109,9 +114,12 @@ 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>());
} }
@@ -120,6 +128,8 @@ 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;
@@ -131,21 +141,10 @@ public class Startup
hubOptions.AddFilter<ConcurrencyFilter>(); hubOptions.AddFilter<ConcurrencyFilter>();
}).AddMessagePackProtocol(opt => }).AddMessagePackProtocol(opt =>
{ {
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, opt.SerializerOptions = msgpackOptions;
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
// replace enum resolver
DynamicEnumAsStringResolver.Instance,
DynamicGenericResolver.Instance,
DynamicUnionResolver.Instance,
DynamicObjectResolver.Instance,
PrimitiveObjectResolver.Instance,
// final fallback(last priority)
StandardResolver.Instance);
opt.SerializerOptions = MessagePackSerializerOptions.Standard var dummy = new GroupPruneSettingsDto(new GroupData("TEST-GID", null), true, 14);
.WithCompression(MessagePackCompression.Lz4Block) MessagePackSerializer.Serialize(dummy, msgpackOptions);
.WithResolver(resolver);
}); });
@@ -166,7 +165,7 @@ public class Startup
KeyPrefix = "", KeyPrefix = "",
Hosts = new RedisHost[] Hosts = new RedisHost[]
{ {
new RedisHost(){ Host = address, Port = port }, new(){ Host = address, Port = port },
}, },
AllowAdmin = true, AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout, ConnectTimeout = options.ConnectTimeout,
@@ -292,11 +291,12 @@ 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,6 +22,7 @@ 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

@@ -0,0 +1,68 @@
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,6 +7,7 @@
"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(10), token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromMinutes(1), 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

@@ -329,7 +329,7 @@ public partial class LightlessWizardModule : InteractionModuleBase
private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl)
{ {
var regex = new Regex(@"^https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/(\d{8})/?$"); var regex = new Regex(@"^https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/(\d+)/?$");
var matches = regex.Match(lodestoneUrl); var matches = regex.Match(lodestoneUrl);
var isLodestoneUrl = matches.Success; var isLodestoneUrl = matches.Success;
if (!isLodestoneUrl || matches.Groups.Count < 1) return null; if (!isLodestoneUrl || matches.Groups.Count < 1) return null;

View File

@@ -91,6 +91,12 @@ public class LightlessDbContext : DbContext
mb.Entity<GroupProfile>().ToTable("group_profiles"); mb.Entity<GroupProfile>().ToTable("group_profiles");
mb.Entity<GroupProfile>().HasKey(u => u.GroupGID); mb.Entity<GroupProfile>().HasKey(u => u.GroupGID);
mb.Entity<GroupProfile>().HasIndex(c => c.GroupGID); mb.Entity<GroupProfile>().HasIndex(c => c.GroupGID);
mb.Entity<Group>()
.HasOne(g => g.Profile)
.WithOne(p => p.Group)
.HasForeignKey<GroupProfile>(p => p.GroupGID)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
mb.Entity<GroupTempInvite>().ToTable("group_temp_invites"); mb.Entity<GroupTempInvite>().ToTable("group_temp_invites");
mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite }); mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID); mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);

View File

@@ -11,6 +11,7 @@ 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

@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LightlessSyncServer.Migrations
{
/// <inheritdoc />
public partial class FixForeignKeyGroupProfiles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_group_profiles_groups_group_gid",
table: "group_profiles");
migrationBuilder.AddForeignKey(
name: "fk_group_profiles_groups_group_gid",
table: "group_profiles",
column: "group_gid",
principalTable: "groups",
principalColumn: "gid",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_group_profiles_groups_group_gid",
table: "group_profiles");
migrationBuilder.AddForeignKey(
name: "fk_group_profiles_groups_group_gid",
table: "group_profiles",
column: "group_gid",
principalTable: "groups",
principalColumn: "gid");
}
}
}

View File

@@ -0,0 +1,40 @@
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,6 +430,14 @@ 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")
@@ -820,14 +828,14 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("character varying(15)") .HasColumnType("character varying(15)")
.HasColumnName("alias"); .HasColumnName("alias");
b.Property<bool>("HasVanity")
.HasColumnType("boolean")
.HasColumnName("has_vanity");
b.Property<bool>("ChatBanned") b.Property<bool>("ChatBanned")
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("chat_banned"); .HasColumnName("chat_banned");
b.Property<bool>("HasVanity")
.HasColumnType("boolean")
.HasColumnName("has_vanity");
b.Property<bool>("IsAdmin") b.Property<bool>("IsAdmin")
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("is_admin"); .HasColumnName("is_admin");
@@ -1220,6 +1228,7 @@ namespace LightlessSyncServer.Migrations
b.HasOne("LightlessSyncShared.Models.Group", "Group") b.HasOne("LightlessSyncShared.Models.Group", "Group")
.WithOne("Profile") .WithOne("Profile")
.HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID") .HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_group_profiles_groups_group_gid"); .HasConstraintName("fk_group_profiles_groups_group_gid");
b.Navigation("Group"); b.Navigation("Group");

View File

@@ -12,6 +12,8 @@ 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; }