prevent report dodging by leaving channel, allow reports to work after leaving the channel too
This commit is contained in:
@@ -334,12 +334,6 @@ public partial class LightlessHub
|
||||
{
|
||||
var channel = request.Channel.WithNormalizedCustomKey();
|
||||
|
||||
if (!_chatChannelService.TryGetPresence(UserUID, channel, out _))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Join the chat channel before reporting messages.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_chatChannelService.TryGetMessage(request.MessageId, out var messageEntry))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Unable to locate the reported message. It may have already expired.").ConfigureAwait(false);
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSyncServer.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSyncServer.Services;
|
||||
|
||||
public sealed class ChatChannelService
|
||||
public sealed class ChatChannelService : IDisposable
|
||||
{
|
||||
private readonly ILogger<ChatChannelService> _logger;
|
||||
private readonly Dictionary<string, ZoneChannelDefinition> _zoneDefinitions;
|
||||
private readonly Dictionary<ChannelKey, HashSet<string>> _membersByChannel = new();
|
||||
private readonly Dictionary<string, Dictionary<ChannelKey, ChatPresenceEntry>> _presenceByUser = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<ChannelKey, Dictionary<string, ChatParticipantInfo>> _participantsByChannel = new();
|
||||
private readonly Dictionary<ChannelKey, Dictionary<string, InactiveParticipantEntry>> _inactiveParticipantsByChannel = new();
|
||||
private readonly Dictionary<ChannelKey, LinkedList<ChatMessageLogEntry>> _messagesByChannel = new();
|
||||
private readonly Dictionary<string, (ChannelKey Channel, LinkedListNode<ChatMessageLogEntry> Node)> _messageIndex = new(StringComparer.Ordinal);
|
||||
private readonly object _syncRoot = new();
|
||||
private const int MaxMessagesPerChannel = 200;
|
||||
private static readonly TimeSpan InactiveParticipantRetention = TimeSpan.FromMinutes(15);
|
||||
private static readonly TimeSpan InactiveParticipantCleanupInterval = TimeSpan.FromMinutes(1);
|
||||
private readonly Timer _inactiveParticipantCleanupTimer;
|
||||
|
||||
public ChatChannelService(ILogger<ChatChannelService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_zoneDefinitions = ChatZoneDefinitions.Defaults
|
||||
.ToDictionary(definition => definition.Key, StringComparer.OrdinalIgnoreCase);
|
||||
_inactiveParticipantCleanupTimer = new Timer(_ => CleanupExpiredInactiveParticipants(), null, InactiveParticipantCleanupInterval, InactiveParticipantCleanupInterval);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ZoneChatChannelInfoDto> GetZoneChannelInfos() =>
|
||||
@@ -271,6 +272,22 @@ public sealed class ChatChannelService
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_inactiveParticipantsByChannel.TryGetValue(key, out var inactive) &&
|
||||
inactive.TryGetValue(token, out var inactiveEntry))
|
||||
{
|
||||
if (inactiveEntry.ExpiresAt > DateTime.UtcNow)
|
||||
{
|
||||
participant = inactiveEntry.Participant;
|
||||
return true;
|
||||
}
|
||||
|
||||
inactive.Remove(token);
|
||||
if (inactive.Count == 0)
|
||||
{
|
||||
_inactiveParticipantsByChannel.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
participant = default;
|
||||
@@ -412,6 +429,7 @@ public sealed class ChatChannelService
|
||||
}
|
||||
|
||||
participantsByToken[token] = finalParticipant;
|
||||
RemoveInactiveParticipantLocked(key, token);
|
||||
|
||||
_logger.LogDebug("Chat presence updated for {User} in {Channel}", userUid, Describe(key));
|
||||
return entryToStore;
|
||||
@@ -446,9 +464,77 @@ public sealed class ChatChannelService
|
||||
}
|
||||
}
|
||||
|
||||
StoreInactiveParticipantLocked(key, existing.Participant);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void StoreInactiveParticipantLocked(ChannelKey key, ChatParticipantInfo participant)
|
||||
{
|
||||
if (string.IsNullOrEmpty(participant.Token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_inactiveParticipantsByChannel.TryGetValue(key, out var inactive))
|
||||
{
|
||||
inactive = new Dictionary<string, InactiveParticipantEntry>(StringComparer.Ordinal);
|
||||
_inactiveParticipantsByChannel[key] = inactive;
|
||||
}
|
||||
|
||||
inactive[participant.Token] = new InactiveParticipantEntry(participant, DateTime.UtcNow + InactiveParticipantRetention);
|
||||
}
|
||||
|
||||
private void RemoveInactiveParticipantLocked(ChannelKey key, string token)
|
||||
{
|
||||
if (_inactiveParticipantsByChannel.TryGetValue(key, out var inactive) &&
|
||||
inactive.Remove(token) &&
|
||||
inactive.Count == 0)
|
||||
{
|
||||
_inactiveParticipantsByChannel.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupExpiredInactiveParticipants()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_inactiveParticipantsByChannel.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var channelsToRemove = new List<ChannelKey>();
|
||||
|
||||
foreach (var (key, inactive) in _inactiveParticipantsByChannel)
|
||||
{
|
||||
var tokensToRemove = new List<string>();
|
||||
foreach (var (token, entry) in inactive)
|
||||
{
|
||||
if (entry.ExpiresAt <= now)
|
||||
{
|
||||
tokensToRemove.Add(token);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var token in tokensToRemove)
|
||||
{
|
||||
inactive.Remove(token);
|
||||
}
|
||||
|
||||
if (inactive.Count == 0)
|
||||
{
|
||||
channelsToRemove.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var channel in channelsToRemove)
|
||||
{
|
||||
_inactiveParticipantsByChannel.Remove(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateToken()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
@@ -458,4 +544,11 @@ public sealed class ChatChannelService
|
||||
|
||||
private static string Describe(ChannelKey key)
|
||||
=> $"{key.Type}:{key.WorldId}:{key.CustomKey}";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_inactiveParticipantCleanupTimer.Dispose();
|
||||
}
|
||||
|
||||
private readonly record struct InactiveParticipantEntry(ChatParticipantInfo Participant, DateTime ExpiresAt);
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ internal class DiscordBot : IHostedService
|
||||
var embed = await BuildChatReportEmbedAsync(dbContext, report, token).ConfigureAwait(false);
|
||||
|
||||
var components = new ComponentBuilder()
|
||||
.WithButton("Actioned", $"{ChatReportButtonPrefix}-resolve-{report.ReportId}", ButtonStyle.Danger)
|
||||
.WithButton("Resolve", $"{ChatReportButtonPrefix}-resolve-{report.ReportId}", ButtonStyle.Danger)
|
||||
.WithButton("Dismiss", $"{ChatReportButtonPrefix}-dismiss-{report.ReportId}", ButtonStyle.Secondary)
|
||||
.WithButton("Ban From Chat", $"{ChatReportButtonPrefix}-banchat-{report.ReportId}", ButtonStyle.Danger);
|
||||
|
||||
@@ -395,7 +395,7 @@ internal class DiscordBot : IHostedService
|
||||
switch (action)
|
||||
{
|
||||
case "resolve":
|
||||
resolutionLabel = "Actioned";
|
||||
resolutionLabel = "Resolved";
|
||||
break;
|
||||
case "dismiss":
|
||||
resolutionLabel = "Dismissed";
|
||||
@@ -431,7 +431,7 @@ internal class DiscordBot : IHostedService
|
||||
|
||||
string responseText = action switch
|
||||
{
|
||||
"resolve" => "actioned",
|
||||
"resolve" => "resolved",
|
||||
"dismiss" => "dismissed",
|
||||
"banchat" => "chat access revoked",
|
||||
_ => "processed"
|
||||
@@ -473,7 +473,7 @@ internal class DiscordBot : IHostedService
|
||||
embedBuilder.Fields.RemoveAll(f => string.Equals(f.Name, "Resolution", StringComparison.OrdinalIgnoreCase));
|
||||
var resolutionText = action switch
|
||||
{
|
||||
"resolve" => "Actioned",
|
||||
"resolve" => "Resolved",
|
||||
"dismiss" => "Dismissed",
|
||||
"banchat" => "Chat access revoked",
|
||||
_ => "Processed"
|
||||
|
||||
Reference in New Issue
Block a user