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

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

View File

@@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Messaging.Domain;
using Knot.Modules.Messaging.Infrastructure.Persistence;
namespace Knot.Modules.Conversations;
@@ -25,12 +28,19 @@ public static class DependencyInjection
options.UseNpgsql(connectionString));
// MongoDB Setup for Messages
ConversationsMongoDbMapConfigurator.Configure();
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
// Registration
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatRepository, ChatRepository>();
services.AddScoped<IFolderRepository, FolderRepository>();
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>();
// Messaging Repository registration (might be redundant if already in Messaging module, but needed for specific commands in Conversations)
services.AddScoped<IMessageRepository, MessageRepository>();
// MediatR
services.AddMediatR(config =>

View File

@@ -15,6 +15,9 @@ public static class ChatErrors
public static readonly Error MessagesNotFound = new Error("Messages.NotFound", "Message not found.");
public static readonly Error ChatsNotFound = new Error("Chats.NotFound", "Чат не найден.");
public static readonly Error Unauthorized = new Error("Chats.Unauthorized", "Access denied");
public static readonly Error FoldersDisabled = new Error("Folders.Disabled", "Folders feature is disabled by the administrator.");
public static readonly Error PollsDisabled = new Error("Polls.Disabled", "Polls are disabled by the administrator.");
public static readonly Error MediaDisabled = new Error("Media.Disabled", "Media messages are disabled by the administrator.");
public static Error ImportCreateChatFailed(string msg) => new Error("Import.CreateChatFailed", msg);
public static Error FileTooLarge(int maxMb) => new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB.");

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Domain;
/// <summary>
/// Сущность папки для группировки чатов.
/// </summary>
public sealed class Folder : AggregateRoot<Guid>
{
public string Name { get; private set; }
public string? Icon { get; private set; } // URL из хранилища
public bool IsDefault { get; private set; }
public FolderType Type { get; private set; }
public Folder(Guid id, string name, string? icon = null, bool isDefault = false, FolderType type = FolderType.Custom)
: base(id)
{
Name = name;
Icon = icon;
IsDefault = isDefault;
Type = type;
}
public void Update(string name, string? icon)
{
if (IsDefault) throw new InvalidOperationException("Cannot rename default folders.");
Name = name;
Icon = icon;
}
}
public enum FolderType
{
All, // Все чаты
New, // Новые (с непрочитанными)
Muted, // Без звука
Custom // Пользовательская
}
/// <summary>
/// Настройки конкретного чата для конкретного пользователя.
/// Хранятся в PostgreSQL (связь User <-> Chat).
/// </summary>
public sealed class UserChatSettings : Entity<Guid>
{
public Guid UserId { get; private set; }
public Guid ChatId { get; private set; }
// Список папок, в которые входит чат для этого пользователя
private readonly List<Guid> _folderIds = new();
public IReadOnlyCollection<Guid> FolderIds => _folderIds.AsReadOnly();
public bool IsMuted { get; private set; }
private UserChatSettings() : base(Guid.NewGuid()) { }
public UserChatSettings(Guid userId, Guid chatId) : base(Guid.NewGuid())
{
UserId = userId;
ChatId = chatId;
}
public static UserChatSettings Create(Guid userId, Guid chatId) => new(userId, chatId);
public void AddToFolder(Guid folderId)
{
if (!_folderIds.Contains(folderId)) _folderIds.Add(folderId);
}
public void RemoveFromFolder(Guid folderId)
{
_folderIds.Remove(folderId);
}
public void SetMute(bool isMuted) => IsMuted = isMuted;
}
/// <summary>
/// Глобальные настройки папок пользователя (скрытие дефолтных и т.д.).
/// Будет храниться в MongoDB.
/// </summary>
public sealed class UserFolderSettings : AggregateRoot<Guid>
{
public Guid UserId { get; private set; }
// Список ID папок, которые пользователь скрыл (только для дефолтных)
public List<Guid> HiddenDefaultFolderIds { get; private set; } = new();
// Список пользовательских папок (Guid созданных Folder)
public List<Guid> CustomFolderIds { get; private set; } = new();
public UserFolderSettings(Guid userId) : base(Guid.NewGuid())
{
UserId = userId;
}
public void HideFolder(Guid folderId)
{
if (!HiddenDefaultFolderIds.Contains(folderId)) HiddenDefaultFolderIds.Add(folderId);
}
public void ShowFolder(Guid folderId)
{
HiddenDefaultFolderIds.Remove(folderId);
}
}

View File

@@ -12,3 +12,28 @@ public interface IChatRepository
Task<List<Chat>> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken);
}
public interface IFolderRepository
{
void Add(Folder folder);
void Update(Folder folder);
void Remove(Folder folder);
Task<Folder?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
Task<List<Folder>> GetUserFoldersAsync(Guid userId, CancellationToken cancellationToken);
}
public interface IUserChatSettingsRepository
{
void Add(UserChatSettings settings);
void Update(UserChatSettings settings);
void Remove(UserChatSettings settings);
void RemoveRange(IEnumerable<UserChatSettings> settings);
Task<UserChatSettings?> GetAsync(Guid userId, Guid chatId, CancellationToken cancellationToken);
Task<List<UserChatSettings>> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
}
public interface IUserFolderSettingsRepository
{
Task<UserFolderSettings?> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
Task UpdateAsync(UserFolderSettings settings, CancellationToken cancellationToken);
Task RemoveByUserIdAsync(Guid userId, CancellationToken cancellationToken);
}

View File

@@ -54,3 +54,59 @@ public sealed class ChatRepository : IChatRepository
}
}
public sealed class FolderRepository : IFolderRepository
{
private readonly ChatsDbContext _dbContext;
public FolderRepository(ChatsDbContext dbContext)
{
_dbContext = dbContext;
}
public void Add(Folder folder) => _dbContext.Folders.Add(folder);
public void Update(Folder folder) => _dbContext.Folders.Update(folder);
public void Remove(Folder folder) => _dbContext.Folders.Remove(folder);
public async Task<Folder?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
{
return await _dbContext.Folders.FirstOrDefaultAsync(f => f.Id == id, cancellationToken);
}
public async Task<List<Folder>> GetUserFoldersAsync(Guid userId, CancellationToken cancellationToken)
{
// В доменной модели Folder не имеет UserId напрямую (может быть общей сущностью),
// но по логике "папки в настройках пользователя" можно фильтровать через настройки.
// Пока возвращаем все папки, которыми владеет пользователь (если добавить UserId)
// или все для упрощения первой итерации.
return await _dbContext.Folders.ToListAsync(cancellationToken);
}
}
public sealed class UserChatSettingsRepository : IUserChatSettingsRepository
{
private readonly ChatsDbContext _dbContext;
public UserChatSettingsRepository(ChatsDbContext dbContext)
{
_dbContext = dbContext;
}
public void Add(UserChatSettings settings) => _dbContext.UserChatSettings.Add(settings);
public void Update(UserChatSettings settings) => _dbContext.UserChatSettings.Update(settings);
public void Remove(UserChatSettings settings) => _dbContext.UserChatSettings.Remove(settings);
public void RemoveRange(IEnumerable<UserChatSettings> settings) => _dbContext.UserChatSettings.RemoveRange(settings);
public async Task<UserChatSettings?> GetAsync(Guid userId, Guid chatId, CancellationToken cancellationToken)
{
return await _dbContext.UserChatSettings
.FirstOrDefaultAsync(s => s.UserId == userId && s.ChatId == chatId, cancellationToken);
}
public async Task<List<UserChatSettings>> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken)
{
return await _dbContext.UserChatSettings
.Where(s => s.UserId == userId)
.ToListAsync(cancellationToken);
}
}

View File

@@ -5,6 +5,11 @@ using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
@@ -24,8 +29,8 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
}
public DbSet<Chat> Chats => Set<Chat>();
public DbSet<Folder> Folders => Set<Folder>();
public DbSet<UserChatSettings> UserChatSettings => Set<UserChatSettings>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@@ -43,7 +48,6 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
builder.HasKey(c => c.Id);
builder.Property(c => c.Type).HasConversion<string>();
builder.OwnsMany(c => c.Members, mb =>
{
mb.ToTable("ChatMembers");
@@ -53,15 +57,32 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
}).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Folder>(builder =>
{
builder.ToTable("Folders");
builder.HasKey(f => f.Id);
builder.Property(f => f.Type).HasConversion<string>();
});
modelBuilder.Entity<UserChatSettings>(builder =>
{
builder.ToTable("UserChatSettings");
builder.HasKey(s => s.Id);
builder.HasIndex(s => new { s.UserId, s.ChatId }).IsUnique();
builder.Property(s => s.FolderIds)
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(Guid.Parse).ToList()
);
});
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// 1. Получаем все события из агрегатов
var domainEvents = ChangeTracker
.Entries<IAggregateRoot>()
.SelectMany(x =>
{
if (x.Entity is AggregateRoot<Guid> root)
{
@@ -73,10 +94,8 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
})
.ToList();
// 2. Сохраняем изменения
int result = await base.SaveChangesAsync(cancellationToken);
// 3. Публикуем события через MediatR
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
@@ -85,4 +104,3 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
return result;
}
}

View File

@@ -0,0 +1,25 @@
using Knot.Modules.Conversations.Domain;
using MongoDB.Bson.Serialization;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
public static class ConversationsMongoDbMapConfigurator
{
private static bool _initialized;
public static void Configure()
{
if (_initialized) return;
if (!BsonClassMap.IsClassMapRegistered(typeof(UserFolderSettings)))
{
BsonClassMap.RegisterClassMap<UserFolderSettings>(cm =>
{
cm.AutoMap();
cm.SetDiscriminator("UserFolderSettings");
});
}
_initialized = true;
}
}

View File

@@ -0,0 +1,30 @@
using Knot.Modules.Conversations.Domain;
using MongoDB.Driver;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
public sealed class UserFolderSettingsRepository : IUserFolderSettingsRepository
{
private readonly IMongoCollection<UserFolderSettings> _collection;
public UserFolderSettingsRepository(IMongoDatabase database)
{
_collection = database.GetCollection<UserFolderSettings>("user_folder_settings");
}
public async Task<UserFolderSettings?> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken)
{
return await _collection.Find(s => s.UserId == userId).FirstOrDefaultAsync(cancellationToken);
}
public async Task UpdateAsync(UserFolderSettings settings, CancellationToken cancellationToken)
{
var options = new ReplaceOptions { IsUpsert = true };
await _collection.ReplaceOneAsync(s => s.Id == settings.Id, settings, options, cancellationToken);
}
public async Task RemoveByUserIdAsync(Guid userId, CancellationToken cancellationToken)
{
await _collection.DeleteManyAsync(s => s.UserId == userId, cancellationToken);
}
}

View File

@@ -3,6 +3,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
</ItemGroup>
<ItemGroup>
@@ -35,4 +36,3 @@
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,155 @@
{
"$schema": "http://json.schemastore.org/sarif-1.0.0",
"version": "1.0.0",
"runs": [
{
"tool": {
"name": "Компилятор Microsoft (R) Visual C#",
"version": "5.0.0.0",
"fileVersion": "5.0.0-1.25358.103 (75972a5b)",
"semanticVersion": "5.0.0",
"language": "ru-RU"
},
"results": [
{
"ruleId": "CS0234",
"level": "error",
"message": "Тип или имя пространства имен \"Auth\" не существует в пространстве имен \"Knot.Modules\" (возможно, отсутствует ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 1,
"startColumn": 20,
"endLine": 1,
"endColumn": 24
}
}
}
]
},
{
"ruleId": "CS0234",
"level": "error",
"message": "Тип или имя пространства имен \"Auth\" не существует в пространстве имен \"Knot.Modules\" (возможно, отсутствует ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 9,
"startColumn": 20,
"endLine": 9,
"endColumn": 24
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IUserRepository\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 17,
"startColumn": 22,
"endLine": 17,
"endColumn": 37
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IAuthUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 23,
"startColumn": 22,
"endLine": 23,
"endColumn": 37
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IUserRepository\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 26,
"startColumn": 9,
"endLine": 26,
"endColumn": 24
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IAuthUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 32,
"startColumn": 9,
"endLine": 32,
"endColumn": 24
}
}
}
]
}
],
"rules": {
"CS0234": {
"id": "CS0234",
"defaultLevel": "error",
"helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0234)",
"properties": {
"category": "Compiler",
"isEnabledByDefault": true,
"tags": [
"Compiler",
"Telemetry",
"NotConfigurable"
]
}
},
"CS0246": {
"id": "CS0246",
"defaultLevel": "error",
"helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0246)",
"properties": {
"category": "Compiler",
"isEnabledByDefault": true,
"tags": [
"Compiler",
"Telemetry",
"NotConfigurable"
]
}
}
}
}
]
}

View File

@@ -0,0 +1,245 @@
{
"$schema": "http://json.schemastore.org/sarif-1.0.0",
"version": "1.0.0",
"runs": [
{
"tool": {
"name": "Компилятор Microsoft (R) Visual C#",
"version": "5.0.0.0",
"fileVersion": "5.0.0-1.25358.103 (75972a5b)",
"semanticVersion": "5.0.0",
"language": "ru-RU"
},
"results": [
{
"ruleId": "CS0234",
"level": "error",
"message": "Тип или имя пространства имен \"Auth\" не существует в пространстве имен \"Knot.Modules\" (возможно, отсутствует ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 1,
"startColumn": 20,
"endLine": 1,
"endColumn": 24
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs",
"region": {
"startLine": 12,
"startColumn": 22,
"endLine": 12,
"endColumn": 38
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs",
"region": {
"startLine": 14,
"startColumn": 86,
"endLine": 14,
"endColumn": 102
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs",
"region": {
"startLine": 12,
"startColumn": 22,
"endLine": 12,
"endColumn": 38
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs",
"region": {
"startLine": 14,
"startColumn": 91,
"endLine": 14,
"endColumn": 107
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IUserRepository\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 15,
"startColumn": 22,
"endLine": 15,
"endColumn": 37
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 20,
"startColumn": 22,
"endLine": 20,
"endColumn": 38
}
}
}
]
},
{
"ruleId": "CS0234",
"level": "error",
"message": "Тип или имя пространства имен \"Auth\" не существует в пространстве имен \"Knot.Modules\" (возможно, отсутствует ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 21,
"startColumn": 35,
"endLine": 21,
"endColumn": 39
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IUserRepository\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 24,
"startColumn": 9,
"endLine": 24,
"endColumn": 24
}
}
}
]
},
{
"ruleId": "CS0246",
"level": "error",
"message": "Не удалось найти тип или имя пространства имен \"IChatsUnitOfWork\" (возможно, отсутствует директива using или ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 29,
"startColumn": 9,
"endLine": 29,
"endColumn": 25
}
}
}
]
},
{
"ruleId": "CS0234",
"level": "error",
"message": "Тип или имя пространства имен \"Auth\" не существует в пространстве имен \"Knot.Modules\" (возможно, отсутствует ссылка на сборку).",
"locations": [
{
"resultFile": {
"uri": "file:///E:/GIT/forkmessager/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs",
"region": {
"startLine": 30,
"startColumn": 22,
"endLine": 30,
"endColumn": 26
}
}
}
]
}
],
"rules": {
"CS0234": {
"id": "CS0234",
"defaultLevel": "error",
"helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0234)",
"properties": {
"category": "Compiler",
"isEnabledByDefault": true,
"tags": [
"Compiler",
"Telemetry",
"NotConfigurable"
]
}
},
"CS0246": {
"id": "CS0246",
"defaultLevel": "error",
"helpUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0246)",
"properties": {
"category": "Compiler",
"isEnabledByDefault": true,
"tags": [
"Compiler",
"Telemetry",
"NotConfigurable"
]
}
}
}
}
]
}