using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Contracts.Conversations.Domain; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Shared.Kernel; 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, string? CallType = null, string? CallStatus = null, int? Duration = 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, request.StoryMediaType, 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.ToString().ToLower(), att.Url, att.FileName, att.FileSize); } } else if (request.Type == "poll") { if (!_messagesSettings.Current.AllowPolls) return Result.Failure(ChatErrors.PollsDisabled); if (chat.Type != ChatType.Group) return Result.Failure(new Error("Poll.InvalidChat", "Polls are only allowed in groups.")); message = PollMessage.Create( 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); } else if (request.Type == "call") { message = new CallMessage( Guid.NewGuid(), request.ChatId, request.SenderId, request.CallType ?? "voice", request.CallStatus ?? "completed", request.Duration, 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); message.MarkAsRead(request.SenderId); // Отправитель всегда "прочитал" своё сообщение // 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); } }