Структура, доп модули, федерация, документация
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Folders.Commands.AddToFolder;
|
||||
|
||||
public record AddToFolderCommand(Guid UserId, Guid ChatId, List<Guid> FolderIds) : ICommand;
|
||||
|
||||
internal sealed class AddToFolderCommandHandler : ICommandHandler<AddToFolderCommand>
|
||||
{
|
||||
private readonly IUserChatSettingsRepository _settingsRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IChatsSettings _chatsSettings;
|
||||
|
||||
public AddToFolderCommandHandler(
|
||||
IUserChatSettingsRepository settingsRepository,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IChatsSettings chatsSettings)
|
||||
{
|
||||
_settingsRepository = settingsRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_chatsSettings = chatsSettings;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(AddToFolderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_chatsSettings.Current.EnableFolders)
|
||||
{
|
||||
return Result.Failure(ChatErrors.FoldersDisabled);
|
||||
}
|
||||
|
||||
var settings = await _settingsRepository.GetAsync(request.UserId, request.ChatId, cancellationToken);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
settings = UserChatSettings.Create(request.UserId, request.ChatId);
|
||||
_settingsRepository.Add(settings);
|
||||
}
|
||||
|
||||
foreach (var folderId in request.FolderIds)
|
||||
{
|
||||
settings.AddToFolder(folderId);
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Folders.Commands.RemoveFromFolder;
|
||||
|
||||
public record RemoveFromFolderCommand(Guid UserId, Guid ChatId, List<Guid> FolderIds) : ICommand;
|
||||
|
||||
internal sealed class RemoveFromFolderCommandHandler : ICommandHandler<RemoveFromFolderCommand>
|
||||
{
|
||||
private readonly IUserChatSettingsRepository _settingsRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
|
||||
public RemoveFromFolderCommandHandler(IUserChatSettingsRepository settingsRepository, IChatsUnitOfWork unitOfWork)
|
||||
{
|
||||
_settingsRepository = settingsRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(RemoveFromFolderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsRepository.GetAsync(request.UserId, request.ChatId, cancellationToken);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
foreach (var folderId in request.FolderIds)
|
||||
{
|
||||
settings.RemoveFromFolder(folderId);
|
||||
}
|
||||
|
||||
_settingsRepository.Update(settings);
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using global::Knot.Modules.Conversations.Domain;
|
||||
using global::Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using global::Knot.Shared.Kernel;
|
||||
using global::Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MessagingMessageRepository = Knot.Modules.Messaging.Domain.IMessageRepository;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Delete;
|
||||
|
||||
@@ -17,12 +16,12 @@ public sealed record DeleteMessagesCommand(
|
||||
|
||||
public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessagesCommand>
|
||||
{
|
||||
private readonly IMessageRepository _messageRepository;
|
||||
private readonly MessagingMessageRepository _messageRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IHubContext<ChatHub> _hubContext;
|
||||
|
||||
public DeleteMessagesCommandHandler(
|
||||
IMessageRepository messageRepository,
|
||||
MessagingMessageRepository messageRepository,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IHubContext<ChatHub> hubContext)
|
||||
{
|
||||
@@ -67,7 +66,6 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
// Уведомляем только самого пользователя (все его текущие сессии)
|
||||
await _hubContext.Clients.User(request.UserId.ToString()).SendAsync("messages_deleted", new
|
||||
{
|
||||
chatId = request.ChatId,
|
||||
@@ -79,5 +77,3 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
|
||||
return global::Knot.Shared.Kernel.Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -22,7 +23,11 @@ public sealed record SendMessageCommand(
|
||||
Guid? ForwardedFromId = null,
|
||||
Guid? StoryId = null,
|
||||
string? StoryMediaUrl = null,
|
||||
string? StoryMediaType = null) : ICommand<Guid>;
|
||||
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>
|
||||
{
|
||||
@@ -30,17 +35,20 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
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)
|
||||
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)
|
||||
@@ -62,6 +70,8 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
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(),
|
||||
@@ -78,6 +88,8 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -98,6 +110,24 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
((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(
|
||||
|
||||
@@ -16,9 +16,9 @@ public record UploadFileCommand(string FileName, string ContentType, long Length
|
||||
internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileCommand, UploadFileResponseDto>
|
||||
{
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IMessagesSettings _settingsService;
|
||||
|
||||
public UploadFileCommandHandler(IFileStorageService fileStorage, ISettingsService settingsService)
|
||||
public UploadFileCommandHandler(IFileStorageService fileStorage, IMessagesSettings settingsService)
|
||||
{
|
||||
_fileStorage = fileStorage;
|
||||
_settingsService = settingsService;
|
||||
@@ -31,8 +31,10 @@ internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileComma
|
||||
return Result.Failure<UploadFileResponseDto>(ChatErrors.FileEmpty);
|
||||
}
|
||||
|
||||
var maxMb = _settingsService.Current.MaxFileSizeMb;
|
||||
if (request.Length > maxMb * 1024 * 1024)
|
||||
var maxBytes = _settingsService.Current.MaxMediaSizeBytes;
|
||||
var maxMb = maxBytes / (1024 * 1024);
|
||||
|
||||
if (request.Length > maxBytes)
|
||||
{
|
||||
return Result.Failure<UploadFileResponseDto>(ChatErrors.FileTooLarge(maxMb));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MediatR;
|
||||
using System.Text.RegularExpressions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Auth.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||
|
||||
public record DeleteUserCommand(Guid UserId) : ICommand;
|
||||
|
||||
internal sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserChatSettingsRepository _userChatSettingsRepository;
|
||||
private readonly IUserFolderSettingsRepository _userFolderSettingsRepository;
|
||||
private readonly Knot.Modules.Messaging.Domain.IMessageRepository _messageRepository;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IAuthUnitOfWork _authUnitOfWork;
|
||||
|
||||
public DeleteUserCommandHandler(
|
||||
IUserRepository userRepository,
|
||||
IUserChatSettingsRepository userChatSettingsRepository,
|
||||
IUserFolderSettingsRepository userFolderSettingsRepository,
|
||||
Knot.Modules.Messaging.Domain.IMessageRepository messageRepository,
|
||||
IFileStorageService fileStorage,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IAuthUnitOfWork authUnitOfWork)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userChatSettingsRepository = userChatSettingsRepository;
|
||||
_userFolderSettingsRepository = userFolderSettingsRepository;
|
||||
_messageRepository = messageRepository;
|
||||
_fileStorage = fileStorage;
|
||||
_unitOfWork = unitOfWork;
|
||||
_authUnitOfWork = authUnitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null) return Result.Failure(Error.NotFound("User.NotFound", "User not found"));
|
||||
|
||||
// 1. Delete Messages and their files in MongoDB
|
||||
var allMessages = await _messageRepository.GetAllMessagesAsync(cancellationToken);
|
||||
var userMessages = allMessages.Where(m => m.SenderId == request.UserId).ToList();
|
||||
|
||||
foreach (var msg in userMessages)
|
||||
{
|
||||
if (msg is MediaMessage mediaMsg)
|
||||
{
|
||||
foreach (var media in mediaMsg.Media)
|
||||
{
|
||||
bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id &&
|
||||
m is MediaMessage mm && mm.Media.Any(ame => ame.Url == media.Url));
|
||||
|
||||
if (!isUsedElsewhere)
|
||||
{
|
||||
var fileId = ExtractFileId(media.Url);
|
||||
if (!string.IsNullOrEmpty(fileId))
|
||||
{
|
||||
await _fileStorage.DeleteFileAsync(fileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _messageRepository.DeleteUserMessagesAsync(request.UserId, cancellationToken);
|
||||
|
||||
// 2. Delete Folder Settings (MongoDB)
|
||||
await _userFolderSettingsRepository.RemoveByUserIdAsync(request.UserId, cancellationToken);
|
||||
|
||||
// 3. Delete Chat Settings (Postgres)
|
||||
var chatSettings = await _userChatSettingsRepository.GetByUserIdAsync(request.UserId, cancellationToken);
|
||||
_userChatSettingsRepository.RemoveRange(chatSettings);
|
||||
|
||||
// 4. Delete User (Postgres - Auth Module)
|
||||
_userRepository.Remove(user);
|
||||
|
||||
// Commit all
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _authUnitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
private string? ExtractFileId(string url)
|
||||
{
|
||||
var match = Regex.Match(url, @"/([^/]+)$");
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user