diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Chat.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Chat.cs index 1e21a90..b0c6125 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Chat.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Chat.cs @@ -41,8 +41,10 @@ public partial class LightlessHub var groupInfos = await DbContext.Groups .AsNoTracking() - .Where(g => g.OwnerUID == userUid + .Where(g => g.ChatEnabled + && (g.OwnerUID == userUid || DbContext.GroupPairs.Any(p => p.GroupGID == g.GID && p.GroupUserUID == userUid)) + ) .ToListAsync() .ConfigureAwait(false); @@ -162,6 +164,13 @@ public partial class LightlessHub 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) || await DbContext.GroupPairs .AsNoTracking() @@ -207,6 +216,23 @@ public partial class LightlessHub 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)) { throw new HubException("Chat access has been revoked."); diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index d371e65..3647e5f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -1,6 +1,7 @@ using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSyncServer.Models; @@ -11,10 +12,8 @@ using LightlessSyncShared.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using System.Reflection; +using System.Collections.Concurrent; using System.Security.Cryptography; namespace LightlessSyncServer.Hubs; @@ -52,6 +51,8 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } + private static readonly ConcurrentDictionary GroupChatToggleCooldowns = new(StringComparer.Ordinal); + [Authorize(Policy = "Identified")] public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto) { @@ -60,15 +61,73 @@ public partial class LightlessHub var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); if (!hasRights) return; - group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites); - group.PreferDisableSounds = dto.Permissions.HasFlag(GroupPermissions.PreferDisableSounds); - group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); - group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); + var permissions = dto.Permissions; + var isOwner = string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal); + var chatEnabled = group.ChatEnabled; + 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); 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")] @@ -235,6 +294,7 @@ public partial class LightlessHub PreferDisableAnimations = defaultPermissions.DisableGroupAnimations, PreferDisableSounds = defaultPermissions.DisableGroupSounds, PreferDisableVFX = defaultPermissions.DisableGroupVFX, + ChatEnabled = true, CreatedDate = currentTime, }; diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 8a39f4e..546cf0c 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -118,6 +118,7 @@ public static class Extensions permissions.SetPreferDisableSounds(group.PreferDisableSounds); permissions.SetPreferDisableVFX(group.PreferDisableVFX); permissions.SetDisableInvites(!group.InvitesEnabled); + permissions.SetDisableChat(!group.ChatEnabled); return permissions; } diff --git a/LightlessSyncServer/LightlessSyncShared/Data/LightlessDbContext.cs b/LightlessSyncServer/LightlessSyncShared/Data/LightlessDbContext.cs index 0b22ef4..77884ca 100644 --- a/LightlessSyncServer/LightlessSyncShared/Data/LightlessDbContext.cs +++ b/LightlessSyncServer/LightlessSyncShared/Data/LightlessDbContext.cs @@ -80,6 +80,9 @@ public class LightlessDbContext : DbContext .WithOne(p => p.Group) .HasForeignKey(p => p.GroupGID) .IsRequired(false); + mb.Entity() + .Property(g => g.ChatEnabled) + .HasDefaultValue(true); mb.Entity().ToTable("group_pairs"); mb.Entity().HasKey(u => new { u.GroupGID, u.GroupUserUID }); mb.Entity().HasIndex(c => c.GroupUserUID); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index 853bc37..f4bf651 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -468,6 +468,11 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("prefer_disable_vfx"); + b.Property("ChatEnabled") + .HasColumnType("boolean") + .HasColumnName("chat_enabled") + .HasDefaultValue(true); + b.HasKey("GID") .HasName("pk_groups"); diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs index b712406..733ba3c 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -19,5 +19,6 @@ public class Group public bool PreferDisableSounds { get; set; } public bool PreferDisableAnimations { get; set; } public bool PreferDisableVFX { get; set; } + public bool ChatEnabled { get; set; } = true; public DateTime CreatedDate { get; set; } = DateTime.UtcNow; }