631 lines
23 KiB
C#
631 lines
23 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using LightlessSync.API.Data.Enum;
|
|
using LightlessSync.API.Dto.Chat;
|
|
using LightlessSync.API.Dto.User;
|
|
using LightlessSyncServer.Models;
|
|
using LightlessSyncServer.Services;
|
|
using LightlessSyncServer.Utils;
|
|
using LightlessSyncShared.Models;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace LightlessSyncServer.Hubs;
|
|
|
|
public partial class LightlessHub
|
|
{
|
|
private const int MaxChatMessageLength = 400;
|
|
private const int ChatRateLimitMessages = 7;
|
|
private static readonly TimeSpan ChatRateLimitWindow = TimeSpan.FromMinutes(1);
|
|
private static readonly ConcurrentDictionary<string, ChatRateLimitState> ChatRateLimiters = new(StringComparer.Ordinal);
|
|
|
|
private sealed class ChatRateLimitState
|
|
{
|
|
public readonly Queue<DateTime> Events = new();
|
|
public readonly object SyncRoot = new();
|
|
}
|
|
private static readonly JsonSerializerOptions ChatReportSnapshotSerializerOptions = new(JsonSerializerDefaults.General)
|
|
{
|
|
WriteIndented = false
|
|
};
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public Task<IReadOnlyList<ZoneChatChannelInfoDto>> GetZoneChatChannels()
|
|
{
|
|
return Task.FromResult(_chatChannelService.GetZoneChannelInfos());
|
|
}
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public async Task<IReadOnlyList<GroupChatChannelInfoDto>> GetGroupChatChannels()
|
|
{
|
|
var userUid = UserUID;
|
|
|
|
var groupInfos = await DbContext.Groups
|
|
.AsNoTracking()
|
|
.Where(g => g.OwnerUID == userUid
|
|
|| DbContext.GroupPairs.Any(p => p.GroupGID == g.GID && p.GroupUserUID == userUid))
|
|
.ToListAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
return groupInfos
|
|
.Select(g =>
|
|
{
|
|
var displayName = string.IsNullOrWhiteSpace(g.Alias) ? g.GID : g.Alias!;
|
|
var descriptor = new ChatChannelDescriptor
|
|
{
|
|
Type = ChatChannelType.Group,
|
|
WorldId = 0,
|
|
ZoneId = 0,
|
|
CustomKey = g.GID
|
|
};
|
|
|
|
return new GroupChatChannelInfoDto(
|
|
descriptor,
|
|
displayName,
|
|
g.GID,
|
|
g.OwnerUID == userUid);
|
|
})
|
|
.OrderBy(info => info.DisplayName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public async Task UpdateChatPresence(ChatPresenceUpdateDto presence)
|
|
{
|
|
var channel = presence.Channel.WithNormalizedCustomKey();
|
|
|
|
var userRecord = await DbContext.Users
|
|
.AsNoTracking()
|
|
.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (userRecord.ChatBanned)
|
|
{
|
|
_chatChannelService.RemovePresence(UserUID);
|
|
await NotifyChatBanAsync(UserUID).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (!presence.IsActive)
|
|
{
|
|
_chatChannelService.RemovePresence(UserUID, channel);
|
|
return;
|
|
}
|
|
|
|
switch (channel.Type)
|
|
{
|
|
case ChatChannelType.Zone:
|
|
if (!_chatChannelService.TryResolveZone(channel.CustomKey, out var definition))
|
|
{
|
|
throw new HubException("Unsupported chat channel.");
|
|
}
|
|
|
|
if (channel.WorldId == 0 || !WorldRegistry.IsKnownWorld(channel.WorldId))
|
|
{
|
|
throw new HubException("Unsupported chat channel.");
|
|
}
|
|
|
|
if (presence.TerritoryId == 0 || !definition.TerritoryIds.Contains(presence.TerritoryId))
|
|
{
|
|
throw new HubException("Zone chat is only available in supported territories.");
|
|
}
|
|
|
|
string? hashedCid = null;
|
|
var isLightfinder = false;
|
|
if (IsValidHashedCid(UserCharaIdent))
|
|
{
|
|
var (entry, expiry) = await TryGetBroadcastEntryAsync(UserCharaIdent).ConfigureAwait(false);
|
|
isLightfinder = HasActiveBroadcast(entry, expiry);
|
|
if (isLightfinder)
|
|
{
|
|
hashedCid = UserCharaIdent;
|
|
}
|
|
}
|
|
|
|
_chatChannelService.UpdateZonePresence(
|
|
UserUID,
|
|
definition,
|
|
channel.WorldId,
|
|
presence.TerritoryId,
|
|
hashedCid,
|
|
isLightfinder,
|
|
isActive: true);
|
|
break;
|
|
|
|
case ChatChannelType.Group:
|
|
var groupKey = channel.CustomKey ?? string.Empty;
|
|
if (string.IsNullOrEmpty(groupKey))
|
|
{
|
|
throw new HubException("Unsupported chat channel.");
|
|
}
|
|
|
|
var userData = userRecord.ToUserData();
|
|
|
|
var group = await DbContext.Groups
|
|
.AsNoTracking()
|
|
.SingleOrDefaultAsync(g => g.GID == groupKey, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (group is null)
|
|
{
|
|
throw new HubException("Unsupported chat channel.");
|
|
}
|
|
|
|
var isMember = string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal)
|
|
|| await DbContext.GroupPairs
|
|
.AsNoTracking()
|
|
.AnyAsync(gp => gp.GroupGID == groupKey && gp.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (!isMember)
|
|
{
|
|
throw new HubException("Join the syncshell before using chat.");
|
|
}
|
|
|
|
var displayName = string.IsNullOrWhiteSpace(group.Alias) ? group.GID : group.Alias;
|
|
|
|
_chatChannelService.UpdateGroupPresence(
|
|
UserUID,
|
|
group.GID,
|
|
displayName,
|
|
userData,
|
|
IsValidHashedCid(UserCharaIdent) ? UserCharaIdent : null,
|
|
isActive: true);
|
|
break;
|
|
|
|
default:
|
|
throw new HubException("Unsupported chat channel.");
|
|
}
|
|
}
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public async Task SendChatMessage(ChatSendRequestDto request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Message))
|
|
{
|
|
throw new HubException("Message cannot be empty.");
|
|
}
|
|
|
|
var channel = request.Channel.WithNormalizedCustomKey();
|
|
|
|
if (await HandleIfChatBannedAsync(UserUID).ConfigureAwait(false))
|
|
{
|
|
throw new HubException("Chat access has been revoked.");
|
|
}
|
|
|
|
if (!_chatChannelService.TryGetPresence(UserUID, channel, out var presence))
|
|
{
|
|
throw new HubException("Join a chat channel before sending messages.");
|
|
}
|
|
|
|
if (!UseChatRateLimit(UserUID))
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can send at most " + ChatRateLimitMessages + " chat messages per minute. Please wait before sending more.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var sanitizedMessage = request.Message.Trim().ReplaceLineEndings(" ");
|
|
if (sanitizedMessage.Length > MaxChatMessageLength)
|
|
{
|
|
sanitizedMessage = sanitizedMessage[..MaxChatMessageLength];
|
|
}
|
|
|
|
var recipients = _chatChannelService.GetMembers(presence.Channel);
|
|
var recipientsList = recipients.ToList();
|
|
if (recipientsList.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var bannedRecipients = recipientsList.Count == 0
|
|
? new List<string>()
|
|
: await DbContext.Users.AsNoTracking()
|
|
.Where(u => recipientsList.Contains(u.UID) && u.ChatBanned)
|
|
.Select(u => u.UID)
|
|
.ToListAsync(RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
HashSet<string>? bannedSet = null;
|
|
if (bannedRecipients.Count > 0)
|
|
{
|
|
bannedSet = new HashSet<string>(bannedRecipients, StringComparer.Ordinal);
|
|
foreach (var bannedUid in bannedSet)
|
|
{
|
|
_chatChannelService.RemovePresence(bannedUid);
|
|
await NotifyChatBanAsync(bannedUid).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
var deliveryTargets = new Dictionary<string, (string Uid, bool IncludeSensitive)>(StringComparer.Ordinal);
|
|
foreach (var uid in recipientsList)
|
|
{
|
|
if (bannedSet != null && bannedSet.Contains(uid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (_userConnections.TryGetValue(uid, out var connectionId))
|
|
{
|
|
var includeSensitive = await AllowsLightfinderDetailsAsync(presence.Channel, uid).ConfigureAwait(false);
|
|
if (deliveryTargets.TryGetValue(connectionId, out var existing))
|
|
{
|
|
deliveryTargets[connectionId] = (existing.Uid, existing.IncludeSensitive || includeSensitive);
|
|
}
|
|
else
|
|
{
|
|
deliveryTargets[connectionId] = (uid, includeSensitive);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_chatChannelService.RemovePresence(uid);
|
|
}
|
|
}
|
|
|
|
if (deliveryTargets.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var timestamp = DateTime.UtcNow;
|
|
var messageId = _chatChannelService.RecordMessage(presence.Channel, presence.Participant, sanitizedMessage, timestamp);
|
|
var sendTasks = new List<Task>(deliveryTargets.Count);
|
|
|
|
foreach (var (connectionId, target) in deliveryTargets)
|
|
{
|
|
var sender = BuildSenderDescriptor(presence.Channel, presence.Participant, target.IncludeSensitive);
|
|
var payload = new ChatMessageDto(
|
|
presence.Channel,
|
|
sender,
|
|
sanitizedMessage,
|
|
timestamp,
|
|
messageId);
|
|
|
|
sendTasks.Add(Clients.Client(connectionId).Client_ChatReceive(payload));
|
|
}
|
|
|
|
await Task.WhenAll(sendTasks).ConfigureAwait(false);
|
|
}
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public async Task<ChatParticipantResolveResultDto?> ResolveChatParticipant(ChatParticipantResolveRequestDto request)
|
|
{
|
|
var channel = request.Channel.WithNormalizedCustomKey();
|
|
|
|
if (!_chatChannelService.TryGetPresence(UserUID, channel, out _))
|
|
{
|
|
throw new HubException("Join the chat channel before resolving participants.");
|
|
}
|
|
|
|
if (!_chatChannelService.TryResolveParticipant(channel, request.Token, out var participant))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var viewerAllowsDetails = await ViewerAllowsLightfinderDetailsAsync(channel).ConfigureAwait(false);
|
|
var includeSensitiveInfo = channel.Type == ChatChannelType.Group || viewerAllowsDetails;
|
|
|
|
var sender = BuildSenderDescriptor(channel, participant, includeSensitiveInfo);
|
|
|
|
if (!includeSensitiveInfo)
|
|
{
|
|
return new ChatParticipantResolveResultDto(channel, sender, null);
|
|
}
|
|
|
|
UserProfileDto? profile = null;
|
|
if (channel.Type == ChatChannelType.Group)
|
|
{
|
|
profile = await LoadChatParticipantProfileAsync(participant.UserUid).ConfigureAwait(false);
|
|
}
|
|
else if (participant.IsLightfinder && !string.IsNullOrEmpty(participant.HashedCid))
|
|
{
|
|
profile = await LoadChatParticipantProfileAsync(participant.UserUid).ConfigureAwait(false);
|
|
}
|
|
|
|
return new ChatParticipantResolveResultDto(channel, sender, profile);
|
|
}
|
|
|
|
[Authorize(Policy = "Identified")]
|
|
public async Task ReportChatMessage(ChatReportSubmitDto request)
|
|
{
|
|
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);
|
|
return;
|
|
}
|
|
|
|
var requestedChannelKey = ChannelKey.FromDescriptor(channel);
|
|
var messageChannelKey = ChannelKey.FromDescriptor(messageEntry.Channel.WithNormalizedCustomKey());
|
|
if (!requestedChannelKey.Equals(messageChannelKey))
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The reported message no longer matches this channel.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (string.Equals(messageEntry.SenderUserUid, UserUID, StringComparison.Ordinal))
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You cannot report your own message.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var reason = request.Reason?.Trim();
|
|
if (string.IsNullOrWhiteSpace(reason))
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Please provide a short explanation for the report.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
const int MaxReasonLength = 500;
|
|
if (reason.Length > MaxReasonLength)
|
|
{
|
|
reason = reason[..MaxReasonLength];
|
|
}
|
|
|
|
var additionalContext = string.IsNullOrWhiteSpace(request.AdditionalContext)
|
|
? null
|
|
: request.AdditionalContext.Trim();
|
|
|
|
const int MaxContextLength = 1000;
|
|
if (!string.IsNullOrEmpty(additionalContext) && additionalContext.Length > MaxContextLength)
|
|
{
|
|
additionalContext = additionalContext[..MaxContextLength];
|
|
}
|
|
|
|
var alreadyReported = await DbContext.ReportedChatMessages
|
|
.AsNoTracking()
|
|
.AnyAsync(r => r.MessageId == request.MessageId && r.ReporterUserUid == UserUID && !r.Resolved, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (alreadyReported)
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You already reported this message and it is pending review.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
var oneHourAgo = DateTime.UtcNow - TimeSpan.FromHours(1);
|
|
var reportRateLimited = await DbContext.ReportedChatMessages
|
|
.AsNoTracking()
|
|
.AnyAsync(r => r.ReporterUserUid == UserUID && r.ReportTimeUtc >= oneHourAgo, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (reportRateLimited)
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can file at most one chat report per hour.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(messageEntry.SenderUserUid))
|
|
{
|
|
var targetAlreadyPending = await DbContext.ReportedChatMessages
|
|
.AsNoTracking()
|
|
.AnyAsync(r => r.ReportedUserUid == messageEntry.SenderUserUid && !r.Resolved, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (targetAlreadyPending)
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "This user already has a report pending review.").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var snapshotEntries = _chatChannelService.GetRecentMessages(messageEntry.Channel, 25);
|
|
var snapshotItems = snapshotEntries
|
|
.Select(e => new ChatReportSnapshotItem(
|
|
e.MessageId,
|
|
e.SentAtUtc,
|
|
e.SenderUserUid,
|
|
e.SenderUser?.AliasOrUID,
|
|
e.SenderIsLightfinder,
|
|
e.SenderHashedCid,
|
|
e.Message))
|
|
.ToArray();
|
|
|
|
var snapshotJson = JsonSerializer.Serialize(snapshotItems, ChatReportSnapshotSerializerOptions);
|
|
|
|
var report = new ReportedChatMessage
|
|
{
|
|
ReportTimeUtc = DateTime.UtcNow,
|
|
ReporterUserUid = UserUID,
|
|
ReportedUserUid = messageEntry.SenderUserUid,
|
|
ChannelType = messageEntry.Channel.Type,
|
|
WorldId = messageEntry.Channel.WorldId,
|
|
ZoneId = messageEntry.Channel.ZoneId,
|
|
ChannelKey = messageEntry.Channel.CustomKey ?? string.Empty,
|
|
MessageId = messageEntry.MessageId,
|
|
MessageSentAtUtc = messageEntry.SentAtUtc,
|
|
MessageContent = messageEntry.Message,
|
|
SenderToken = messageEntry.SenderToken,
|
|
SenderHashedCid = messageEntry.SenderHashedCid,
|
|
SenderDisplayName = messageEntry.SenderUser?.AliasOrUID,
|
|
SenderWasLightfinder = messageEntry.SenderIsLightfinder,
|
|
SnapshotJson = snapshotJson,
|
|
Reason = reason,
|
|
AdditionalContext = additionalContext
|
|
};
|
|
|
|
DbContext.ReportedChatMessages.Add(report);
|
|
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
|
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Thank you. Your report has been queued for moderator review.").ConfigureAwait(false);
|
|
}
|
|
|
|
private ChatSenderDescriptor BuildSenderDescriptor(ChatChannelDescriptor descriptor, ChatParticipantInfo participant, bool includeSensitiveInfo = false)
|
|
{
|
|
var kind = descriptor.Type == ChatChannelType.Group
|
|
? ChatSenderKind.IdentifiedUser
|
|
: ChatSenderKind.Anonymous;
|
|
|
|
string? displayName;
|
|
if (kind == ChatSenderKind.IdentifiedUser)
|
|
{
|
|
displayName = participant.User?.Alias ?? participant.User?.UID ?? participant.UserUid;
|
|
}
|
|
else if (includeSensitiveInfo && participant.IsLightfinder && !string.IsNullOrEmpty(participant.HashedCid))
|
|
{
|
|
displayName = participant.HashedCid;
|
|
}
|
|
else
|
|
{
|
|
var source = participant.UserUid ?? string.Empty;
|
|
var suffix = source.Length >= 4 ? source[^4..] : source;
|
|
displayName = string.IsNullOrEmpty(suffix) ? "Anonymous" : $"Anon-{suffix}";
|
|
}
|
|
|
|
var hashedCid = includeSensitiveInfo && participant.IsLightfinder
|
|
? participant.HashedCid
|
|
: null;
|
|
|
|
var canResolveProfile = kind == ChatSenderKind.IdentifiedUser || participant.IsLightfinder;
|
|
|
|
return new ChatSenderDescriptor(
|
|
kind,
|
|
participant.Token,
|
|
displayName,
|
|
hashedCid,
|
|
descriptor.Type == ChatChannelType.Group ? participant.User : null,
|
|
canResolveProfile);
|
|
}
|
|
|
|
private async Task<UserProfileDto?> LoadChatParticipantProfileAsync(string userUid)
|
|
{
|
|
if (string.IsNullOrEmpty(userUid))
|
|
return null;
|
|
|
|
var targetUser = await DbContext.Users
|
|
.AsNoTracking()
|
|
.SingleOrDefaultAsync(u => u.UID == userUid, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (targetUser is null)
|
|
return null;
|
|
|
|
var userData = targetUser.ToUserData();
|
|
|
|
var profileData = await DbContext.UserProfileData
|
|
.AsNoTracking()
|
|
.SingleOrDefaultAsync(p => p.UserUID == userUid, cancellationToken: RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (profileData is null)
|
|
{
|
|
return new UserProfileDto(userData, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: Array.Empty<int>());
|
|
}
|
|
|
|
if (profileData.FlaggedForReport)
|
|
{
|
|
return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile is flagged for report and pending evaluation", Tags: Array.Empty<int>());
|
|
}
|
|
|
|
if (profileData.ProfileDisabled)
|
|
{
|
|
return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile was permanently disabled", Tags: Array.Empty<int>());
|
|
}
|
|
|
|
return profileData.ToDTO();
|
|
}
|
|
|
|
private async Task<bool> ViewerAllowsLightfinderDetailsAsync(ChatChannelDescriptor descriptor)
|
|
{
|
|
if (descriptor.Type == ChatChannelType.Group)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var viewerCid = UserCharaIdent;
|
|
if (!IsValidHashedCid(viewerCid))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var (entry, expiry) = await TryGetBroadcastEntryAsync(viewerCid).ConfigureAwait(false);
|
|
return HasActiveBroadcast(entry, expiry);
|
|
}
|
|
|
|
private async Task<bool> AllowsLightfinderDetailsAsync(ChatChannelDescriptor descriptor, string userUid)
|
|
{
|
|
if (descriptor.Type == ChatChannelType.Group)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (_chatChannelService.TryGetPresence(userUid, descriptor, out var presence))
|
|
{
|
|
if (!presence.Participant.IsLightfinder || !IsValidHashedCid(presence.Participant.HashedCid))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var (entry, expiry) = await TryGetBroadcastEntryAsync(presence.Participant.HashedCid!).ConfigureAwait(false);
|
|
if (!IsActiveBroadcastForUser(entry, expiry, userUid))
|
|
{
|
|
_chatChannelService.RefreshLightfinderState(userUid, null, isLightfinder: false);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> HandleIfChatBannedAsync(string userUid)
|
|
{
|
|
var isBanned = await DbContext.Users
|
|
.AsNoTracking()
|
|
.AnyAsync(u => u.UID == userUid && u.ChatBanned, RequestAbortedToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (!isBanned)
|
|
return false;
|
|
|
|
_chatChannelService.RemovePresence(userUid);
|
|
await NotifyChatBanAsync(userUid).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
|
|
private async Task NotifyChatBanAsync(string userUid)
|
|
{
|
|
if (string.Equals(userUid, UserUID, StringComparison.Ordinal))
|
|
{
|
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your chat access has been revoked.").ConfigureAwait(false);
|
|
}
|
|
else if (_userConnections.TryGetValue(userUid, out var connectionId))
|
|
{
|
|
await Clients.Client(connectionId).Client_ReceiveServerMessage(MessageSeverity.Error, "Your chat access has been revoked.").ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static bool UseChatRateLimit(string userUid)
|
|
{
|
|
var state = ChatRateLimiters.GetOrAdd(userUid, _ => new ChatRateLimitState());
|
|
lock (state.SyncRoot)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
while (state.Events.Count > 0 && now - state.Events.Peek() >= ChatRateLimitWindow)
|
|
{
|
|
state.Events.Dequeue();
|
|
}
|
|
|
|
if (state.Events.Count >= ChatRateLimitMessages)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
state.Events.Enqueue(now);
|
|
return true;
|
|
}
|
|
}
|
|
}
|