Структура, доп модули, федерация, документация

This commit is contained in:
Халимов Рустам
2026-03-27 00:55:01 +03:00
parent 7cb6ac61dd
commit 7ef73b414c
64 changed files with 3080 additions and 133 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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(

View File

@@ -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));
}

View File

@@ -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;
}
}