170 lines
6.2 KiB
C#
170 lines
6.2 KiB
C#
using Knot.Modules.Messaging.Application.Abstractions;
|
|
using Knot.Modules.Messaging.Domain;
|
|
using Knot.Modules.Conversations.Domain;
|
|
using Knot.Shared.Kernel;
|
|
using Knot.Shared.Kernel.Configuration;
|
|
using Knot.Modules.Conversations.Application.Abstractions;
|
|
|
|
namespace Knot.Modules.Conversations.Application.Messages.Send;
|
|
|
|
/// <summary>
|
|
/// Команда для отправки сообщения в чат.
|
|
/// </summary>
|
|
public record AttachmentRequest(string Type, string Url, string? FileName, long? FileSize);
|
|
|
|
public sealed record SendMessageCommand(
|
|
Guid ChatId,
|
|
Guid SenderId,
|
|
string? Content,
|
|
string Type,
|
|
List<AttachmentRequest>? Attachments = null,
|
|
Guid? ReplyToId = null,
|
|
string? Quote = null,
|
|
Guid? ForwardedFromId = null,
|
|
Guid? StoryId = null,
|
|
string? StoryMediaUrl = null,
|
|
string? StoryMediaType = null,
|
|
List<string>? PollOptions = null,
|
|
bool? PollIsAnonymous = null,
|
|
bool? PollAllowMultipleAnswers = null,
|
|
DateTime? PollExpiresAt = null) : ICommand<Guid>;
|
|
|
|
public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageCommand, Guid>
|
|
{
|
|
private readonly IChatRepository _chatRepository;
|
|
private readonly IMessageRepository _messageRepository;
|
|
private readonly IChatsUnitOfWork _unitOfWork;
|
|
private readonly MediatR.IMediator _mediator;
|
|
private readonly IMessagesSettings _messagesSettings;
|
|
|
|
public SendMessageCommandHandler(
|
|
IChatRepository chatRepository,
|
|
IMessageRepository messageRepository,
|
|
IChatsUnitOfWork unitOfWork,
|
|
MediatR.IMediator mediator,
|
|
IMessagesSettings messagesSettings)
|
|
{
|
|
_chatRepository = chatRepository;
|
|
_messageRepository = messageRepository;
|
|
_unitOfWork = unitOfWork;
|
|
_mediator = mediator;
|
|
_messagesSettings = messagesSettings;
|
|
}
|
|
|
|
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
|
{
|
|
// 1. Проверяем существование чата
|
|
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
|
if (chat is null)
|
|
{
|
|
return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
|
}
|
|
|
|
// 2. Проверяем, является ли отправитель участником
|
|
if (!chat.Members.Any(m => m.UserId == request.SenderId))
|
|
{
|
|
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
|
}
|
|
|
|
// 3. Создаем сообщение
|
|
Message message;
|
|
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
|
{
|
|
if (!_messagesSettings.Current.AllowMedia) return Result.Failure<Guid>(ChatErrors.MediaDisabled);
|
|
|
|
var parsedStoryMediaType = Enum.TryParse<MediaType>(request.StoryMediaType, true, out var sTypeEnum) ? sTypeEnum : MediaType.Image;
|
|
message = new StoryMessage(
|
|
Guid.NewGuid(),
|
|
request.ChatId,
|
|
request.SenderId,
|
|
request.StoryId ?? Guid.Empty,
|
|
request.StoryMediaUrl ?? string.Empty,
|
|
parsedStoryMediaType,
|
|
request.Content,
|
|
request.ReplyToId,
|
|
request.ForwardedFromId,
|
|
DateTime.UtcNow,
|
|
false);
|
|
}
|
|
else if (request.Attachments != null && request.Attachments.Any())
|
|
{
|
|
if (!_messagesSettings.Current.AllowMedia) return Result.Failure<Guid>(ChatErrors.MediaDisabled);
|
|
|
|
var firstAtt = request.Attachments.First();
|
|
var parsedType = Enum.TryParse<MediaType>(firstAtt.Type, true, out var mTypeEnum) ? mTypeEnum : MediaType.File;
|
|
|
|
message = new MediaMessage(
|
|
Guid.NewGuid(),
|
|
request.ChatId,
|
|
request.SenderId,
|
|
parsedType,
|
|
request.Content,
|
|
request.ReplyToId,
|
|
request.ForwardedFromId,
|
|
DateTime.UtcNow,
|
|
false);
|
|
|
|
foreach (var att in request.Attachments)
|
|
{
|
|
var pType = Enum.TryParse<MediaType>(att.Type, true, out var tEnum) ? tEnum : MediaType.File;
|
|
((MediaMessage)message).AddMedia(pType, att.Url, att.FileName, att.FileSize);
|
|
}
|
|
}
|
|
else if (request.Type == "poll")
|
|
{
|
|
if (!_messagesSettings.Current.AllowPolls) return Result.Failure<Guid>(ChatErrors.PollsDisabled);
|
|
|
|
message = new PollMessage(
|
|
Guid.NewGuid(),
|
|
request.ChatId,
|
|
request.SenderId,
|
|
request.Content ?? "Poll",
|
|
request.PollOptions ?? new List<string>(),
|
|
request.PollIsAnonymous ?? true,
|
|
request.PollAllowMultipleAnswers ?? false,
|
|
request.PollExpiresAt,
|
|
request.ReplyToId,
|
|
request.ForwardedFromId,
|
|
DateTime.UtcNow,
|
|
false);
|
|
}
|
|
else
|
|
{
|
|
message = new TextMessage(
|
|
Guid.NewGuid(),
|
|
request.ChatId,
|
|
request.SenderId,
|
|
request.Content ?? string.Empty,
|
|
request.ReplyToId,
|
|
request.Quote,
|
|
request.ForwardedFromId,
|
|
DateTime.UtcNow,
|
|
false);
|
|
}
|
|
|
|
// 4. Последовательность сообщений High-Water Mark
|
|
chat.IncrementSequenceId();
|
|
message.SetSequenceId(chat.LastMessageSequenceId);
|
|
|
|
var senderMember = chat.Members.First(m => m.UserId == request.SenderId);
|
|
senderMember.UpdateReadCursor(message.Id, message.SequenceId);
|
|
senderMember.UpdateDeliveredCursor(message.Id);
|
|
|
|
// 5. Сохраняем
|
|
_messageRepository.Add(message);
|
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
|
|
|
await _mediator.Publish(new MessageSentDomainEvent(
|
|
message.Id,
|
|
message.ChatId,
|
|
message.SenderId,
|
|
message.Content),
|
|
cancellationToken);
|
|
|
|
return Result.Success(message.Id);
|
|
}
|
|
}
|
|
|
|
|
|
|