Compare commits

...

12 Commits

Author SHA1 Message Date
6216664e18 Merge branch 'master' into cache-test-fix 2025-12-23 17:12:45 +00:00
54992234d5 Merge pull request 'disable-chat-groups' (#45) from disable-chat-groups into master
Reviewed-on: #45
2025-12-23 16:55:33 +00:00
90e81fb955 Merge branch 'master' into disable-chat-groups 2025-12-23 16:55:21 +00:00
defnotken
4012b33f98 Trying a different method of setting access time. 2025-12-23 10:36:38 -06:00
33fb6ba2eb Merge pull request 'Done same for user' (#47) from fix-profile-hide into master
Reviewed-on: #47
2025-12-22 20:31:36 +00:00
5b5f8edc3b Merge pull request 'Removed code that would prevent changes on disabled profile.' (#46) from fix-profile-hide into master
Reviewed-on: #46
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-22 14:53:52 +00:00
defnotken
4ec6169ebd fix context 2025-12-21 21:02:59 -06:00
defnotken
ec26abaeb5 Revert "Merge pull request 'revert ce9b94a534c1239b4b89c4ccf05354c477da49d9' (#44) from defnotken-patch-1 into master"
This reverts commit 76bdbe28da, reversing
changes made to ce9b94a534.
2025-12-21 21:00:53 -06:00
3350f2ebf6 Merge branch 'master' into disable-chat-groups 2025-12-22 02:57:55 +00:00
defnotken
670e0e01f1 Merge branch 'disable-chat-groups' of https://git.lightless-sync.org/Lightless-Sync/LightlessServer into disable-chat-groups 2025-12-21 20:48:22 -06:00
defnotken
12d51dc689 Generate migration 2025-12-21 20:48:07 -06:00
defnotken
860bc955a4 Generate migration 2025-12-21 20:43:24 -06:00
10 changed files with 1504 additions and 42 deletions

View File

@@ -41,8 +41,10 @@ public partial class LightlessHub
var groupInfos = await DbContext.Groups var groupInfos = await DbContext.Groups
.AsNoTracking() .AsNoTracking()
.Where(g => g.OwnerUID == userUid .Where(g => g.ChatEnabled
&& (g.OwnerUID == userUid
|| DbContext.GroupPairs.Any(p => p.GroupGID == g.GID && p.GroupUserUID == userUid)) || DbContext.GroupPairs.Any(p => p.GroupGID == g.GID && p.GroupUserUID == userUid))
)
.ToListAsync() .ToListAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -162,6 +164,13 @@ public partial class LightlessHub
throw new HubException("Unsupported chat channel."); throw new HubException("Unsupported chat channel.");
} }
if (!group.ChatEnabled)
{
TryInvokeChatService(() => _chatChannelService.RemovePresence(UserUID, channel), "removing chat presence", channel);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "This Syncshell chat is disabled.").ConfigureAwait(false);
return;
}
var isMember = string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal) var isMember = string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal)
|| await DbContext.GroupPairs || await DbContext.GroupPairs
.AsNoTracking() .AsNoTracking()
@@ -207,6 +216,23 @@ public partial class LightlessHub
var channel = request.Channel.WithNormalizedCustomKey(); var channel = request.Channel.WithNormalizedCustomKey();
if (channel.Type == ChatChannelType.Group)
{
var groupId = channel.CustomKey ?? string.Empty;
var chatEnabled = !string.IsNullOrEmpty(groupId) && await DbContext.Groups
.AsNoTracking()
.Where(g => g.GID == groupId)
.Select(g => g.ChatEnabled)
.SingleOrDefaultAsync(RequestAbortedToken)
.ConfigureAwait(false);
if (!chatEnabled)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "This Syncshell chat is disabled.").ConfigureAwait(false);
return;
}
}
if (await HandleIfChatBannedAsync(UserUID).ConfigureAwait(false)) if (await HandleIfChatBannedAsync(UserUID).ConfigureAwait(false))
{ {
throw new HubException("Chat access has been revoked."); throw new HubException("Chat access has been revoked.");

View File

@@ -1,6 +1,7 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSyncServer.Models; using LightlessSyncServer.Models;
@@ -11,10 +12,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 Newtonsoft.Json.Linq;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using System.Collections.Concurrent;
using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
namespace LightlessSyncServer.Hubs; namespace LightlessSyncServer.Hubs;
@@ -52,6 +51,8 @@ public partial class LightlessHub
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
} }
private static readonly ConcurrentDictionary<string, DateTime> GroupChatToggleCooldowns = new(StringComparer.Ordinal);
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto) public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto)
{ {
@@ -60,15 +61,73 @@ public partial class LightlessHub
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;
group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites); var permissions = dto.Permissions;
group.PreferDisableSounds = dto.Permissions.HasFlag(GroupPermissions.PreferDisableSounds); var isOwner = string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal);
group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); var chatEnabled = group.ChatEnabled;
group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); var chatChanged = false;
if (!isOwner)
{
permissions.SetDisableChat(!group.ChatEnabled);
}
else
{
var requestedChatEnabled = !permissions.IsDisableChat();
if (requestedChatEnabled != group.ChatEnabled)
{
var now = DateTime.UtcNow;
if (GroupChatToggleCooldowns.TryGetValue(group.GID, out var lockedUntil) && lockedUntil > now)
{
var remaining = lockedUntil - now;
var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes));
await Clients.Caller.Client_ReceiveServerMessage(
MessageSeverity.Warning,
$"Syncshell chat can be toggled again in {minutes} minute{(minutes == 1 ? string.Empty : "s")}."
).ConfigureAwait(false);
permissions.SetDisableChat(!group.ChatEnabled);
}
else
{
chatEnabled = requestedChatEnabled;
group.ChatEnabled = chatEnabled;
GroupChatToggleCooldowns[group.GID] = now.AddMinutes(5);
chatChanged = true;
}
}
}
group.InvitesEnabled = !permissions.HasFlag(GroupPermissions.DisableInvites);
group.PreferDisableSounds = permissions.HasFlag(GroupPermissions.PreferDisableSounds);
group.PreferDisableAnimations = permissions.HasFlag(GroupPermissions.PreferDisableAnimations);
group.PreferDisableVFX = permissions.HasFlag(GroupPermissions.PreferDisableVFX);
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList(); var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList();
await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, permissions)).ConfigureAwait(false);
if (isOwner && chatChanged && !chatEnabled)
{
var groupDisplayName = string.IsNullOrWhiteSpace(group.Alias) ? group.GID : group.Alias;
var descriptor = new ChatChannelDescriptor
{
Type = ChatChannelType.Group,
WorldId = 0,
ZoneId = 0,
CustomKey = group.GID
};
foreach (var uid in groupPairs)
{
TryInvokeChatService(() => _chatChannelService.RemovePresence(uid, descriptor), "removing group chat presence", descriptor, uid);
}
await Clients.Users(groupPairs)
.Client_ReceiveServerMessage(
MessageSeverity.Information,
$"Syncshell chat for '{groupDisplayName}' has been disabled.")
.ConfigureAwait(false);
}
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -235,6 +294,7 @@ public partial class LightlessHub
PreferDisableAnimations = defaultPermissions.DisableGroupAnimations, PreferDisableAnimations = defaultPermissions.DisableGroupAnimations,
PreferDisableSounds = defaultPermissions.DisableGroupSounds, PreferDisableSounds = defaultPermissions.DisableGroupSounds,
PreferDisableVFX = defaultPermissions.DisableGroupVFX, PreferDisableVFX = defaultPermissions.DisableGroupVFX,
ChatEnabled = true,
CreatedDate = currentTime, CreatedDate = currentTime,
}; };

View File

@@ -118,6 +118,7 @@ public static class Extensions
permissions.SetPreferDisableSounds(group.PreferDisableSounds); permissions.SetPreferDisableSounds(group.PreferDisableSounds);
permissions.SetPreferDisableVFX(group.PreferDisableVFX); permissions.SetPreferDisableVFX(group.PreferDisableVFX);
permissions.SetDisableInvites(!group.InvitesEnabled); permissions.SetDisableInvites(!group.InvitesEnabled);
permissions.SetDisableChat(!group.ChatEnabled);
return permissions; return permissions;
} }

View File

@@ -80,6 +80,9 @@ public class LightlessDbContext : DbContext
.WithOne(p => p.Group) .WithOne(p => p.Group)
.HasForeignKey<GroupProfile>(p => p.GroupGID) .HasForeignKey<GroupProfile>(p => p.GroupGID)
.IsRequired(false); .IsRequired(false);
mb.Entity<Group>()
.Property(g => g.ChatEnabled)
.HasDefaultValue(true);
mb.Entity<GroupPair>().ToTable("group_pairs"); mb.Entity<GroupPair>().ToTable("group_pairs");
mb.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID }); mb.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID });
mb.Entity<GroupPair>().HasIndex(c => c.GroupUserUID); mb.Entity<GroupPair>().HasIndex(c => c.GroupUserUID);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -438,6 +438,12 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("auto_prune_enabled"); .HasColumnName("auto_prune_enabled");
b.Property<bool>("ChatEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("chat_enabled");
b.Property<DateTime>("CreatedDate") b.Property<DateTime>("CreatedDate")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")

View File

@@ -19,5 +19,6 @@ public class Group
public bool PreferDisableSounds { get; set; } public bool PreferDisableSounds { get; set; }
public bool PreferDisableAnimations { get; set; } public bool PreferDisableAnimations { get; set; }
public bool PreferDisableVFX { get; set; } public bool PreferDisableVFX { get; set; }
public bool ChatEnabled { get; set; } = true;
public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
} }

View File

@@ -116,11 +116,12 @@ public sealed class CachedFileProvider : IDisposable
var tempFileName = destinationFilePath + ".dl"; var tempFileName = destinationFilePath + ".dl";
File.Copy(coldStorageFilePath.FullName, tempFileName, true); File.Copy(coldStorageFilePath.FullName, tempFileName, true);
File.Move(tempFileName, destinationFilePath, true); File.Move(tempFileName, destinationFilePath, true);
coldStorageFilePath.LastAccessTimeUtc = DateTime.UtcNow;
var destinationFile = new FileInfo(destinationFilePath); File.SetLastAccessTimeUtc(coldStorageFilePath.FullName, DateTime.UtcNow);
destinationFile.LastAccessTimeUtc = DateTime.UtcNow; File.SetLastAccessTimeUtc(destinationFilePath, DateTime.UtcNow);
destinationFile.CreationTimeUtc = DateTime.UtcNow; File.SetCreationTimeUtc(destinationFilePath, DateTime.UtcNow);
destinationFile.LastWriteTimeUtc = DateTime.UtcNow; File.SetLastWriteTimeUtc(destinationFilePath, DateTime.UtcNow);
_metrics.IncGauge(MetricsAPI.GaugeFilesTotal); _metrics.IncGauge(MetricsAPI.GaugeFilesTotal);
_metrics.IncGauge(MetricsAPI.GaugeFilesTotalSize, new FileInfo(destinationFilePath).Length); _metrics.IncGauge(MetricsAPI.GaugeFilesTotalSize, new FileInfo(destinationFilePath).Length);
return true; return true;
@@ -173,7 +174,14 @@ public sealed class CachedFileProvider : IDisposable
var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash); var fi = FilePathUtil.GetFileInfoForHash(_hotStoragePath, hash);
if (fi == null) return null; if (fi == null) return null;
fi.LastAccessTimeUtc = DateTime.UtcNow; try
{
File.SetLastAccessTimeUtc(fi.FullName, DateTime.UtcNow);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update LastAccessTimeUtc for file {hash}", hash);
}
_fileStatisticsService.LogFile(hash, fi.Length); _fileStatisticsService.LogFile(hash, fi.Length);