Compare commits

..

7 Commits

Author SHA1 Message Date
Tsubasahane
1786ad1683 Update LightlessAPI 2025-12-31 22:56:32 +08:00
Tsubasahane
53e089a65b ToggleLocationSharing returns bool 2025-12-29 15:35:54 +08:00
Tsubasahane
c3e87eb7df ShareLocation : Migration 2025-12-29 09:55:53 +08:00
Tsubasahane
a533fca195 transfer sharing status 2025-12-28 19:54:26 +08:00
Tsubasahane
717d8e46f1 Location Share but with expireAt 2025-12-28 13:07:36 +08:00
Tsubasahane
d3790013c6 Revert "Share Location"
This reverts commit 9971b14177.
2025-12-28 11:00:50 +08:00
Tsubasahane
9971b14177 Share Location 2025-12-27 19:52:28 +08:00
4 changed files with 117 additions and 204 deletions

View File

@@ -28,72 +28,25 @@ public partial class LightlessHub
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
if (!userHasRights) return; if (!userHasRights) return;
var targetUid = dto.User.UID?.Trim(); var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(targetUid)) return; if (!userExists) return;
if (string.Equals(group.OwnerUID, targetUid, StringComparison.Ordinal)) if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return;
return;
var groupPair = await DbContext.GroupPairs var alias = string.IsNullOrEmpty(groupPair.GroupUser.Alias) ? "-" : groupPair.GroupUser.Alias;
.Include(p => p.GroupUser) var ban = new GroupBan()
.SingleOrDefaultAsync(p => p.GroupGID == dto.Group.GID && p.GroupUserUID == targetUid, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
if (groupPair?.IsModerator == true)
return;
var now = DateTime.UtcNow;
var existingBan = await DbContext.Set<GroupBan>().SingleOrDefaultAsync(b => b.GroupGID == dto.Group.GID && b.BannedUserUID == targetUid, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
var userExists = await DbContext.Users.AsNoTracking().AnyAsync(u => u.UID == targetUid || u.Alias == targetUid, RequestAbortedToken).ConfigureAwait(false);
if (!userExists && existingBan == null)
return;
const string marker = " (Alias at time of ban:";
string suffix;
if (existingBan?.BannedReason is { } existingReason)
{ {
var idx = existingReason.IndexOf(marker, StringComparison.Ordinal); BannedByUID = UserUID,
suffix = idx >= 0 ? existingReason.Substring(startIndex: idx) : string.Empty; BannedReason = $"{reason} (Alias at time of ban: {alias})",
} BannedOn = DateTime.UtcNow,
else BannedUserUID = dto.User.UID,
{ GroupGID = dto.Group.GID,
var alias = groupPair?.GroupUser?.Alias; };
alias = string.IsNullOrWhiteSpace(alias) ? "-" : alias;
suffix = $" (Alias at time of ban: {alias})";
}
var baseReason = (reason ?? string.Empty).Trim(); DbContext.Add(ban);
var finalReason = string.IsNullOrEmpty(suffix) ? baseReason : (baseReason + suffix); await DbContext.SaveChangesAsync().ConfigureAwait(false);
if (existingBan != null) await GroupRemoveUser(dto).ConfigureAwait(false);
{
existingBan.BannedByUID = UserUID;
existingBan.BannedReason = finalReason;
DbContext.Update(existingBan);
}
else
{
var ban = new GroupBan
{
BannedByUID = UserUID,
BannedReason = finalReason,
BannedOn = now,
BannedUserUID = targetUid,
GroupGID = dto.Group.GID,
};
DbContext.Add(ban);
}
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
if (groupPair != null)
{
await GroupRemoveUser(dto).ConfigureAwait(false);
}
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
} }
@@ -447,9 +400,9 @@ public partial class LightlessHub
var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false);
List<BannedGroupUserDto> bannedGroupUsers = [.. banEntries.Select(b => List<BannedGroupUserDto> bannedGroupUsers = banEntries.Select(b =>
new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn,
b.BannedByUID))]; b.BannedByUID)).ToList();
_logger.LogCallInfo(LightlessHubLogger.Args(dto, bannedGroupUsers.Count)); _logger.LogCallInfo(LightlessHubLogger.Args(dto, bannedGroupUsers.Count));
@@ -909,85 +862,85 @@ public partial class LightlessHub
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
public async Task GroupSetProfile(GroupProfileDto dto) public async Task GroupSetProfile(GroupProfileDto dto)
{ {
_logger.LogCallInfo(LightlessHubLogger.Args(dto)); _logger.LogCallInfo(LightlessHubLogger.Args(dto));
var cancellationToken = RequestAbortedToken; var cancellationToken = RequestAbortedToken;
if (dto.Group == null) return; if (dto.Group == null) return;
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
if (!hasRights) return; if (!hasRights) return;
var groupProfileDb = await DbContext.GroupProfiles var groupProfileDb = await DbContext.GroupProfiles
.Include(g => g.Group) .Include(g => g.Group)
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken)
.ConfigureAwait(false);
ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.PictureBase64))
{
profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
if (!profileResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
//Banner image validation
if (!string.IsNullOrEmpty(dto.BannerBase64))
{
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
if (!bannerResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
var sanitizedProfileImage = profileResult?.Base64Image;
var sanitizedBannerImage = bannerResult?.Base64Image;
if (groupProfileDb == null)
{
groupProfileDb = new GroupProfile
{
GroupGID = dto.Group.GID,
Group = group,
ProfileDisabled = dto.IsDisabled ?? false,
IsNSFW = dto.IsNsfw ?? false,
};
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
}
else
{
groupProfileDb.Group ??= group;
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
}
var userIds = await DbContext.GroupPairs
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
.Select(p => p.GroupUserUID)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
if (userIds.Count > 0)
{
var profileDto = groupProfileDb.ToDTO();
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
.ConfigureAwait(false); .ConfigureAwait(false);
}
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.PictureBase64))
{
profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
if (!profileResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
//Banner image validation
if (!string.IsNullOrEmpty(dto.BannerBase64))
{
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
if (!bannerResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
var sanitizedProfileImage = profileResult?.Base64Image;
var sanitizedBannerImage = bannerResult?.Base64Image;
if (groupProfileDb == null)
{
groupProfileDb = new GroupProfile
{
GroupGID = dto.Group.GID,
Group = group,
ProfileDisabled = dto.IsDisabled ?? false,
IsNSFW = dto.IsNsfw ?? false,
};
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
}
else
{
groupProfileDb.Group ??= group;
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
}
var userIds = await DbContext.GroupPairs
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
.Select(p => p.GroupUserUID)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
if (userIds.Count > 0)
{
var profileDto = groupProfileDb.ToDTO();
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
.ConfigureAwait(false);
}
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -1150,11 +1103,11 @@ public partial class LightlessHub
return false; return false;
} }
var (isOwnerOrMod, _) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false);
if (!isOwnerOrMod) 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 or moderator of the syncshell to broadcast it."); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner of the syncshell to broadcast it.");
return false; return false;
} }

View File

@@ -72,7 +72,7 @@ public class LightlessCensus : IHostedService
Dictionary<ushort, short> worldDcs = new(); Dictionary<ushort, short> worldDcs = new();
var dcs = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/en/WorldDCGroupType.csv", cancellationToken).ConfigureAwait(false); var dcs = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv", cancellationToken).ConfigureAwait(false);
// dc: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv // dc: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/WorldDCGroupType.csv
// id, name, region // id, name, region
@@ -92,7 +92,7 @@ public class LightlessCensus : IHostedService
_dcs[id] = name; _dcs[id] = name;
} }
var worlds = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/en/World.csv", cancellationToken).ConfigureAwait(false); var worlds = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv", cancellationToken).ConfigureAwait(false);
// world: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv // world: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/World.csv
// id, internalname, name, region, usertype, datacenter, ispublic // id, internalname, name, region, usertype, datacenter, ispublic
@@ -114,7 +114,7 @@ public class LightlessCensus : IHostedService
_logger.LogInformation("World: ID: {id}, Name: {name}, DC: {dc}", id, name, dc); _logger.LogInformation("World: ID: {id}, Name: {name}, DC: {dc}", id, name, dc);
} }
var races = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/en/Race.csv", cancellationToken).ConfigureAwait(false); var races = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv", cancellationToken).ConfigureAwait(false);
// race: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv // race: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Race.csv
// id, masc name, fem name, other crap I don't care about // id, masc name, fem name, other crap I don't care about
@@ -134,7 +134,7 @@ public class LightlessCensus : IHostedService
_logger.LogInformation("Race: ID: {id}, Name: {name}", id, name); _logger.LogInformation("Race: ID: {id}, Name: {name}", id, name);
} }
var tribe = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/en/Tribe.csv", cancellationToken).ConfigureAwait(false); var tribe = await client.GetStringAsync("https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv", cancellationToken).ConfigureAwait(false);
// tribe: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv // tribe: https://raw.githubusercontent.com/xivapi/ffxiv-datamining/master/csv/Tribe.csv
// id masc name, fem name, other crap I don't care about // id masc name, fem name, other crap I don't care about

View File

@@ -2,19 +2,15 @@
using LightlessSyncShared.Data; using LightlessSyncShared.Data;
using LightlessSyncShared.Models; using LightlessSyncShared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis.Extensions.Core.Abstractions;
namespace LightlessSyncServer.Services namespace LightlessSyncServer.Services
{ {
public class PruneService(LightlessDbContext dbContext, IRedisDatabase redis) : IPruneService public class PruneService(LightlessDbContext dbContext) : IPruneService
{ {
private readonly LightlessDbContext _dbContext = dbContext; private readonly LightlessDbContext _dbContext = dbContext;
private readonly IRedisDatabase _redis = redis;
public async Task<int> CountPrunableUsersAsync(string groupGid, int days, CancellationToken ct) public async Task<int> CountPrunableUsersAsync(string groupGid, int days, CancellationToken ct)
{ {
var onlineUids = await GetOnlineUidsAsync().ConfigureAwait(false);
var allGroupUsers = await _dbContext.GroupPairs var allGroupUsers = await _dbContext.GroupPairs
.Include(p => p.GroupUser) .Include(p => p.GroupUser)
.Include(p => p.Group) .Include(p => p.Group)
@@ -24,14 +20,17 @@ namespace LightlessSyncServer.Services
var inactivitySpan = GetInactivitySpan(days); var inactivitySpan = GetInactivitySpan(days);
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var usersToPrune = GetPruneUserList(allGroupUsers, onlineUids, inactivitySpan, now); var usersToPrune = allGroupUsers.Where(p =>
return usersToPrune.Count; !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) public async Task<IReadOnlyList<GroupPair>> ExecutePruneAsync(string groupGid, int days, CancellationToken ct)
{ {
var onlineUids = await GetOnlineUidsAsync().ConfigureAwait(false);
var allGroupUsers = await _dbContext.GroupPairs var allGroupUsers = await _dbContext.GroupPairs
.Include(p => p.GroupUser) .Include(p => p.GroupUser)
.Include(p => p.Group) .Include(p => p.Group)
@@ -41,7 +40,12 @@ namespace LightlessSyncServer.Services
var inactivitySpan = GetInactivitySpan(days); var inactivitySpan = GetInactivitySpan(days);
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var usersToPrune = GetPruneUserList(allGroupUsers, onlineUids, inactivitySpan, now); 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); _dbContext.GroupPairs.RemoveRange(usersToPrune);
await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false); await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
@@ -49,52 +53,8 @@ namespace LightlessSyncServer.Services
return usersToPrune; return usersToPrune;
} }
private static List<GroupPair> GetPruneUserList( private static TimeSpan GetInactivitySpan(int days) => days == 0
List<GroupPair> allGroupUsers, ? TimeSpan.FromMinutes(15)
HashSet<string> onlineUids, : TimeSpan.FromDays(days);
TimeSpan inactivitySpan,
DateTime now)
{
return
[
.. allGroupUsers.Where(p =>
!p.IsPinned &&
!p.IsModerator &&
!string.Equals(p.Group.OwnerUID, p.GroupUserUID, StringComparison.Ordinal) &&
!onlineUids.Contains(p.GroupUserUID) &&
p.GroupUser.LastLoggedIn < now - inactivitySpan),
];
}
private async Task<HashSet<string>> GetOnlineUidsAsync()
{
var keys = await _redis.SearchKeysAsync("UID:*").ConfigureAwait(false);
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var k in keys)
{
if (string.IsNullOrEmpty(k)) continue;
const string prefix = "UID:";
if (k.StartsWith(prefix, StringComparison.Ordinal))
{
var uid = k.Substring(prefix.Length);
if (!string.IsNullOrEmpty(uid))
set.Add(uid);
}
else
{
var idx = k.IndexOf(':', StringComparison.Ordinal);
if (idx >= 0 && idx < k.Length - 1)
set.Add(k[(idx + 1)..]);
}
}
return set;
}
private static TimeSpan GetInactivitySpan(int days) =>
days == 0 ? TimeSpan.FromHours(2) : TimeSpan.FromDays(days);
} }
} }

View File

@@ -24,7 +24,7 @@ namespace LightlessSyncServer.Worker
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<LightlessHub>>(); var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<LightlessHub>>();
var groups = await db.Groups var groups = await db.Groups
.Where(g => g.AutoPruneEnabled) .Where(g => g.AutoPruneEnabled && g.AutoPruneDays > 0)
.ToListAsync(stoppingToken).ConfigureAwait(false); .ToListAsync(stoppingToken).ConfigureAwait(false);
foreach (var group in groups) foreach (var group in groups)