199 lines
8.8 KiB
C#
199 lines
8.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
|
using Knot.Contracts.Conversations.Domain;
|
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
|
using Knot.Contracts.Messaging.Domain;
|
|
using Knot.Modules.Conversations.Application.DTOs;
|
|
using Knot.Shared.Kernel;
|
|
using MediatR;
|
|
|
|
namespace Knot.Modules.Conversations.Application.Messages.GetMessages;
|
|
|
|
public record GetMessagesQuery(Guid UserId, Guid ChatId, string? Cursor, long? Pivot = null, long? AfterSequenceId = null, int? Limit = null) : IQuery<List<MessageDetailDto>>;
|
|
|
|
internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery, List<MessageDetailDto>>
|
|
{
|
|
private readonly IMessageRepository _messageRepository;
|
|
private readonly IUserDisplayNameProvider _userProvider;
|
|
private readonly IChatRepository _chatRepository;
|
|
private readonly IMessageReactionRepository _reactionRepository;
|
|
|
|
public GetMessagesQueryHandler(IMessageRepository messageRepository, IUserDisplayNameProvider userProvider, IChatRepository chatRepository, IMessageReactionRepository reactionRepository)
|
|
{
|
|
_messageRepository = messageRepository;
|
|
_userProvider = userProvider;
|
|
_chatRepository = chatRepository;
|
|
_reactionRepository = reactionRepository;
|
|
}
|
|
|
|
public async Task<Result<List<MessageDetailDto>>> Handle(GetMessagesQuery request, CancellationToken cancellationToken)
|
|
{
|
|
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
|
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
|
|
{
|
|
return Result.Failure<List<MessageDetailDto>>(ChatErrors.ChatsForbidden);
|
|
}
|
|
|
|
List<Message> messages;
|
|
int queryLimit = request.Limit ?? ChatConstants.DefaultMessageQueryLimit;
|
|
|
|
if (request.AfterSequenceId.HasValue)
|
|
{
|
|
// Получаем только сообщения ПОСЛЕ указанного sequenceId (для синхронизации)
|
|
messages = await _messageRepository.GetChatMessagesAfterAsync(request.ChatId, request.AfterSequenceId.Value, queryLimit, cancellationToken);
|
|
}
|
|
else if (request.Pivot.HasValue)
|
|
{
|
|
messages = await _messageRepository.GetChatMessagesAroundAsync(request.ChatId, request.Pivot.Value, queryLimit, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
DateTime? cursorDate = null;
|
|
long? cursorSequenceId = null;
|
|
|
|
if (!string.IsNullOrEmpty(request.Cursor))
|
|
{
|
|
if (long.TryParse(request.Cursor, out var seqId))
|
|
{
|
|
cursorSequenceId = seqId;
|
|
}
|
|
else if (DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
|
|
{
|
|
cursorDate = parsed.ToUniversalTime();
|
|
}
|
|
}
|
|
|
|
messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, cursorSequenceId, queryLimit, cancellationToken);
|
|
}
|
|
|
|
var result = new List<MessageDetailDto>();
|
|
var userIdsToFetch = new HashSet<Guid>();
|
|
var replyMessages = new Dictionary<Guid, Message>();
|
|
|
|
// Filter messages that are deleted for the current user before processing
|
|
var filteredMessages = messages.Where(m => !m.IsDeletedForUser(request.UserId)).ToList();
|
|
|
|
foreach (var m in filteredMessages)
|
|
{
|
|
userIdsToFetch.Add(m.SenderId);
|
|
|
|
if (m is PollMessage poll && !poll.IsAnonymous)
|
|
{
|
|
foreach (var vote in poll.Votes)
|
|
{
|
|
userIdsToFetch.Add(vote.UserId);
|
|
}
|
|
}
|
|
|
|
if (!m.ReplyToId.HasValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var replyMsg = await _messageRepository.GetByIdAsync(m.ReplyToId.Value, cancellationToken);
|
|
if (replyMsg == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
replyMessages[replyMsg.Id] = replyMsg;
|
|
userIdsToFetch.Add(replyMsg.SenderId);
|
|
}
|
|
|
|
var senders = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
|
|
|
var messageIds = filteredMessages.Select(m => m.Id).ToList();
|
|
var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken);
|
|
var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
foreach (var message in messages)
|
|
{
|
|
if (message.IsDeletedForUser(request.UserId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
senders.TryGetValue(message.SenderId, out var sender);
|
|
reactionsByMessage.TryGetValue(message.Id, out var reactions);
|
|
|
|
|
|
Message? replyMsg = null;
|
|
if (message.ReplyToId.HasValue)
|
|
{
|
|
replyMessages.TryGetValue(message.ReplyToId.Value, out replyMsg);
|
|
}
|
|
|
|
UserInfo? replySender = null;
|
|
if (replyMsg != null)
|
|
{
|
|
senders.TryGetValue(replyMsg.SenderId, out replySender);
|
|
}
|
|
|
|
result.Add(new MessageDetailDto(
|
|
message.Id,
|
|
message.ChatId,
|
|
message.SenderId,
|
|
message.Content,
|
|
message.Type.ToLower(),
|
|
message.ReplyToId,
|
|
replyMsg != null ? new ReplyToMessageDto(
|
|
replyMsg.Id,
|
|
replyMsg.Content,
|
|
replyMsg.IsDeleted,
|
|
replyMsg is MediaMessage mm ? mm.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() : new List<MediaDto>(),
|
|
replySender != null ? new MessageSenderDto(replySender.Id, replySender.Username, replySender.DisplayName, replySender.Avatar) : null
|
|
) : null,
|
|
(message as TextMessage)?.Quote,
|
|
message.IsEdited,
|
|
message.IsDeleted,
|
|
message.CreatedAt,
|
|
message.SequenceId,
|
|
message.ForwardedFromId,
|
|
null, // ForwardedFrom details not implemented here yet
|
|
(message as StoryMessage)?.StoryId,
|
|
(message as StoryMessage)?.StoryMediaUrl,
|
|
(message as StoryMessage)?.StoryMediaType,
|
|
(message as MediaMessage)?.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() ?? new List<MediaDto>(),
|
|
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null),
|
|
message.ReadByUsers.Select(id => new ReadByDto(id)).ToList(),
|
|
reactions?.Select(r =>
|
|
{
|
|
senders.TryGetValue(r.UserId, out var ru);
|
|
return new MessageReactionDto(r.Id, r.Emoji, r.UserId, ru != null ? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar) : null);
|
|
}).ToList() ?? new List<MessageReactionDto>(),
|
|
(message as CallMessage)?.CallType,
|
|
(message as CallMessage)?.CallStatus,
|
|
(message as CallMessage)?.Duration,
|
|
(message as PollMessage)?.Options.Select(o =>
|
|
{
|
|
var pm = (PollMessage)message;
|
|
var voters = pm.IsAnonymous == false
|
|
? pm.Votes
|
|
.Where(v => v.OptionId == o.Id)
|
|
.Select(v =>
|
|
{
|
|
senders.TryGetValue(v.UserId, out var vu);
|
|
return vu != null
|
|
? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar)
|
|
: new MessageSenderDto(v.UserId, "unknown", "Unknown", null);
|
|
})
|
|
.ToList()
|
|
: null;
|
|
return new PollOptionDto(o.Id, o.Text, o.VoteCount, voters, pm.IsAnonymous == false ? pm.Votes.Where(v => v.OptionId == o.Id).Select(v => v.UserId).ToList() : null);
|
|
}).ToList(),
|
|
(message as PollMessage)?.IsMultipleChoice,
|
|
(message as PollMessage)?.IsAnonymous,
|
|
(message as PollMessage)?.IsClosed,
|
|
(message as PollMessage)?.Votes.Where(v => v.UserId == request.UserId).Select(v => v.OptionId).ToList(),
|
|
message.IsDeletedForUser(request.UserId)
|
|
));
|
|
}
|
|
|
|
return Result.Success(result);
|
|
}
|
|
}
|