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; /// /// Команда для отправки сообщения в чат. /// public record AttachmentRequest(string Type, string Url, string? FileName, long? FileSize); public sealed record SendMessageCommand( Guid ChatId, Guid SenderId, string? Content, string Type, List? Attachments = null, Guid? ReplyToId = null, string? Quote = null, Guid? ForwardedFromId = null, Guid? StoryId = null, string? StoryMediaUrl = null, string? StoryMediaType = null, List? PollOptions = null, bool? PollIsAnonymous = null, bool? PollAllowMultipleAnswers = null, DateTime? PollExpiresAt = null) : ICommand; public sealed class SendMessageCommandHandler : ICommandHandler { 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> Handle(SendMessageCommand request, CancellationToken cancellationToken) { // 1. Проверяем существование чата var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); if (chat is null) { return Result.Failure(ChatErrors.ChatsNotFound); } // 2. Проверяем, является ли отправитель участником if (!chat.Members.Any(m => m.UserId == request.SenderId)) { return Result.Failure(ChatErrors.ChatsForbidden); } // 3. Создаем сообщение Message message; if (request.Type == "story_reply" || request.Type == "story_reaction") { if (!_messagesSettings.Current.AllowMedia) return Result.Failure(ChatErrors.MediaDisabled); var parsedStoryMediaType = Enum.TryParse(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(ChatErrors.MediaDisabled); var firstAtt = request.Attachments.First(); var parsedType = Enum.TryParse(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(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(ChatErrors.PollsDisabled); message = new PollMessage( Guid.NewGuid(), request.ChatId, request.SenderId, request.Content ?? "Poll", request.PollOptions ?? new List(), 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); } }