215 lines
7.1 KiB
C#
215 lines
7.1 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD>.
|
||
/// </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,
|
||
string? CallType = null,
|
||
string? CallStatus = null,
|
||
int? Duration = 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. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
||
if (chat is null)
|
||
{
|
||
return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
||
}
|
||
|
||
// 2. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||
if (!chat.Members.Any(m => m.UserId == request.SenderId))
|
||
{
|
||
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
||
}
|
||
|
||
// 3. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||
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,
|
||
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<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.ToString().ToLower(), att.Url, att.FileName, att.FileSize);
|
||
}
|
||
}
|
||
else if (request.Type == "poll")
|
||
{
|
||
if (!_messagesSettings.Current.AllowPolls) return Result.Failure<Guid>(ChatErrors.PollsDisabled);
|
||
if (chat.Type != ChatType.Group) return Result.Failure<Guid>(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<string>(),
|
||
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. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 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. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||
_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);
|
||
}
|
||
}
|