768 lines
24 KiB
C#
768 lines
24 KiB
C#
using System.Security.Cryptography;
|
|
using LightlessSync.API.Data;
|
|
using LightlessSync.API.Dto.Chat;
|
|
using LightlessSyncServer.Configuration;
|
|
using LightlessSyncServer.Models;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace LightlessSyncServer.Services;
|
|
|
|
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, LinkedList<ChatMessageLogEntry>> _messagesByChannel = new();
|
|
private readonly Dictionary<string, (ChannelKey Channel, LinkedListNode<ChatMessageLogEntry> Node)> _messageIndex = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, Dictionary<ChannelKey, HashSet<string>>> _mutedTokensByUser = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, Dictionary<ChannelKey, HashSet<string>>> _mutedUidsByUser = new(StringComparer.Ordinal);
|
|
private readonly object _syncRoot = new();
|
|
private const int MaxMessagesPerChannel = 200;
|
|
internal const int MaxMutedParticipantsPerChannel = 8;
|
|
|
|
public ChatChannelService(ILogger<ChatChannelService> logger, IOptions<ChatZoneOverridesOptions>? zoneOverrides = null)
|
|
{
|
|
_logger = logger;
|
|
_zoneDefinitions = BuildZoneDefinitions(zoneOverrides?.Value);
|
|
}
|
|
|
|
private Dictionary<string, ZoneChannelDefinition> BuildZoneDefinitions(ChatZoneOverridesOptions? overrides)
|
|
{
|
|
var definitions = ChatZoneDefinitions.Defaults
|
|
.ToDictionary(definition => definition.Key, StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (overrides?.Zones is null || overrides.Zones.Count == 0)
|
|
{
|
|
return definitions;
|
|
}
|
|
|
|
foreach (var entry in overrides.Zones)
|
|
{
|
|
if (entry is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!TryCreateZoneDefinition(entry, out var definition))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
definitions[definition.Key] = definition;
|
|
}
|
|
|
|
return definitions;
|
|
}
|
|
|
|
private bool TryCreateZoneDefinition(ChatZoneOverride entry, out ZoneChannelDefinition definition)
|
|
{
|
|
definition = default;
|
|
|
|
var key = NormalizeZoneKey(entry.Key);
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
_logger.LogWarning("Skipped chat zone override with missing key.");
|
|
return false;
|
|
}
|
|
|
|
var territoryIds = new HashSet<ushort>();
|
|
if (entry.TerritoryIds is not null)
|
|
{
|
|
foreach (var candidate in entry.TerritoryIds)
|
|
{
|
|
if (candidate > 0)
|
|
{
|
|
territoryIds.Add(candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
var territoryNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
if (entry.TerritoryNames is not null)
|
|
{
|
|
foreach (var name in entry.TerritoryNames)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
continue;
|
|
|
|
var trimmed = name.Trim();
|
|
territoryNames.Add(trimmed);
|
|
if (TerritoryRegistry.TryGetIds(trimmed, out var ids))
|
|
{
|
|
territoryIds.UnionWith(ids);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Chat zone override {Zone} references unknown territory '{Territory}'.", key, trimmed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (territoryIds.Count == 0)
|
|
{
|
|
_logger.LogWarning("Skipped chat zone override for {Zone}: no territory IDs resolved.", key);
|
|
return false;
|
|
}
|
|
|
|
if (territoryNames.Count == 0)
|
|
{
|
|
foreach (var territoryId in territoryIds)
|
|
{
|
|
if (TerritoryRegistry.ById.TryGetValue(territoryId, out var territory))
|
|
{
|
|
territoryNames.Add(territory.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (territoryNames.Count == 0)
|
|
{
|
|
territoryNames.Add("Territory");
|
|
}
|
|
|
|
var descriptor = new ChatChannelDescriptor
|
|
{
|
|
Type = ChatChannelType.Zone,
|
|
WorldId = 0,
|
|
ZoneId = 0,
|
|
CustomKey = key
|
|
};
|
|
|
|
var displayName = string.IsNullOrWhiteSpace(entry.DisplayName)
|
|
? key
|
|
: entry.DisplayName.Trim();
|
|
|
|
definition = new ZoneChannelDefinition(
|
|
key,
|
|
displayName,
|
|
descriptor,
|
|
territoryNames.ToArray(),
|
|
territoryIds);
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string NormalizeZoneKey(string? value) =>
|
|
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
|
|
|
|
public IReadOnlyList<ZoneChatChannelInfoDto> GetZoneChannelInfos() =>
|
|
_zoneDefinitions.Values
|
|
.Select(definition => new ZoneChatChannelInfoDto(
|
|
definition.Descriptor,
|
|
definition.DisplayName,
|
|
definition.TerritoryNames))
|
|
.ToArray();
|
|
|
|
public bool TryResolveZone(string? key, out ZoneChannelDefinition definition)
|
|
{
|
|
definition = default;
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return false;
|
|
|
|
return _zoneDefinitions.TryGetValue(key, out definition);
|
|
}
|
|
|
|
public ChatPresenceEntry? UpdateZonePresence(
|
|
string userUid,
|
|
ZoneChannelDefinition definition,
|
|
ushort worldId,
|
|
ushort territoryId,
|
|
string? hashedCid,
|
|
bool isLightfinder,
|
|
bool isActive)
|
|
{
|
|
if (worldId == 0 || !WorldRegistry.IsKnownWorld(worldId))
|
|
{
|
|
_logger.LogWarning("Rejected zone chat presence for {User} in {Zone}: unknown world {WorldId}", userUid, definition.Key, worldId);
|
|
return null;
|
|
}
|
|
|
|
if (!definition.TerritoryIds.Contains(territoryId))
|
|
{
|
|
_logger.LogWarning("Rejected zone chat presence for {User} in {Zone}: invalid territory {TerritoryId}", userUid, definition.Key, territoryId);
|
|
return null;
|
|
}
|
|
|
|
var descriptor = definition.Descriptor with { WorldId = worldId, ZoneId = territoryId };
|
|
var participant = new ChatParticipantInfo(
|
|
Token: string.Empty,
|
|
UserUid: userUid,
|
|
User: null,
|
|
HashedCid: isLightfinder ? hashedCid : null,
|
|
IsLightfinder: isLightfinder);
|
|
|
|
return UpdatePresence(
|
|
userUid,
|
|
descriptor,
|
|
definition.DisplayName,
|
|
participant,
|
|
isActive,
|
|
replaceExistingOfSameType: true);
|
|
}
|
|
|
|
public ChatPresenceEntry? UpdateGroupPresence(
|
|
string userUid,
|
|
string groupId,
|
|
string displayName,
|
|
UserData user,
|
|
string? hashedCid,
|
|
bool isActive)
|
|
{
|
|
var descriptor = new ChatChannelDescriptor
|
|
{
|
|
Type = ChatChannelType.Group,
|
|
WorldId = 0,
|
|
ZoneId = 0,
|
|
CustomKey = groupId
|
|
};
|
|
|
|
var participant = new ChatParticipantInfo(
|
|
Token: string.Empty,
|
|
UserUid: userUid,
|
|
User: user,
|
|
HashedCid: hashedCid,
|
|
IsLightfinder: !string.IsNullOrEmpty(hashedCid));
|
|
|
|
return UpdatePresence(
|
|
userUid,
|
|
descriptor,
|
|
displayName,
|
|
participant,
|
|
isActive,
|
|
replaceExistingOfSameType: false);
|
|
}
|
|
|
|
public bool TryGetPresence(string userUid, ChatChannelDescriptor channel, out ChatPresenceEntry presence)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(channel);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (_presenceByUser.TryGetValue(userUid, out var entries) && entries.TryGetValue(key, out presence))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
presence = default;
|
|
return false;
|
|
}
|
|
|
|
public IReadOnlyCollection<string> GetMembers(ChatChannelDescriptor channel)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(channel);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (_membersByChannel.TryGetValue(key, out var members))
|
|
{
|
|
return members.ToArray();
|
|
}
|
|
}
|
|
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
public string RecordMessage(ChatChannelDescriptor channel, ChatParticipantInfo participant, string message, DateTime sentAtUtc)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(channel);
|
|
var messageId = Guid.NewGuid().ToString("N");
|
|
var entry = new ChatMessageLogEntry(
|
|
messageId,
|
|
channel,
|
|
sentAtUtc,
|
|
participant.UserUid,
|
|
participant.User,
|
|
participant.IsLightfinder,
|
|
participant.HashedCid,
|
|
message);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_messagesByChannel.TryGetValue(key, out var list))
|
|
{
|
|
list = new LinkedList<ChatMessageLogEntry>();
|
|
_messagesByChannel[key] = list;
|
|
}
|
|
|
|
var node = list.AddLast(entry);
|
|
_messageIndex[messageId] = (key, node);
|
|
|
|
while (list.Count > MaxMessagesPerChannel)
|
|
{
|
|
var removedNode = list.First;
|
|
if (removedNode is null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
list.RemoveFirst();
|
|
_messageIndex.Remove(removedNode.Value.MessageId);
|
|
}
|
|
}
|
|
|
|
return messageId;
|
|
}
|
|
|
|
public bool TryGetMessage(string messageId, out ChatMessageLogEntry entry)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
if (_messageIndex.TryGetValue(messageId, out var located))
|
|
{
|
|
entry = located.Node.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
entry = default;
|
|
return false;
|
|
}
|
|
|
|
public IReadOnlyList<ChatMessageLogEntry> GetRecentMessages(ChatChannelDescriptor descriptor, int maxCount)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(descriptor);
|
|
if (!_messagesByChannel.TryGetValue(key, out var list) || list.Count == 0)
|
|
{
|
|
return Array.Empty<ChatMessageLogEntry>();
|
|
}
|
|
|
|
var take = Math.Min(maxCount, list.Count);
|
|
var result = new ChatMessageLogEntry[take];
|
|
var node = list.Last;
|
|
for (var i = take - 1; i >= 0 && node is not null; i--)
|
|
{
|
|
result[i] = node.Value;
|
|
node = node.Previous;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public bool RemovePresence(string userUid, ChatChannelDescriptor? channel = null)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(userUid);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_presenceByUser.TryGetValue(userUid, out var entries))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (channel is null)
|
|
{
|
|
foreach (var existing in entries.Keys.ToList())
|
|
{
|
|
RemovePresenceInternal(userUid, entries, existing);
|
|
}
|
|
|
|
_presenceByUser.Remove(userUid);
|
|
return true;
|
|
}
|
|
|
|
var key = ChannelKey.FromDescriptor(channel.Value);
|
|
var removed = RemovePresenceInternal(userUid, entries, key);
|
|
|
|
if (entries.Count == 0)
|
|
{
|
|
_presenceByUser.Remove(userUid);
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
}
|
|
|
|
public void RefreshLightfinderState(string userUid, string? hashedCid, bool isLightfinder)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(userUid);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_presenceByUser.TryGetValue(userUid, out var entries) || entries.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var (key, existing) in entries.ToArray())
|
|
{
|
|
var updatedParticipant = existing.Participant with
|
|
{
|
|
HashedCid = isLightfinder ? hashedCid : null,
|
|
IsLightfinder = isLightfinder
|
|
};
|
|
|
|
var updatedEntry = existing with
|
|
{
|
|
Participant = updatedParticipant,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
entries[key] = updatedEntry;
|
|
|
|
if (_participantsByChannel.TryGetValue(key, out var participants))
|
|
{
|
|
participants[updatedParticipant.Token] = updatedParticipant;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private ChatPresenceEntry? UpdatePresence(
|
|
string userUid,
|
|
ChatChannelDescriptor descriptor,
|
|
string displayName,
|
|
ChatParticipantInfo participant,
|
|
bool isActive,
|
|
bool replaceExistingOfSameType)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(userUid);
|
|
|
|
var normalizedDescriptor = descriptor.WithNormalizedCustomKey();
|
|
var key = ChannelKey.FromDescriptor(normalizedDescriptor);
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_presenceByUser.TryGetValue(userUid, out var entries))
|
|
{
|
|
if (!isActive)
|
|
return null;
|
|
|
|
entries = new Dictionary<ChannelKey, ChatPresenceEntry>();
|
|
_presenceByUser[userUid] = entries;
|
|
}
|
|
|
|
string? reusableToken = null;
|
|
|
|
if (entries.TryGetValue(key, out var existing))
|
|
{
|
|
reusableToken = existing.Participant.Token;
|
|
RemovePresenceInternal(userUid, entries, key);
|
|
}
|
|
|
|
if (replaceExistingOfSameType)
|
|
{
|
|
foreach (var candidate in entries.Keys.Where(k => k.Type == key.Type).ToList())
|
|
{
|
|
if (entries.TryGetValue(candidate, out var entry))
|
|
{
|
|
reusableToken ??= entry.Participant.Token;
|
|
}
|
|
|
|
RemovePresenceInternal(userUid, entries, candidate);
|
|
}
|
|
|
|
if (!isActive)
|
|
{
|
|
if (entries.Count == 0)
|
|
{
|
|
_presenceByUser.Remove(userUid);
|
|
}
|
|
|
|
_logger.LogDebug("Chat presence cleared for {User} ({Type})", userUid, normalizedDescriptor.Type);
|
|
return null;
|
|
}
|
|
}
|
|
else if (!isActive)
|
|
{
|
|
var removed = RemovePresenceInternal(userUid, entries, key);
|
|
if (removed)
|
|
{
|
|
_logger.LogDebug("Chat presence removed for {User} from {Channel}", userUid, Describe(key));
|
|
}
|
|
|
|
if (entries.Count == 0)
|
|
{
|
|
_presenceByUser.Remove(userUid);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var token = !string.IsNullOrEmpty(participant.Token)
|
|
? participant.Token
|
|
: reusableToken ?? GenerateToken();
|
|
|
|
var finalParticipant = participant with { Token = token };
|
|
var entryToStore = new ChatPresenceEntry(
|
|
normalizedDescriptor,
|
|
key,
|
|
displayName,
|
|
finalParticipant,
|
|
DateTime.UtcNow);
|
|
|
|
entries[key] = entryToStore;
|
|
|
|
if (!_membersByChannel.TryGetValue(key, out var members))
|
|
{
|
|
members = new HashSet<string>(StringComparer.Ordinal);
|
|
_membersByChannel[key] = members;
|
|
}
|
|
|
|
members.Add(userUid);
|
|
|
|
if (!_participantsByChannel.TryGetValue(key, out var participantsByToken))
|
|
{
|
|
participantsByToken = new Dictionary<string, ChatParticipantInfo>(StringComparer.Ordinal);
|
|
_participantsByChannel[key] = participantsByToken;
|
|
}
|
|
|
|
participantsByToken[token] = finalParticipant;
|
|
|
|
ApplyUIDMuteIfPresent(normalizedDescriptor, finalParticipant);
|
|
|
|
_logger.LogDebug("Chat presence updated for {User} in {Channel}", userUid, Describe(key));
|
|
return entryToStore;
|
|
}
|
|
}
|
|
|
|
private bool RemovePresenceInternal(string userUid, Dictionary<ChannelKey, ChatPresenceEntry> entries, ChannelKey key)
|
|
{
|
|
if (!entries.TryGetValue(key, out var existing))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
entries.Remove(key);
|
|
|
|
if (_membersByChannel.TryGetValue(key, out var members))
|
|
{
|
|
members.Remove(userUid);
|
|
if (members.Count == 0)
|
|
{
|
|
_membersByChannel.Remove(key);
|
|
// Preserve message history even when a channel becomes empty so moderation can still resolve reports.
|
|
}
|
|
}
|
|
|
|
if (_participantsByChannel.TryGetValue(key, out var participants))
|
|
{
|
|
participants.Remove(existing.Participant.Token);
|
|
if (participants.Count == 0)
|
|
{
|
|
_participantsByChannel.Remove(key);
|
|
}
|
|
}
|
|
|
|
ClearMutesForChannel(userUid, key);
|
|
return true;
|
|
}
|
|
|
|
internal bool TryGetActiveParticipant(ChatChannelDescriptor channel, string token, out ChatParticipantInfo participant)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(channel.WithNormalizedCustomKey());
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (_participantsByChannel.TryGetValue(key, out var participants) &&
|
|
participants.TryGetValue(token, out participant))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
participant = default;
|
|
return false;
|
|
}
|
|
|
|
internal bool IsTokenMuted(string userUid, ChatChannelDescriptor channel, string token)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(channel.WithNormalizedCustomKey());
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_mutedTokensByUser.TryGetValue(userUid, out var channels) ||
|
|
!channels.TryGetValue(key, out var tokens))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (tokens.Contains(token))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (_participantsByChannel.TryGetValue(key, out var participants) &&
|
|
participants.TryGetValue(token, out var participant))
|
|
{
|
|
return IsUIDMutedLocked(userUid, key, participant.UserUid);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public ChatMuteUpdateResult SetMutedParticipant(string userUid, ChatChannelDescriptor channel, ChatParticipantInfo participant, bool mute)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(userUid);
|
|
ArgumentException.ThrowIfNullOrEmpty(participant.Token);
|
|
ArgumentException.ThrowIfNullOrEmpty(participant.UserUid);
|
|
|
|
var key = ChannelKey.FromDescriptor(channel.WithNormalizedCustomKey());
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_mutedTokensByUser.TryGetValue(userUid, out var channels))
|
|
{
|
|
if (!mute)
|
|
{
|
|
return ChatMuteUpdateResult.NoChange;
|
|
}
|
|
|
|
channels = new Dictionary<ChannelKey, HashSet<string>>();
|
|
_mutedTokensByUser[userUid] = channels;
|
|
}
|
|
|
|
if (!channels.TryGetValue(key, out var tokens))
|
|
{
|
|
if (!mute)
|
|
{
|
|
return ChatMuteUpdateResult.NoChange;
|
|
}
|
|
|
|
tokens = new HashSet<string>(StringComparer.Ordinal);
|
|
channels[key] = tokens;
|
|
}
|
|
|
|
if (mute)
|
|
{
|
|
if (!tokens.Contains(participant.Token) && tokens.Count >= MaxMutedParticipantsPerChannel)
|
|
{
|
|
return ChatMuteUpdateResult.ChannelLimitReached;
|
|
}
|
|
|
|
var added = tokens.Add(participant.Token);
|
|
EnsureUIDMuteLocked(userUid, key, participant.UserUid);
|
|
return added ? ChatMuteUpdateResult.Changed : ChatMuteUpdateResult.NoChange;
|
|
}
|
|
|
|
var removed = tokens.Remove(participant.Token);
|
|
if (tokens.Count == 0)
|
|
{
|
|
channels.Remove(key);
|
|
if (channels.Count == 0)
|
|
{
|
|
_mutedTokensByUser.Remove(userUid);
|
|
}
|
|
}
|
|
|
|
RemoveUIDMuteLocked(userUid, key, participant.UserUid);
|
|
return removed ? ChatMuteUpdateResult.Changed : ChatMuteUpdateResult.NoChange;
|
|
}
|
|
}
|
|
|
|
private static string GenerateToken()
|
|
{
|
|
Span<byte> buffer = stackalloc byte[8];
|
|
RandomNumberGenerator.Fill(buffer);
|
|
return Convert.ToHexString(buffer);
|
|
}
|
|
|
|
private static string Describe(ChannelKey key)
|
|
=> $"{key.Type}:{key.WorldId}:{key.CustomKey}";
|
|
|
|
private void ClearMutesForChannel(string userUid, ChannelKey key)
|
|
{
|
|
if (_mutedTokensByUser.TryGetValue(userUid, out var tokenChannels) &&
|
|
tokenChannels.Remove(key) &&
|
|
tokenChannels.Count == 0)
|
|
{
|
|
_mutedTokensByUser.Remove(userUid);
|
|
}
|
|
|
|
if (_mutedUidsByUser.TryGetValue(userUid, out var uidChannels) &&
|
|
uidChannels.Remove(key) &&
|
|
uidChannels.Count == 0)
|
|
{
|
|
_mutedUidsByUser.Remove(userUid);
|
|
}
|
|
}
|
|
|
|
private void ApplyUIDMuteIfPresent(ChatChannelDescriptor descriptor, ChatParticipantInfo participant)
|
|
{
|
|
var key = ChannelKey.FromDescriptor(descriptor);
|
|
foreach (var kvp in _mutedUidsByUser)
|
|
{
|
|
var muter = kvp.Key;
|
|
var channels = kvp.Value;
|
|
if (!channels.TryGetValue(key, out var mutedUids) || !mutedUids.Contains(participant.UserUid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!_mutedTokensByUser.TryGetValue(muter, out var tokenChannels))
|
|
{
|
|
tokenChannels = new Dictionary<ChannelKey, HashSet<string>>();
|
|
_mutedTokensByUser[muter] = tokenChannels;
|
|
}
|
|
|
|
if (!tokenChannels.TryGetValue(key, out var tokens))
|
|
{
|
|
tokens = new HashSet<string>(StringComparer.Ordinal);
|
|
tokenChannels[key] = tokens;
|
|
}
|
|
|
|
tokens.Add(participant.Token);
|
|
}
|
|
}
|
|
|
|
private void EnsureUIDMuteLocked(string userUid, ChannelKey key, string targetUid)
|
|
{
|
|
if (!_mutedUidsByUser.TryGetValue(userUid, out var channels))
|
|
{
|
|
channels = new Dictionary<ChannelKey, HashSet<string>>();
|
|
_mutedUidsByUser[userUid] = channels;
|
|
}
|
|
|
|
if (!channels.TryGetValue(key, out var set))
|
|
{
|
|
set = new HashSet<string>(StringComparer.Ordinal);
|
|
channels[key] = set;
|
|
}
|
|
|
|
set.Add(targetUid);
|
|
}
|
|
|
|
private void RemoveUIDMuteLocked(string userUid, ChannelKey key, string targetUid)
|
|
{
|
|
if (!_mutedUidsByUser.TryGetValue(userUid, out var channels) ||
|
|
!channels.TryGetValue(key, out var set))
|
|
{
|
|
return;
|
|
}
|
|
|
|
set.Remove(targetUid);
|
|
if (set.Count == 0)
|
|
{
|
|
channels.Remove(key);
|
|
if (channels.Count == 0)
|
|
{
|
|
_mutedUidsByUser.Remove(userUid);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsUIDMutedLocked(string userUid, ChannelKey key, string targetUid)
|
|
{
|
|
return _mutedUidsByUser.TryGetValue(userUid, out var channels) &&
|
|
channels.TryGetValue(key, out var set) &&
|
|
set.Contains(targetUid);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
public enum ChatMuteUpdateResult
|
|
{
|
|
NoChange,
|
|
Changed,
|
|
ChannelLimitReached
|
|
}
|