Compare commits

..

12 Commits

Author SHA1 Message Date
c3784de9cd Merge branch 'master' into fix-prune 2026-01-02 22:22:36 +00:00
cake
5aaa6ff35c update submodule 2026-01-02 23:21:33 +01:00
cake
2ef0725f4e fix submodule 2026-01-02 23:18:04 +01:00
cake
6e540ad0c6 Removal of zero days. 2025-12-30 02:05:06 +01:00
cake
863042d1fe Merge branch 'Location' into fix-prune 2025-12-30 01:52:42 +01:00
cake
b29475ea41 Check on redis if user is online as well. 2025-12-30 01:40:41 +01: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
5 changed files with 100 additions and 200 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"));
} }
@@ -373,7 +326,7 @@ public partial class LightlessHub
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), 1))
.ConfigureAwait(false); .ConfigureAwait(false);
_logger.LogCallInfo(LightlessHubLogger.Args(gid)); _logger.LogCallInfo(LightlessHubLogger.Args(gid));
return new GroupJoinDto(newGroup.ToGroupData(), passwd, initialPrefPermissions.ToEnum()); return new GroupJoinDto(newGroup.ToGroupData(), passwd, initialPrefPermissions.ToEnum());
@@ -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));
@@ -878,7 +831,7 @@ public partial class LightlessHub
} }
var data = await DbContext.GroupProfiles var data = await DbContext.GroupProfiles
.Include(gp => gp.Group) .Include(gp => gp.Group)
.FirstOrDefaultAsync( .FirstOrDefaultAsync(
g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID, g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID,
cancellationToken cancellationToken
@@ -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

@@ -20,14 +20,13 @@ public class ShardServerFilesController : ControllerBase
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature) public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature)
{ {
var result = _cdnDownloadsService.GetDownloadWithCacheCheck(hash, expires, signature); var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
return result.Status switch return result.Status switch
{ {
CDNDownloadsService.ResultStatus.Disabled => NotFound(), CDNDownloadsService.ResultStatus.Disabled => NotFound(),
CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(), CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
CDNDownloadsService.ResultStatus.NotFound => NotFound(), CDNDownloadsService.ResultStatus.NotFound => NotFound(),
CDNDownloadsService.ResultStatus.Downloading => StatusCode(503),
CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"), CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
_ => NotFound() _ => NotFound()
}; };

View File

@@ -10,8 +10,7 @@ public class CDNDownloadsService
Disabled, Disabled,
Unauthorized, Unauthorized,
NotFound, NotFound,
Success, Success
Downloading
} }
public readonly record struct Result(ResultStatus Status, FileInfo? File); public readonly record struct Result(ResultStatus Status, FileInfo? File);
@@ -54,32 +53,4 @@ public class CDNDownloadsService
return new Result(ResultStatus.Success, fileInfo); return new Result(ResultStatus.Success, fileInfo);
} }
public Result GetDownloadWithCacheCheck(string hash, long expiresUnixSeconds, string signature)
{
if (!_cdnDownloadUrlService.DirectDownloadsEnabled)
{
return new Result(ResultStatus.Disabled, null);
}
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(hash))
{
return new Result(ResultStatus.Unauthorized, null);
}
hash = hash.ToUpperInvariant();
if (!_cdnDownloadUrlService.TryValidateSignature(hash, expiresUnixSeconds, signature))
{
return new Result(ResultStatus.Unauthorized, null);
}
var fileInfo = _cachedFileProvider.TryGetLocalFileInfo(hash);
if (fileInfo == null)
{
return new Result(ResultStatus.Downloading, null);
}
return new Result(ResultStatus.Success, fileInfo);
}
} }

View File

@@ -219,27 +219,4 @@ public sealed class CachedFileProvider : IDisposable
{ {
return hashes.Exists(_currentTransfers.Keys.Contains); return hashes.Exists(_currentTransfers.Keys.Contains);
} }
public FileInfo? TryGetLocalFileInfo(string hash)
{
var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash);
if (fi != null)
{
return GetLocalFilePath(hash);
}
_ = Task.Run(async () =>
{
try
{
await DownloadFileWhenRequired(hash).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Download failed for {hash}", hash);
}
});
return null;
}
} }