Импорт

This commit is contained in:
Халимов Рустам
2026-04-06 22:20:13 +03:00
parent fa185afc73
commit 1558b20470
317 changed files with 18311 additions and 924 deletions
@@ -1,11 +0,0 @@
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Application.Abstractions;
/// <summary>
/// Unit of Work специфичный для модуля Chats.
/// </summary>
public interface IChatsUnitOfWork : IUnitOfWork
{
}
@@ -1,11 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Application.Abstractions;
public interface IUserDeleterService
{
Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
}
@@ -1,8 +0,0 @@
using System;
namespace Knot.Modules.Conversations.Application.Abstractions;
public interface IUserStatusService
{
bool IsUserOnline(string userId);
}
@@ -4,8 +4,8 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Storage;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
using SixLabors.ImageSharp;
@@ -2,8 +2,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
@@ -1,7 +1,7 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations.Application.Chats.Create;
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -147,7 +147,9 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
chat.CreatedAt,
members,
messagesList,
unreadCount
unreadCount,
chat.IsImporting,
chat.ImportJobId
));
}
@@ -1,6 +1,6 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
@@ -1,12 +1,13 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Shared.Kernel.Storage;
namespace Knot.Modules.Conversations.Application.Chats.LeaveOrDelete;
@@ -15,11 +16,19 @@ public record LeaveOrDeleteChatCommand(Guid ChatId, Guid UserId) : ICommand<Succ
internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrDeleteChatCommand, SuccessResponse>
{
private readonly IChatRepository _chatRepository;
private readonly IMessageRepository _messageRepository;
private readonly IFileStorageService _fileStorage;
private readonly IChatsUnitOfWork _uow;
public LeaveOrDeleteChatCommandHandler(IChatRepository chatRepository, IChatsUnitOfWork uow)
public LeaveOrDeleteChatCommandHandler(
IChatRepository chatRepository,
IMessageRepository messageRepository,
IFileStorageService fileStorage,
IChatsUnitOfWork uow)
{
_chatRepository = chatRepository;
_messageRepository = messageRepository;
_fileStorage = fileStorage;
_uow = uow;
}
@@ -36,13 +45,18 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
return Result.Failure<SuccessResponse>(ChatErrors.Unauthorized);
}
if (chat.Type == ChatType.Group)
// If it's a private chat or the last member leaving a group, delete everything
bool shouldDeleteEverything = chat.Type != ChatType.Group || chat.Members.Count <= 1;
if (chat.Type == ChatType.Group && !shouldDeleteEverything)
{
chat.RemoveMember(request.UserId);
_chatRepository.Update(chat);
}
else
{
// DELETE ALL MESSAGES AND FILES FIRST
await DeleteChatMediaAndMessagesAsync(chat.Id, cancellationToken);
_chatRepository.Remove(chat);
}
@@ -50,5 +64,45 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
return Result.Success(new SuccessResponse(true));
}
}
private async Task DeleteChatMediaAndMessagesAsync(Guid chatId, CancellationToken ct)
{
try
{
// Get all messages directly from Mongo (not paged)
var messages = await _messageRepository.GetChatMessagesAsync(chatId, int.MaxValue, 0, ct);
foreach (var msg in messages)
{
if (msg is Knot.Contracts.Messaging.Domain.MediaMessage mediaMsg)
{
foreach (var media in mediaMsg.Media)
{
if (!string.IsNullOrEmpty(media.Url))
{
var fileId = ExtractFileId(media.Url);
if (!string.IsNullOrEmpty(fileId))
{
await _fileStorage.DeleteFileAsync(fileId);
}
}
}
}
}
await _messageRepository.DeleteChatMessagesAsync(chatId, ct);
}
catch (Exception ex)
{
// Log if possible, but don't fail chat deletion
Console.WriteLine($"[Cleanup] Error deleting chat media: {ex.Message}");
}
}
private string? ExtractFileId(string url)
{
var lastSlash = url.LastIndexOf('/');
if (lastSlash == -1) return null;
var id = url[(lastSlash + 1)..];
if (id.Contains('?')) id = id[..id.IndexOf('?')];
return id;
}
}
@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
@@ -2,8 +2,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
using MediatR;
using System.Linq;
@@ -2,8 +2,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
@@ -12,6 +12,8 @@ public record ChatDto(
DateTime CreatedAt,
List<ChatMemberDto> Members,
List<ChatMessageDto> Messages,
int UnreadCount
int UnreadCount,
bool IsImporting = false,
Guid? ImportJobId = null
);
@@ -1,4 +1,4 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using System;
namespace Knot.Modules.Conversations.Application.DTOs;
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
namespace Knot.Modules.Conversations.Application.DTOs;
@@ -1,5 +1,5 @@
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
@@ -1,5 +1,5 @@
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
@@ -1,5 +1,5 @@
using global::Knot.Modules.Conversations.Application.Abstractions;
using global::Knot.Modules.Conversations.Domain;
using global::Knot.Contracts.Conversations.Application.Abstractions;
using global::Knot.Contracts.Conversations.Domain;
using global::Knot.Modules.Conversations.Infrastructure.SignalR;
using global::Knot.Shared.Kernel;
using MediatR;
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -39,12 +39,21 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
}
DateTime? cursorDate = null;
if (!string.IsNullOrEmpty(request.Cursor) && DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
long? cursorSequenceId = null;
if (!string.IsNullOrEmpty(request.Cursor))
{
cursorDate = parsed.ToUniversalTime();
if (long.TryParse(request.Cursor, out var seqId))
{
cursorSequenceId = seqId;
}
else if (DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
{
cursorDate = parsed.ToUniversalTime();
}
}
var messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, ChatConstants.DefaultMessageQueryLimit, cancellationToken);
var messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, cursorSequenceId, ChatConstants.DefaultMessageQueryLimit, cancellationToken);
var result = new List<MessageDetailDto>();
var userIdsToFetch = new HashSet<Guid>();
@@ -88,7 +97,8 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
ReplyToMessageDto? replyToObj = null;
if (message.ReplyToId.HasValue && replyMessages.TryGetValue(message.ReplyToId.Value, out var replyMsg))
{
var senderObj = senders.TryGetValue(replyMsg.SenderId, out var rs)
senders.TryGetValue(replyMsg.SenderId, out var rs);
var senderObj = rs != null
? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar)
: null;
@@ -135,7 +145,9 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
message.CreatedAt,
message.SequenceId,
message.ForwardedFromId,
message.ForwardedFromId.HasValue && senders.TryGetValue(message.ForwardedFromId.Value, out var fwdUser) ? new MessageSenderDto(fwdUser.Id, fwdUser.Username, fwdUser.DisplayName, fwdUser.Avatar) : null,
message.ForwardedFromId.HasValue && senders.TryGetValue(message.ForwardedFromId.Value, out var fwdUser)
? new MessageSenderDto(fwdUser.Id, fwdUser.Username, fwdUser.DisplayName, fwdUser.Avatar)
: null,
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
@@ -6,9 +6,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -88,7 +88,12 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
var filteredMedia = messageMedia.Where(media =>
{
var mType = media.Type?.ToLower() ?? "file";
var isGif = mType == "image" && media.Url != null && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
var filename = media.Filename?.ToLower() ?? "";
var url = media.Url?.ToLower() ?? "";
var isGif = mType == "gif" ||
(mType == "image" && (filename.EndsWith(".mp4") || filename.EndsWith(".gif") || url.EndsWith(".gif") || filename.Contains("gif"))) ||
(mType == "video" && (filename.Contains("animation") || filename.Contains("gif")));
if (filterType == "gifs")
{
@@ -1,7 +1,7 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using Knot.Shared.Kernel;
using MediatR;
@@ -1,7 +1,7 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using Knot.Shared.Kernel;
using MediatR;
@@ -1,7 +1,7 @@
using MediatR;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations.Application.Messages.Read;
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -1,8 +1,8 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Application.Messages.Send;
@@ -1,4 +1,4 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using System;
using System.IO;
using System.Threading;
@@ -2,8 +2,8 @@ using System.Text.RegularExpressions;
using Knot.Contracts.Auth.Application.Abstractions;
using Knot.Contracts.Auth.Domain;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Storage;
using MediatR;
@@ -55,15 +55,18 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserComma
{
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)
if (!string.IsNullOrEmpty(media.Url))
{
var fileId = ExtractFileId(media.Url);
if (!string.IsNullOrEmpty(fileId))
bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id &&
m is MediaMessage mm && mm.Media.Any(ame => !string.IsNullOrEmpty(ame.Url) && ame.Url == media.Url));
if (!isUsedElsewhere)
{
await _fileStorage.DeleteFileAsync(fileId);
var fileId = ExtractFileId(media.Url);
if (!string.IsNullOrEmpty(fileId))
{
await _fileStorage.DeleteFileAsync(fileId);
}
}
}
}
@@ -1,16 +1,14 @@
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
using Knot.Modules.Conversations.Infrastructure.Services;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ConversationsAbstractions = Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Messaging.Application.Abstractions;
namespace Knot.Modules.Conversations;
@@ -29,21 +27,22 @@ public static class DependencyInjection
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
services.AddScoped<ConversationsAbstractions.IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatRepository, ChatRepository>();
services.AddScoped<IFolderRepository, FolderRepository>();
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>();
services.AddScoped<Knot.Contracts.Conversations.Domain.IUserFolderSettingsRepository, UserFolderSettingsRepository>();
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>();
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IChatAccessProvider, ChatAccessProvider>();
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
services.AddScoped<ConversationsAbstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserDeleterService, Knot.Modules.Conversations.Infrastructure.Services.UserDeleterService>();
services.AddScoped<Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService, UserStatusService>();
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, UserStatusService>();
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserDeleterService, UserDeleterService>();
return services;
}
}
@@ -1,157 +0,0 @@
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Domain;
public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent;
public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent;
/// <summary>
/// Тип чата: личный или групповой.
/// </summary>
public enum ChatType
{
Personal,
Group,
Favorites
}
/// <summary>
/// Роль участника в чате.
/// </summary>
public static class ChatRole
{
public const string Owner = "owner";
public const string Admin = "admin";
public const string Member = "member";
}
/// <summary>
/// Сущность чата (Агрегат).
/// </summary>
public sealed class Chat : AggregateRoot<Guid>
{
public ChatType Type { get; private set; }
public string? Name { get; private set; }
public string? Description { get; private set; }
public string? Avatar { get; private set; }
public DateTime CreatedAt { get; private set; }
public long LastMessageSequenceId { get; private set; }
private readonly List<ChatMember> _members = new();
public IReadOnlyCollection<ChatMember> Members => _members.AsReadOnly();
private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
{
Type = type;
Name = name;
Avatar = avatar;
Description = description;
CreatedAt = DateTime.UtcNow;
}
/// <summary>
/// Создает личный чат между двумя пользователями.
/// </summary>
public static Chat CreatePersonal()
{
var chat = new Chat(Guid.NewGuid(), ChatType.Personal, null, null);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
/// <summary>
/// Создает групповой чат.
/// </summary>
public static Chat CreateGroup(string name, string? avatar = null)
{
var chat = new Chat(Guid.NewGuid(), ChatType.Group, name, avatar);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
/// <summary>
/// Фабричный метод для создания чата.
/// </summary>
public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
{
var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
public void AddMember(Guid userId, string role = "member")
{
if (_members.Any(m => m.UserId == userId))
{
return;
}
_members.Add(new ChatMember(Id, userId, role));
RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId));
}
public void RemoveMember(Guid userId)
{
var member = _members.FirstOrDefault(m => m.UserId == userId);
if (member != null)
{
_members.Remove(member);
}
}
public void UpdateName(string name) => Name = name;
public void UpdateDescription(string? description) => Description = description;
public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl;
public long IncrementSequenceId()
{
return ++LastMessageSequenceId;
}
}
/// <summary>
/// Участник чата.
/// </summary>
public sealed class ChatMember : Entity<Guid>
{
public Guid ChatId { get; private set; }
public Guid UserId { get; private set; }
public string Role { get; private set; }
public DateTime JoinedAt { get; private set; }
public bool IsPinned { get; private set; }
public bool IsMuted { get; private set; }
public Guid? LastReadMessageId { get; private set; }
public long LastReadSequenceId { get; private set; }
public Guid? LastDeliveredMessageId { get; private set; }
// For EF Core
private ChatMember() : base(Guid.Empty) { Role = "member"; }
internal ChatMember(Guid chatId, Guid userId, string role) : base(Guid.NewGuid())
{
ChatId = chatId;
UserId = userId;
Role = role;
JoinedAt = DateTime.UtcNow;
}
public void TogglePin() => IsPinned = !IsPinned;
public void UpdateReadCursor(Guid messageId, long sequenceId)
{
if (sequenceId > LastReadSequenceId)
{
LastReadMessageId = messageId;
LastReadSequenceId = sequenceId;
}
}
public void UpdateDeliveredCursor(Guid messageId)
{
LastDeliveredMessageId = messageId;
}
}
@@ -1,10 +0,0 @@
namespace Knot.Modules.Conversations.Domain;
public static class ChatConstants
{
public const int DefaultMessageQueryLimit = 100;
public const int MaxSharedMediaQueryLimit = 300;
public const int SearchMessagesLimit = 50;
public const int MaxFileUploadSizeMb = 50;
}
@@ -1,25 +0,0 @@
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Domain;
public static class ChatErrors
{
public static readonly Error FileEmpty = new Error("File.Empty", "No file uploaded");
public static readonly Error FileInvalidExtension = new Error("File.InvalidExtension", "Must be a ZIP archive");
public static readonly Error ImportExpired = new Error("Import.Expired", "Session not found or expired");
public static readonly Error ImportMissing = new Error("Import.Missing", "ZIP file lost");
public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Chat not found or access denied");
public static readonly Error NotFound = new Error("Chat.NotFound", "Chat not found"); // Alias
public static readonly Error NotMember = new Error("Chat.NotMember", "You are not a member of this chat");
public static readonly Error ChatsForbidden = new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.");
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.");
}
@@ -1,108 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
using Knot.Modules.Conversations.Domain;
namespace Knot.Modules.Conversations.Domain;
public interface IChatRepository
{
void Add(Chat chat);
void Update(Chat chat);
void Remove(Chat chat);
Task<Chat?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
Task<Chat?> GetFavoritesAsync(Guid userId, CancellationToken cancellationToken);
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);
}
@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using MediatR;
using Microsoft.AspNetCore.SignalR;
@@ -1,5 +1,5 @@
using Knot.Contracts.Conversations.Domain;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Conversations.Domain;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
@@ -32,4 +32,4 @@ public class ChatsDbContextFactory : IDesignTimeDbContextFactory<ChatsDbContext>
optionsBuilder.UseNpgsql(connectionString);
return new ChatsDbContext(optionsBuilder.Options);
}
}
}
@@ -4,19 +4,18 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using DomainChat = Knot.Modules.Conversations.Domain.Chat;
using DomainChat = Knot.Contracts.Conversations.Domain.Chat;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
public sealed class ChatsDbContext : DbContext, Knot.Contracts.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
{
private readonly IMediator? _mediator;
private readonly IEncryptionService? _encryptionService;
@@ -54,6 +53,8 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli
{
builder.ToTable("Chats");
builder.HasKey(c => c.Id);
builder.Property(c => c.IsImporting);
builder.Property(c => c.ImportJobId);
builder.Property(c => c.Type).HasConversion<string>();
builder.OwnsMany(c => c.Members, mb =>
@@ -1,4 +1,4 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using MongoDB.Bson.Serialization;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
@@ -1,4 +1,4 @@
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using MongoDB.Driver;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
@@ -18,4 +18,4 @@ public sealed class UserDeleterService : IUserDeleterService
{
return await _sender.Send(new DeleteUserCommand(userId), cancellationToken);
}
}
}
@@ -1,9 +1,11 @@
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Infrastructure.SignalR;
namespace Knot.Modules.Conversations.Infrastructure.Services;
public sealed class UserStatusService : IUserStatusService, Knot.Contracts.Conversations.Abstractions.IUserStatusService
public sealed class UserStatusService :
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService,
Knot.Contracts.Conversations.Abstractions.IUserStatusService
{
public bool IsUserOnline(string userId)
{
@@ -8,7 +8,7 @@ using Knot.Modules.Conversations.Application.Messages.Send;
using Knot.Modules.Conversations.Application.Messages.Read;
using Knot.Modules.Conversations.Application.Messages.Delete;
using Knot.Modules.Conversations.Application.Messages.React;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Microsoft.Extensions.Caching.Memory;
using Knot.Contracts.Auth.Domain;
@@ -5,4 +5,4 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Microsoft.AspNetCore.SignalR;
public class MessageNotifier : IMessageNotifier { private readonly IHubContext<ChatHub> _hubContext; public MessageNotifier(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
public class MessageNotifier : IMessageNotifier { private readonly IHubContext<ChatHub> _hubContext; public MessageNotifier(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
@@ -26,7 +26,7 @@ namespace Knot.Modules.Conversations.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -56,9 +56,9 @@ namespace Knot.Modules.Conversations.Migrations
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -0,0 +1,166 @@
// <auto-generated />
using System;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Conversations.Migrations
{
[DbContext(typeof(ChatsDbContext))]
[Migration("20260406122703_AddIsImportingToChat")]
partial class AddIsImportingToChat
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("chats")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IsImporting")
.HasColumnType("boolean");
b.Property<long>("LastMessageSequenceId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Icon")
.HasColumnType("text");
b.Property<bool>("IsDefault")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Folders", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChatId")
.HasColumnType("uuid");
b.Property<string>("FolderIds")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsMuted")
.HasColumnType("boolean");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChatId")
.IsUnique();
b.ToTable("UserChatSettings", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<Guid>("ChatId")
.HasColumnType("uuid");
b1.Property<bool>("IsMuted")
.HasColumnType("boolean");
b1.Property<bool>("IsPinned")
.HasColumnType("boolean");
b1.Property<DateTime>("JoinedAt")
.HasColumnType("timestamp with time zone");
b1.Property<Guid?>("LastDeliveredMessageId")
.HasColumnType("uuid");
b1.Property<Guid?>("LastReadMessageId")
.HasColumnType("uuid");
b1.Property<long>("LastReadSequenceId")
.HasColumnType("bigint");
b1.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.HasKey("Id");
b1.HasIndex("ChatId", "UserId")
.IsUnique();
b1.ToTable("ChatMembers", "chats");
b1.WithOwner()
.HasForeignKey("ChatId");
});
b.Navigation("Members");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,32 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Knot.Modules.Conversations.Migrations
{
/// <inheritdoc />
public partial class AddIsImportingToChat : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsImporting",
schema: "chats",
table: "Chats",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsImporting",
schema: "chats",
table: "Chats");
}
}
}
@@ -0,0 +1,169 @@
// <auto-generated />
using System;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Conversations.Migrations
{
[DbContext(typeof(ChatsDbContext))]
[Migration("20260406123444_AddImportJobIdToChat")]
partial class AddImportJobIdToChat
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("chats")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Guid?>("ImportJobId")
.HasColumnType("uuid");
b.Property<bool>("IsImporting")
.HasColumnType("boolean");
b.Property<long>("LastMessageSequenceId")
.HasColumnType("bigint");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Icon")
.HasColumnType("text");
b.Property<bool>("IsDefault")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Folders", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChatId")
.HasColumnType("uuid");
b.Property<string>("FolderIds")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsMuted")
.HasColumnType("boolean");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChatId")
.IsUnique();
b.ToTable("UserChatSettings", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<Guid>("ChatId")
.HasColumnType("uuid");
b1.Property<bool>("IsMuted")
.HasColumnType("boolean");
b1.Property<bool>("IsPinned")
.HasColumnType("boolean");
b1.Property<DateTime>("JoinedAt")
.HasColumnType("timestamp with time zone");
b1.Property<Guid?>("LastDeliveredMessageId")
.HasColumnType("uuid");
b1.Property<Guid?>("LastReadMessageId")
.HasColumnType("uuid");
b1.Property<long>("LastReadSequenceId")
.HasColumnType("bigint");
b1.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.HasKey("Id");
b1.HasIndex("ChatId", "UserId")
.IsUnique();
b1.ToTable("ChatMembers", "chats");
b1.WithOwner()
.HasForeignKey("ChatId");
});
b.Navigation("Members");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,31 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Knot.Modules.Conversations.Migrations
{
/// <inheritdoc />
public partial class AddImportJobIdToChat : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ImportJobId",
schema: "chats",
table: "Chats",
type: "uuid",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ImportJobId",
schema: "chats",
table: "Chats");
}
}
}
@@ -23,7 +23,7 @@ namespace Knot.Modules.Conversations.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -38,6 +38,12 @@ namespace Knot.Modules.Conversations.Migrations
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Guid?>("ImportJobId")
.HasColumnType("uuid");
b.Property<bool>("IsImporting")
.HasColumnType("boolean");
b.Property<long>("LastMessageSequenceId")
.HasColumnType("bigint");
@@ -53,9 +59,61 @@ namespace Knot.Modules.Conversations.Migrations
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
{
b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Icon")
.HasColumnType("text");
b.Property<bool>("IsDefault")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Folders", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChatId")
.HasColumnType("uuid");
b.Property<string>("FolderIds")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsMuted")
.HasColumnType("boolean");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChatId")
.IsUnique();
b.ToTable("UserChatSettings", "chats");
});
modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -9,7 +9,7 @@ using Knot.Modules.Conversations.Application.Chats.Members;
using Knot.Modules.Conversations.Application.Chats.TogglePin;
using Knot.Modules.Conversations.Application.Chats.Update;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Modules.Conversations.Domain;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.AspNetCore.Builder;