Compare commits
31 Commits
metrics-li
...
syncshell-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
884ad25c33 | ||
|
|
3926f3be89 | ||
|
|
d28198a9c8 | ||
|
|
7cc6918b12 | ||
|
|
dba7536a7f | ||
|
|
f35c0c4c2a | ||
|
|
ad00f7b078 | ||
|
|
c30190704f | ||
|
|
bab81aaf51 | ||
|
|
4fdc2a5c29 | ||
|
|
bbcf98576e | ||
| 583f1a8957 | |||
|
|
2ebdd6e0c7 | ||
|
|
2407259769 | ||
| 03af0b853c | |||
|
|
53f663fcbf | ||
|
|
47a94cb79f | ||
| f933b40368 | |||
|
|
b670cb69dd | ||
|
|
50f3b0d644 | ||
|
|
3a6203844e | ||
|
|
80086f6817 | ||
| 7e565ff85e | |||
|
|
49177e639e | ||
|
|
b36b1fb8f9 | ||
|
|
79483205f1 | ||
|
|
280cc2ebbb | ||
|
|
7909850ad5 | ||
|
|
f60994fa58 | ||
|
|
96f230cd21 | ||
|
|
59f3739b9c |
Submodule LightlessAPI updated: 44fbe10458...0bc7abb274
@@ -209,7 +209,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
||||||
|
|
||||||
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid || g.Group.Alias == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||||
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
||||||
|
|
||||||
return (true, isOwnerResult.ReferredGroup);
|
return (true, isOwnerResult.ReferredGroup);
|
||||||
@@ -217,7 +217,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
||||||
{
|
{
|
||||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid || g.Alias == gid).ConfigureAwait(false);
|
||||||
if (group == null) return (false, null);
|
if (group == null) return (false, null);
|
||||||
|
|
||||||
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using LightlessSyncShared.Utils;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace LightlessSyncServer.Hubs;
|
namespace LightlessSyncServer.Hubs;
|
||||||
@@ -745,27 +747,39 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var cancellationToken = RequestAbortedToken;
|
var cancellationToken = RequestAbortedToken;
|
||||||
|
|
||||||
var data = await DbContext.GroupProfiles
|
if (dto?.Group == null)
|
||||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null);
|
|
||||||
|
|
||||||
if (data is not null)
|
|
||||||
{
|
{
|
||||||
profileDto = profileDto with
|
_logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is null"));
|
||||||
{
|
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
Description = data.Description,
|
|
||||||
Tags = data.Tags,
|
|
||||||
PictureBase64 = data.Base64GroupProfileImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
await Clients.User(UserUID)
|
|
||||||
.Client_GroupSendProfile(profileDto)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return profileDto;
|
var data = await DbContext.GroupProfiles
|
||||||
|
.Include(gp => gp.Group)
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ProfileDisabled)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(Group: dto.Group, Description: "This profile was permanently disabled", Tags: [], PictureBase64: null, IsNsfw: false, IsDisabled: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return data.ToDTO();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args(ex, "GroupGetProfile: failed to map GroupProfileDto for {Group}", dto.Group.GID ?? dto.Group.AliasOrGID));
|
||||||
|
return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -773,38 +787,88 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||||
|
|
||||||
|
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
|
||||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID,
|
.FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID,
|
||||||
RequestAbortedToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (groupProfileDb != null)
|
|
||||||
|
if (!string.IsNullOrEmpty(dto.PictureBase64))
|
||||||
{
|
{
|
||||||
groupProfileDb.Description = dto.Description;
|
byte[] imageData;
|
||||||
groupProfileDb.Tags = dto.Tags;
|
try
|
||||||
groupProfileDb.Base64GroupProfileImage = dto.PictureBase64;
|
{
|
||||||
|
imageData = Convert.FromBase64String(dto.PictureBase64);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryStream ms = new(imageData);
|
||||||
|
await using (ms.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using var image = Image.Load<Rgba32>(imageData);
|
||||||
|
|
||||||
|
if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupProfileDb == null)
|
||||||
|
{
|
||||||
|
groupProfileDb = new GroupProfile
|
||||||
|
{
|
||||||
|
GroupGID = dto.Group.GID,
|
||||||
|
ProfileDisabled = false,
|
||||||
|
IsNSFW = dto.IsNsfw ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
groupProfileDb.UpdateProfileFromDto(dto);
|
||||||
|
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var groupProfile = new GroupProfile
|
if (groupProfileDb?.ProfileDisabled ?? false)
|
||||||
{
|
{
|
||||||
GroupGID = dto.Group.GID,
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||||
Description = dto.Description,
|
return;
|
||||||
Tags = dto.Tags,
|
}
|
||||||
Base64GroupProfileImage = dto.PictureBase64,
|
|
||||||
};
|
|
||||||
|
|
||||||
await DbContext.GroupProfiles.AddAsync(groupProfile,
|
groupProfileDb.UpdateProfileFromDto(dto);
|
||||||
RequestAbortedToken)
|
|
||||||
|
var userIds = await DbContext.GroupPairs
|
||||||
|
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
|
||||||
|
.Select(p => p.GroupUserUID)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (userIds.Count > 0)
|
||||||
|
{
|
||||||
|
var profileDto = groupProfileDb.ToDTO();
|
||||||
|
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -990,6 +1054,4 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -523,6 +523,52 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(BroadcastRedisEntry? Entry, TimeSpan? Expiry)> TryGetBroadcastEntryAsync(string hashedCid)
|
||||||
|
{
|
||||||
|
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
RedisValueWithExpiry value;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = await _redis.Database.StringGetWithExpiryAsync(key).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileLookupFailed", "CID", hashedCid, "Error", ex));
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value.IsNullOrEmpty || value.Expiry is null || value.Expiry <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
BroadcastRedisEntry? entry;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entry = JsonSerializer.Deserialize<BroadcastRedisEntry>(value.Value!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileDeserializeFailed", "CID", hashedCid, "Raw", value.Value.ToString(), "Error", ex));
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileEntryMismatch", "CID", hashedCid, "EntryCID", entry?.HashedCID ?? "null"));
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (entry, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasActiveBroadcast(BroadcastRedisEntry? entry, TimeSpan? expiry) =>
|
||||||
|
entry?.HasOwner() == true && expiry.HasValue && expiry.Value > TimeSpan.Zero;
|
||||||
|
|
||||||
|
private static bool IsActiveBroadcastForUser(BroadcastRedisEntry? entry, TimeSpan? expiry, string userUid) =>
|
||||||
|
HasActiveBroadcast(entry, expiry) && entry!.OwnedBy(userUid);
|
||||||
|
|
||||||
private static bool IsValidHashedCid(string? cid)
|
private static bool IsValidHashedCid(string? cid)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(cid))
|
if (string.IsNullOrWhiteSpace(cid))
|
||||||
@@ -780,16 +826,107 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.");
|
return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||||
if (data == null) return new UserProfileDto(user.User, false, null, null, null);
|
if (data == null) return new UserProfileDto(user.User, false, null, null, null, []);
|
||||||
|
|
||||||
if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation");
|
if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation", []);
|
||||||
if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled");
|
if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled", []);
|
||||||
|
|
||||||
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription);
|
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription, data.Tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "Identified")]
|
||||||
|
public async Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||||
|
{
|
||||||
|
_logger.LogCallInfo(LightlessHubLogger.Args("LightfinderProfile", hashedCid));
|
||||||
|
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Lightfinder is currently disabled.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidHashedCid(hashedCid))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileInvalidCid", hashedCid));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Invalid Lightfinder target.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewerCid = UserCharaIdent;
|
||||||
|
if (!IsValidHashedCid(viewerCid))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (viewerEntry, viewerExpiry) = await TryGetBroadcastEntryAsync(viewerCid).ConfigureAwait(false);
|
||||||
|
if (!IsActiveBroadcastForUser(viewerEntry, viewerExpiry, UserUID))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (targetEntry, targetExpiry) = await TryGetBroadcastEntryAsync(hashedCid).ConfigureAwait(false);
|
||||||
|
if (!HasActiveBroadcast(targetEntry, targetExpiry))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(targetEntry!.OwnerUID))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUser = await DbContext.Users.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.UID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (targetUser == null)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileMissingUser", hashedCid, "OwnerUID", targetEntry.OwnerUID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Unable to load the players profile at this time.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayAlias = string.IsNullOrWhiteSpace(targetUser.Alias)
|
||||||
|
? "LightfinderUser"
|
||||||
|
: targetUser.Alias;
|
||||||
|
|
||||||
|
var userData = new UserData(
|
||||||
|
UID: hashedCid,
|
||||||
|
Alias: displayAlias,
|
||||||
|
IsAdmin: false,
|
||||||
|
IsModerator: false,
|
||||||
|
HasVanity: false,
|
||||||
|
TextColorHex: targetUser.TextColorHex,
|
||||||
|
TextGlowColorHex: targetUser.TextGlowColorHex);
|
||||||
|
|
||||||
|
var profile = await DbContext.UserProfileData.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.UserUID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, false, null, null, null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.FlaggedForReport)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.ProfileDisabled)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription, profile.Tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -989,76 +1126,78 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||||
|
|
||||||
|
var cancellationToken = RequestAbortedToken;
|
||||||
|
|
||||||
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
|
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
|
||||||
|
|
||||||
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (existingData?.FlaggedForReport ?? false)
|
//Image Check of size/format
|
||||||
{
|
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingData?.ProfileDisabled ?? false)
|
|
||||||
{
|
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
||||||
{
|
{
|
||||||
byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
|
byte[] imageData;
|
||||||
using MemoryStream ms = new(imageData);
|
try
|
||||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
|
||||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
using var image = Image.Load<Rgba32>(imageData);
|
|
||||||
|
|
||||||
if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024))
|
MemoryStream ms = new(imageData);
|
||||||
|
await using (ms.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false);
|
var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false);
|
||||||
return;
|
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using var image = Image.Load<Rgba32>(imageData);
|
||||||
|
|
||||||
|
if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingData != null)
|
if (existingData != null)
|
||||||
{
|
{
|
||||||
if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase))
|
if (existingData.FlaggedForReport)
|
||||||
{
|
{
|
||||||
existingData.Base64ProfileImage = null;
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
||||||
}
|
return;
|
||||||
else if (dto.ProfilePictureBase64 != null)
|
|
||||||
{
|
|
||||||
existingData.Base64ProfileImage = dto.ProfilePictureBase64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.IsNSFW != null)
|
if (existingData.ProfileDisabled)
|
||||||
{
|
{
|
||||||
existingData.IsNSFW = dto.IsNSFW.Value;
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.Description != null)
|
existingData.UpdateProfileFromDto(dto);
|
||||||
{
|
|
||||||
existingData.UserDescription = dto.Description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UserProfileData userProfileData = new()
|
UserProfileData newUserProfileData = new()
|
||||||
{
|
{
|
||||||
UserUID = dto.User.UID,
|
UserUID = dto.User.UID,
|
||||||
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
||||||
UserDescription = dto.Description ?? null,
|
UserDescription = dto.Description ?? null,
|
||||||
IsNSFW = dto.IsNSFW ?? false
|
IsNSFW = dto.IsNSFW ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false);
|
newUserProfileData.UpdateProfileFromDto(dto);
|
||||||
|
|
||||||
|
await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
|
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
using static LightlessSyncServer.Hubs.LightlessHub;
|
using static LightlessSyncServer.Hubs.LightlessHub;
|
||||||
|
|
||||||
@@ -8,18 +10,85 @@ namespace LightlessSyncServer.Utils;
|
|||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
public static void UpdateProfileFromDto(this GroupProfile profile, GroupProfileDto dto)
|
||||||
|
{
|
||||||
|
if (profile == null || dto == null) return;
|
||||||
|
|
||||||
|
profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(dto.PictureBase64) ? null : dto.PictureBase64;
|
||||||
|
if (dto.Tags != null) profile.Tags = dto.Tags;
|
||||||
|
if (dto.Description != null) profile.Description = dto.Description;
|
||||||
|
if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto)
|
||||||
|
{
|
||||||
|
if (profile == null || dto == null) return;
|
||||||
|
|
||||||
|
profile.Base64ProfileImage = string.IsNullOrWhiteSpace(dto.ProfilePictureBase64) ? null : dto.ProfilePictureBase64;
|
||||||
|
if (dto.Tags != null) profile.Tags = dto.Tags;
|
||||||
|
if (dto.Description != null) profile.UserDescription = dto.Description;
|
||||||
|
if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GroupProfileDto ToDTO(this GroupProfile groupProfile)
|
||||||
|
{
|
||||||
|
if (groupProfile == null)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupData = groupProfile.Group?.ToGroupData();
|
||||||
|
|
||||||
|
return new GroupProfileDto(
|
||||||
|
groupData,
|
||||||
|
groupProfile.Description,
|
||||||
|
groupProfile.Tags,
|
||||||
|
groupProfile.Base64GroupProfileImage,
|
||||||
|
groupProfile.IsNSFW,
|
||||||
|
groupProfile.ProfileDisabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserProfileDto ToDTO(this UserProfileData userProfileData)
|
||||||
|
{
|
||||||
|
if (userProfileData == null)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null, Tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = userProfileData.User?.ToUserData();
|
||||||
|
|
||||||
|
return new UserProfileDto(
|
||||||
|
userData,
|
||||||
|
userProfileData.ProfileDisabled,
|
||||||
|
userProfileData.IsNSFW,
|
||||||
|
userProfileData.Base64ProfileImage,
|
||||||
|
userProfileData.UserDescription,
|
||||||
|
userProfileData.Tags
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static GroupData ToGroupData(this Group group)
|
public static GroupData ToGroupData(this Group group)
|
||||||
{
|
{
|
||||||
|
if (group == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new GroupData(group.GID, group.Alias, group.CreatedDate);
|
return new GroupData(group.GID, group.Alias, group.CreatedDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserData ToUserData(this GroupPair pair)
|
public static UserData ToUserData(this GroupPair pair)
|
||||||
{
|
{
|
||||||
|
if (pair == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserData ToUserData(this User user)
|
public static UserData ToUserData(this User user)
|
||||||
{
|
{
|
||||||
|
if (user == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new UserData(user.UID, user.Alias);
|
return new UserData(user.UID, user.Alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1177
LightlessSyncServer/LightlessSyncShared/Migrations/20251015173920_AddGroupDisabledAndNSFW.Designer.cs
generated
Normal file
1177
LightlessSyncServer/LightlessSyncShared/Migrations/20251015173920_AddGroupDisabledAndNSFW.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddGroupDisabledAndNSFW : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_nsfw",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "profile_disabled",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_nsfw",
|
||||||
|
table: "group_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "profile_disabled",
|
||||||
|
table: "group_profiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAndChangeTagsUserGroupProfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int[]>(
|
||||||
|
name: "tags",
|
||||||
|
table: "user_profile_data",
|
||||||
|
type: "integer[]",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.Sql("UPDATE group_profiles SET tags = NULL;");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int[]>(
|
||||||
|
name: "tags",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "integer[]",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "tags",
|
||||||
|
table: "user_profile_data");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "tags",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int[]),
|
||||||
|
oldType: "integer[]",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -597,8 +597,16 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("description");
|
.HasColumnName("description");
|
||||||
|
|
||||||
b.Property<string>("Tags")
|
b.Property<bool>("IsNSFW")
|
||||||
.HasColumnType("text")
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_nsfw");
|
||||||
|
|
||||||
|
b.Property<bool>("ProfileDisabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("profile_disabled");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<int[]>("Tags")
|
||||||
|
.HasColumnType("integer[]")
|
||||||
.HasColumnName("tags");
|
.HasColumnName("tags");
|
||||||
|
|
||||||
b.HasKey("GroupGID")
|
b.HasKey("GroupGID")
|
||||||
@@ -832,6 +840,10 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("profile_disabled");
|
.HasColumnName("profile_disabled");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<int[]>("Tags")
|
||||||
|
.HasColumnType("integer[]")
|
||||||
|
.HasColumnName("tags");
|
||||||
|
|
||||||
b.Property<string>("UserDescription")
|
b.Property<string>("UserDescription")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("user_description");
|
.HasColumnName("user_description");
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public class GroupProfile
|
|||||||
public string GroupGID { get; set; }
|
public string GroupGID { get; set; }
|
||||||
public Group Group { get; set; }
|
public Group Group { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Tags { get; set; }
|
public int[] Tags { get; set; }
|
||||||
public string Base64GroupProfileImage { get; set; }
|
public string Base64GroupProfileImage { get; set; }
|
||||||
|
public bool IsNSFW { get; set; } = false;
|
||||||
|
public bool ProfileDisabled { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class UserProfileData
|
|||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
public string UserDescription { get; set; }
|
public string UserDescription { get; set; }
|
||||||
|
public int[] Tags { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Key]
|
[Key]
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ public class ServerFilesController : ControllerBase
|
|||||||
private readonly LightlessMetrics _metricsClient;
|
private readonly LightlessMetrics _metricsClient;
|
||||||
private readonly MainServerShardRegistrationService _shardRegistrationService;
|
private readonly MainServerShardRegistrationService _shardRegistrationService;
|
||||||
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
||||||
|
private readonly CDNDownloadsService _cdnDownloadsService;
|
||||||
|
|
||||||
public ServerFilesController(ILogger<ServerFilesController> logger, CachedFileProvider cachedFileProvider,
|
public ServerFilesController(ILogger<ServerFilesController> logger, CachedFileProvider cachedFileProvider,
|
||||||
IConfigurationService<StaticFilesServerConfiguration> configuration,
|
IConfigurationService<StaticFilesServerConfiguration> configuration,
|
||||||
IHubContext<LightlessHub> hubContext,
|
IHubContext<LightlessHub> hubContext,
|
||||||
IDbContextFactory<LightlessDbContext> lightlessDbContext, LightlessMetrics metricsClient,
|
IDbContextFactory<LightlessDbContext> lightlessDbContext, LightlessMetrics metricsClient,
|
||||||
MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService) : base(logger)
|
MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService,
|
||||||
|
CDNDownloadsService cdnDownloadsService) : base(logger)
|
||||||
{
|
{
|
||||||
_basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)
|
_basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)
|
||||||
? configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory))
|
? configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory))
|
||||||
@@ -51,6 +53,7 @@ public class ServerFilesController : ControllerBase
|
|||||||
_metricsClient = metricsClient;
|
_metricsClient = metricsClient;
|
||||||
_shardRegistrationService = shardRegistrationService;
|
_shardRegistrationService = shardRegistrationService;
|
||||||
_cdnDownloadUrlService = cdnDownloadUrlService;
|
_cdnDownloadUrlService = cdnDownloadUrlService;
|
||||||
|
_cdnDownloadsService = cdnDownloadsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
|
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
|
||||||
@@ -145,24 +148,16 @@ public class ServerFilesController : 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)
|
||||||
{
|
{
|
||||||
if (!_cdnDownloadUrlService.DirectDownloadsEnabled)
|
var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = hash.ToUpperInvariant();
|
return result.Status switch
|
||||||
if (!_cdnDownloadUrlService.TryValidateSignature(hash, expires, signature))
|
|
||||||
{
|
{
|
||||||
return Unauthorized();
|
CDNDownloadsService.ResultStatus.Disabled => NotFound(),
|
||||||
}
|
CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
|
||||||
|
CDNDownloadsService.ResultStatus.NotFound => NotFound(),
|
||||||
var fileInfo = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false);
|
CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
|
||||||
if (fileInfo == null)
|
_ => NotFound()
|
||||||
{
|
};
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return PhysicalFile(fileInfo.FullName, "application/octet-stream");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost(LightlessFiles.ServerFiles_FilesSend)]
|
[HttpPost(LightlessFiles.ServerFiles_FilesSend)]
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using LightlessSync.API.Routes;
|
||||||
|
using LightlessSyncStaticFilesServer.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace LightlessSyncStaticFilesServer.Controllers;
|
||||||
|
|
||||||
|
[Route(LightlessFiles.ServerFiles)]
|
||||||
|
public class ShardServerFilesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CDNDownloadsService _cdnDownloadsService;
|
||||||
|
|
||||||
|
public ShardServerFilesController(ILogger<ShardServerFilesController> logger,
|
||||||
|
CDNDownloadsService cdnDownloadsService) : base(logger)
|
||||||
|
{
|
||||||
|
_cdnDownloadsService = cdnDownloadsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature)
|
||||||
|
{
|
||||||
|
var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result.Status switch
|
||||||
|
{
|
||||||
|
CDNDownloadsService.ResultStatus.Disabled => NotFound(),
|
||||||
|
CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
|
||||||
|
CDNDownloadsService.ResultStatus.NotFound => NotFound(),
|
||||||
|
CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
|
||||||
|
_ => NotFound()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LightlessSyncStaticFilesServer.Services;
|
||||||
|
|
||||||
|
public class CDNDownloadsService
|
||||||
|
{
|
||||||
|
public enum ResultStatus
|
||||||
|
{
|
||||||
|
Disabled,
|
||||||
|
Unauthorized,
|
||||||
|
NotFound,
|
||||||
|
Success
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct Result(ResultStatus Status, FileInfo? File);
|
||||||
|
|
||||||
|
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
||||||
|
private readonly CachedFileProvider _cachedFileProvider;
|
||||||
|
|
||||||
|
public CDNDownloadsService(CDNDownloadUrlService cdnDownloadUrlService, CachedFileProvider cachedFileProvider)
|
||||||
|
{
|
||||||
|
_cdnDownloadUrlService = cdnDownloadUrlService;
|
||||||
|
_cachedFileProvider = cachedFileProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DownloadsEnabled => _cdnDownloadUrlService.DirectDownloadsEnabled;
|
||||||
|
|
||||||
|
public async Task<Result> GetDownloadAsync(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 = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false);
|
||||||
|
if (fileInfo == null)
|
||||||
|
{
|
||||||
|
return new Result(ResultStatus.NotFound, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result(ResultStatus.Success, fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ public class Startup
|
|||||||
services.AddSingleton<ServerTokenGenerator>();
|
services.AddSingleton<ServerTokenGenerator>();
|
||||||
services.AddSingleton<RequestQueueService>();
|
services.AddSingleton<RequestQueueService>();
|
||||||
services.AddSingleton<CDNDownloadUrlService>();
|
services.AddSingleton<CDNDownloadUrlService>();
|
||||||
|
services.AddSingleton<CDNDownloadsService>();
|
||||||
services.AddHostedService(p => p.GetService<RequestQueueService>());
|
services.AddHostedService(p => p.GetService<RequestQueueService>());
|
||||||
services.AddHostedService(m => m.GetService<FileStatisticsService>());
|
services.AddHostedService(m => m.GetService<FileStatisticsService>());
|
||||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||||
@@ -205,7 +206,8 @@ public class Startup
|
|||||||
}
|
}
|
||||||
else if (_isDistributionNode)
|
else if (_isDistributionNode)
|
||||||
{
|
{
|
||||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(DistributionController), typeof(SpeedTestController)));
|
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController),
|
||||||
|
typeof(DistributionController), typeof(ShardServerFilesController), typeof(SpeedTestController)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user