diff --git a/apps/server-net/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs b/apps/server-net/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs index 11be342..186a425 100644 --- a/apps/server-net/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs +++ b/apps/server-net/src/Host/Application/Admin/Commands/CleanRun/CleanRunCommand.cs @@ -8,6 +8,8 @@ using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; using Knot.Modules.Identity.Infrastructure.Persistence; using Knot.Modules.Chats.Infrastructure.Persistence; +using Knot.Modules.Chats.Domain; +using MongoDB.Driver; using Microsoft.EntityFrameworkCore; namespace Host.Application.Admin.Commands; @@ -17,33 +19,36 @@ public record CleanRunCommand(IFileStorageService FileStorage, IdentityDbContext internal sealed class CleanRunCommandHandler : ICommandHandler { private readonly ChatsDbContext _chatsDbContext; + private readonly IMongoCollection _messages; - public CleanRunCommandHandler(ChatsDbContext chatsDbContext) + public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb) { _chatsDbContext = chatsDbContext; + _messages = mongoDb.GetCollection("Messages"); } public async Task> Handle(CleanRunCommand request, CancellationToken cancellationToken) { - var orphanMessages = await _chatsDbContext.Messages - .Include(m => m.Media) - .Where(m => m.IsDeleted || !_chatsDbContext.Chats.Any(c => c.Id == m.ChatId)) - .ToListAsync(cancellationToken); + var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken); + var activeChatIds = allChats.Select(c => c.Id).ToHashSet(); + + var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken); + + var orphanMessages = allMessages + .Where(m => !activeChatIds.Contains(m.ChatId)) + .ToList(); + + var keptMessages = allMessages + .Where(m => activeChatIds.Contains(m.ChatId)) + .ToList(); var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList(); - var keptMessages = await _chatsDbContext.Messages - .AsNoTracking() - .Include(m => m.Media) - .Where(m => !m.IsDeleted && _chatsDbContext.Chats.Any(c => c.Id == m.ChatId)) - .ToListAsync(cancellationToken); - - var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken); var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken); var validUrls = new HashSet(); - var activeMessageUrls = keptMessages + var activeMessageUrls = keptMessages.OfType() .Where(m => m.Media != null) .SelectMany(m => m.Media) .Select(me => me.Url) @@ -85,8 +90,9 @@ internal sealed class CleanRunCommandHandler : ICommandHandler m.Id).ToList(); + var filter = Builders.Filter.In(m => m.Id, orphanIds); + await _messages.DeleteManyAsync(filter, cancellationToken); } return Result.Success(new MessageResponse("Cleanup completed successfully")); diff --git a/apps/server-net/src/Host/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs b/apps/server-net/src/Host/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs index e7f09d4..47403c0 100644 --- a/apps/server-net/src/Host/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs +++ b/apps/server-net/src/Host/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs @@ -8,6 +8,8 @@ using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; using Knot.Modules.Identity.Infrastructure.Persistence; using Knot.Modules.Chats.Infrastructure.Persistence; +using Knot.Modules.Chats.Domain; +using MongoDB.Driver; using Microsoft.EntityFrameworkCore; using Host.Models; @@ -18,35 +20,38 @@ public record CleanDryRunQuery(IFileStorageService FileStorage, IdentityDbContex internal sealed class CleanDryRunQueryHandler : IQueryHandler { private readonly ChatsDbContext _chatsDbContext; + private readonly IMongoCollection _messages; - public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext) + public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb) { _chatsDbContext = chatsDbContext; + _messages = mongoDb.GetCollection("Messages"); } public async Task> Handle(CleanDryRunQuery request, CancellationToken cancellationToken) { - var orphanedMessages = await _chatsDbContext.Messages - .Include(m => m.Media) - .Where(m => m.IsDeleted || !_chatsDbContext.Chats.Any(c => c.Id == m.ChatId)) - .ToListAsync(cancellationToken); + var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken); + var activeChatIds = allChats.Select(c => c.Id).ToHashSet(); + + var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken); + + var orphanedMessages = allMessages + .Where(m => !activeChatIds.Contains(m.ChatId)) + .ToList(); + + var keptMessages = allMessages + .Where(m => activeChatIds.Contains(m.ChatId)) + .ToList(); var orphanedMessagesCount = orphanedMessages.Count; var allMinioFiles = (await request.FileStorage.ListFilesAsync()).ToList(); - var keptMessages = await _chatsDbContext.Messages - .AsNoTracking() - .Include(m => m.Media) - .Where(m => !m.IsDeleted && _chatsDbContext.Chats.Any(c => c.Id == m.ChatId)) - .ToListAsync(cancellationToken); - - var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken); var allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken); var validUrls = new HashSet(); - var activeMessageUrls = keptMessages + var activeMessageUrls = keptMessages.OfType() .Where(m => m.Media != null) .SelectMany(m => m.Media) .Select(me => me.Url) diff --git a/apps/server-net/src/Host/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs b/apps/server-net/src/Host/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs index 2167f73..d6df2be 100644 --- a/apps/server-net/src/Host/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs +++ b/apps/server-net/src/Host/Application/Admin/Queries/GetUserDetails/GetUserDetailsQuery.cs @@ -6,8 +6,8 @@ using MediatR; using Knot.Shared.Kernel; using Host.Models; using Knot.Modules.Identity.Domain; -using Knot.Modules.Chats.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; +using Knot.Modules.Chats.Domain; +using MongoDB.Driver; namespace Host.Application.Admin.Queries; @@ -16,12 +16,12 @@ public record GetUserDetailsQuery(Guid UserId) : IQuery; internal sealed class GetUserDetailsQueryHandler : IQueryHandler { private readonly IUserRepository _userRepository; - private readonly ChatsDbContext _chatsDbContext; + private readonly IMongoCollection _messages; - public GetUserDetailsQueryHandler(IUserRepository userRepository, ChatsDbContext chatsDbContext) + public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase) { _userRepository = userRepository; - _chatsDbContext = chatsDbContext; + _messages = mongoDatabase.GetCollection("Messages"); } public async Task> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken) @@ -32,23 +32,20 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler(IdentityErrors.UserNotFound); } - var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken); + var filter = Builders.Filter.Eq(m => m.SenderId, request.UserId); + var userMessages = await _messages.Find(filter).ToListAsync(cancellationToken); - var allUserMedia = await _chatsDbContext.Messages - .AsNoTracking() - .Where(m => m.SenderId == request.UserId) - .SelectMany(m => m.Media) - .ToListAsync(cancellationToken); + var messagesCount = userMessages.Count; + + var allUserMedia = userMessages.OfType().SelectMany(m => m.Media).ToList(); var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video"); var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio"); var storageUsed = allUserMedia.Sum(m => m.Size ?? 0); - var userContents = await _chatsDbContext.Messages - .AsNoTracking() - .Where(m => m.SenderId == request.UserId) - .Select(m => m.Content) - .ToListAsync(cancellationToken); + var userContents = userMessages.OfType().Select(m => m.Content) + .Concat(userMessages.OfType().Where(m => m.Caption != null).Select(m => m.Caption)) + .ToList(); var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http")); diff --git a/apps/server-net/src/Host/Program.cs b/apps/server-net/src/Host/Program.cs index 8304d72..f077954 100644 --- a/apps/server-net/src/Host/Program.cs +++ b/apps/server-net/src/Host/Program.cs @@ -27,6 +27,7 @@ var builder = WebApplication.CreateBuilder(args); var envMappings = new Dictionary { ["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"], + ["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"], ["Jwt:Secret"] = builder.Configuration["JWT_SECRET"], ["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"], ["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"], @@ -160,6 +161,10 @@ using (var scope = app.Services.CreateScope()) var systemDb = scope.ServiceProvider.GetRequiredService(); await systemDb.Database.MigrateAsync(); + // Set Encryption Service for MongoDB serializers + var encryptionService = scope.ServiceProvider.GetRequiredService(); + Knot.Modules.Chats.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService; + // Initialize Global Settings Cache var settingsService = scope.ServiceProvider.GetRequiredService(); if (settingsService is Knot.Shared.Infrastructure.Configuration.SettingsService concreteSettings) diff --git a/apps/server-net/src/Modules/Chats/Application/Chats/GetChatById/GetChatById.cs b/apps/server-net/src/Modules/Chats/Application/Chats/GetChatById/GetChatById.cs index 1d93ab3..4ee81d6 100644 --- a/apps/server-net/src/Modules/Chats/Application/Chats/GetChatById/GetChatById.cs +++ b/apps/server-net/src/Modules/Chats/Application/Chats/GetChatById/GetChatById.cs @@ -40,33 +40,32 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler(); - foreach (var m in chat.Members) + foreach (var member in chat.Members) { - userIdsToFetch.Add(m.UserId); + userIdsToFetch.Add(member.UserId); } - var chatMessages = await _messageRepository.GetChatMessagesAsync(chat.Id, 1, 0, cancellationToken); - var mFirst = chatMessages.FirstOrDefault(); - if (mFirst != null) + var latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken); + if (latestMessage != null) { - userIdsToFetch.Add(mFirst.SenderId); - foreach (var r in mFirst.Reactions) + userIdsToFetch.Add(latestMessage.SenderId); + foreach (var reaction in latestMessage.Reactions) { - userIdsToFetch.Add(r.UserId); + userIdsToFetch.Add(reaction.UserId); } } var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); var members = new List(); - foreach (var m in chat.Members) + foreach (var member in chat.Members) { - usersInfo.TryGetValue(m.UserId, out var user); + usersInfo.TryGetValue(member.UserId, out var user); members.Add(new ChatMemberDto( - m.Id, - m.UserId, - m.Role, - m.IsPinned, + member.Id, + member.UserId, + member.Role, + member.IsPinned, user != null ? new ChatUserDto( user.Id, user.Username, @@ -79,48 +78,47 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler(); - if (chatMessages.Any()) + if (latestMessage != null) { - var m = mFirst; - usersInfo.TryGetValue(m.SenderId, out var senderObj); + usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj); var reactionsWithUser = new List(); - foreach (var r in m.Reactions) + foreach (var reaction in latestMessage.Reactions) { - usersInfo.TryGetValue(r.UserId, out var rUser); + usersInfo.TryGetValue(reaction.UserId, out var reactionUser); reactionsWithUser.Add(new ReactionDto( - r.Id, - r.Emoji, - r.UserId, - rUser != null - ? new MessageSenderDto(rUser.Id, rUser.Username, rUser.DisplayName, rUser.Avatar) - : new MessageSenderDto(r.UserId, "unknown", "Unknown", null) + reaction.Id, + reaction.Emoji, + reaction.UserId, + reactionUser != null + ? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar) + : new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null) )); } messagesList.Add(new ChatMessageDto( - m.Id, - m.ChatId, - m.SenderId, - m.Content, - m.Type, - m.ReplyToId, - m.Quote, - m.StoryId, - m.StoryMediaUrl, - m.StoryMediaType, - m.IsEdited, - m.IsDeleted, - m.CreatedAt, - m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), + latestMessage.Id, + latestMessage.ChatId, + latestMessage.SenderId, + latestMessage.Content, + latestMessage.Type, + latestMessage.ReplyToId, + latestMessage.Quote, + latestMessage.StoryId, + latestMessage.StoryMediaUrl, + latestMessage.StoryMediaType, + latestMessage.IsEdited, + latestMessage.IsDeleted, + latestMessage.CreatedAt, + latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), senderObj != null ? new MessageSenderDto( senderObj.Id, senderObj.Username, senderObj.DisplayName, senderObj.Avatar - ) : new MessageSenderDto(m.SenderId, "unknown", "Unknown", null), + ) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null), reactionsWithUser, - m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList() + latestMessage.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList() )); } @@ -139,3 +137,4 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler(dto); } } + diff --git a/apps/server-net/src/Modules/Chats/Application/Chats/GetChats/GetChats.cs b/apps/server-net/src/Modules/Chats/Application/Chats/GetChats/GetChats.cs index 9ac35b1..726b4e4 100644 --- a/apps/server-net/src/Modules/Chats/Application/Chats/GetChats/GetChats.cs +++ b/apps/server-net/src/Modules/Chats/Application/Chats/GetChats/GetChats.cs @@ -33,36 +33,38 @@ internal sealed class GetChatsQueryHandler : IQueryHandler(); bool hasFavorites = false; - foreach (var c in userChats) + foreach (var chat in userChats) { - var userIdsToFetch = new HashSet(); - foreach (var m in c.Members) + var latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken); + + if (latestMessage == null) { - userIdsToFetch.Add(m.UserId); + continue; } - var chatMessages = await _messageRepository.GetChatMessagesAsync(c.Id, 1, 0, cancellationToken); - var mFirst = chatMessages.FirstOrDefault(); - if (mFirst != null) + var userIdsToFetch = new HashSet(); + foreach (var member in chat.Members) { - userIdsToFetch.Add(mFirst.SenderId); - foreach (var r in mFirst.Reactions) - { - userIdsToFetch.Add(r.UserId); - } + userIdsToFetch.Add(member.UserId); + } + + userIdsToFetch.Add(latestMessage.SenderId); + foreach (var r in latestMessage.Reactions) + { + userIdsToFetch.Add(r.UserId); } var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); var members = new List(); - foreach (var m in c.Members) + foreach (var member in chat.Members) { - usersInfo.TryGetValue(m.UserId, out var user); + usersInfo.TryGetValue(member.UserId, out var user); members.Add(new ChatMemberDto( - m.Id, - m.UserId, - m.Role, - m.IsPinned, + member.Id, + member.UserId, + member.Role, + member.IsPinned, user != null ? new ChatUserDto( user.Id, user.Username, @@ -74,62 +76,59 @@ internal sealed class GetChatsQueryHandler : IQueryHandler(); + usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj); - if (chatMessages.Any()) + var reactionsWithUser = new List(); + foreach (var reaction in latestMessage.Reactions) { - var m = mFirst; - usersInfo.TryGetValue(m.SenderId, out var senderObj); + usersInfo.TryGetValue(reaction.UserId, out var reactionUser); + reactionsWithUser.Add(new ReactionDto( + reaction.Id, + reaction.Emoji, + reaction.UserId, + reactionUser != null + ? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar) + : new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null) + )); + } - var reactionsWithUser = new List(); - foreach (var r in m.Reactions) - { - usersInfo.TryGetValue(r.UserId, out var rUser); - reactionsWithUser.Add(new ReactionDto( - r.Id, - r.Emoji, - r.UserId, - rUser != null - ? new MessageSenderDto(rUser.Id, rUser.Username, rUser.DisplayName, rUser.Avatar) - : new MessageSenderDto(r.UserId, "unknown", "Unknown", null) - )); - } - - messagesList.Add(new ChatMessageDto( - m.Id, - m.ChatId, - m.SenderId, - m.Content, - m.Type, - m.ReplyToId, - m.Quote, - m.StoryId, - m.StoryMediaUrl, - m.StoryMediaType, - m.IsEdited, - m.IsDeleted, - m.CreatedAt, - m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), + var messagesList = new List + { + new ChatMessageDto( + latestMessage.Id, + latestMessage.ChatId, + latestMessage.SenderId, + latestMessage.Content, + latestMessage.Type, + latestMessage.ReplyToId, + latestMessage.Quote, + latestMessage.StoryId, + latestMessage.StoryMediaUrl, + latestMessage.StoryMediaType, + latestMessage.IsEdited, + latestMessage.IsDeleted, + latestMessage.CreatedAt, + latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), senderObj != null ? new MessageSenderDto( senderObj.Id, senderObj.Username, senderObj.DisplayName, senderObj.Avatar - ) : new MessageSenderDto(m.SenderId, "unknown", "Unknown", null), + ) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null), reactionsWithUser, - m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList() - )); - } + latestMessage.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList() + ) + }; - var unreadCount = await _messageRepository.GetUnreadCountAsync(c.Id, request.UserId, cancellationToken); + var unreadCount = await _messageRepository.GetUnreadCountAsync(chat.Id, request.UserId, cancellationToken); dtos.Add(new ChatDto( - c.Id, - c.Type.ToString().ToLowerInvariant(), - c.Type == ChatType.Favorites ? "Избранное" : (c.Type == ChatType.Personal ? null : c.Name), - c.Description, - c.Avatar, - c.CreatedAt, + chat.Id, + chat.Type.ToString().ToLowerInvariant(), + chat.Type == ChatType.Favorites ? "Избранное" : (chat.Type == ChatType.Personal ? null : chat.Name), + chat.Description, + chat.Avatar, + chat.CreatedAt, members, messagesList, unreadCount @@ -146,3 +145,4 @@ internal sealed class GetChatsQueryHandler : IQueryHandler(); var replyMessages = new Dictionary(); - foreach (var m in messages) + // Filter messages that are deleted for the current user before processing + var filteredMessages = messages.Where(m => !m.IsDeletedForUser(request.UserId)).ToList(); + + foreach (var m in filteredMessages) { - if (m.DeletedByUsers.Contains(request.UserId)) + userIdsToFetch.Add(m.SenderId); + + if (!m.ReplyToId.HasValue) { continue; } - userIdsToFetch.Add(m.SenderId); - if (m.ForwardedFromId.HasValue) + var replyMsg = await _messageRepository.GetByIdAsync(m.ReplyToId.Value, cancellationToken); + if (replyMsg == null) { - userIdsToFetch.Add(m.ForwardedFromId.Value); + continue; } - foreach (var r in m.Reactions) - { - userIdsToFetch.Add(r.UserId); - } - - if (m.ReplyToId.HasValue) - { - var replyMsg = await _messageRepository.GetByIdAsync(m.ReplyToId.Value, cancellationToken); - if (replyMsg != null) - { - replyMessages[replyMsg.Id] = replyMsg; - userIdsToFetch.Add(replyMsg.SenderId); - } - } + replyMessages[replyMsg.Id] = replyMsg; + userIdsToFetch.Add(replyMsg.SenderId); } var senders = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); - foreach (var m in messages) + foreach (var message in messages) { - if (m.DeletedByUsers.Contains(request.UserId)) + if (message.IsDeletedForUser(request.UserId)) { continue; } ReplyToMessageDto? replyToObj = null; - if (m.ReplyToId.HasValue && replyMessages.TryGetValue(m.ReplyToId.Value, out var replyMsg)) + if (message.ReplyToId.HasValue && replyMessages.TryGetValue(message.ReplyToId.Value, out var replyMsg)) { var senderObj = senders.TryGetValue(replyMsg.SenderId, out var rs) ? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar) @@ -94,48 +87,48 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList(), + replyMsg.Content, + replyMsg.IsDeleted, + replyMsg.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList(), senderObj ); } var reactionsWithUser = new List(); - foreach (var r in m.Reactions) + foreach (var reaction in message.Reactions) { - var userObj = senders.TryGetValue(r.UserId, out var ru) - ? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar) - : new MessageSenderDto(r.UserId, "unknown", "Unknown", null); + var userObj = senders.TryGetValue(reaction.UserId, out var reactionUser) + ? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar) + : new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null); reactionsWithUser.Add(new MessageReactionDto( - r.Id, - r.Emoji, - r.UserId, + reaction.Id, + reaction.Emoji, + reaction.UserId, userObj )); } result.Add(new MessageDetailDto( - m.Id, - m.ChatId, - m.SenderId, - m.Content, - m.Type, - m.ReplyToId, + message.Id, + message.ChatId, + message.SenderId, + message.Content, + message.Type, + message.ReplyToId, replyToObj, - m.Quote, - m.IsEdited, - m.IsDeleted, - m.CreatedAt, - m.ForwardedFromId, - m.ForwardedFromId.HasValue && senders.TryGetValue(m.ForwardedFromId.Value, out var fwd) ? new MessageSenderDto(fwd.Id, fwd.Username, fwd.DisplayName, fwd.Avatar) : null, - m.StoryId, - m.StoryMediaUrl, - m.StoryMediaType, - m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), - senders.TryGetValue(m.SenderId, out var s) ? new MessageSenderDto(s.Id, s.Username, s.DisplayName, s.Avatar) : null, - m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList(), + message.Quote, + message.IsEdited, + message.IsDeleted, + message.CreatedAt, + 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.StoryId, + message.StoryMediaUrl, + message.StoryMediaType, + message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), + senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : null, + message.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList(), reactionsWithUser )); } @@ -143,3 +136,4 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler>> Handle(GetSharedMediaQuery request, CancellationToken cancellationToken) { var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); - if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) + if (chat == null || !chat.Members.Any(member => member.UserId == request.UserId)) { return Result.Failure>(ChatErrors.ChatsForbidden); } var messages = await _messageRepository.GetChatMessagesAsync(request.ChatId, ChatConstants.MaxSharedMediaQueryLimit, 0, cancellationToken); - messages = messages.Where(m => !m.IsDeleted && !m.DeletedByUsers.Contains(request.UserId)).ToList(); + messages = messages.Where(message => !message.IsDeletedForUser(request.UserId)).ToList(); var result = new List(); var filterType = request.Type?.ToLower(); - var userIds = messages.Select(m => m.SenderId).Distinct(); + var userIds = messages.Select(message => message.SenderId).Distinct(); var senders = await _userProvider.GetUsersInfoAsync(userIds, cancellationToken); - foreach (var m in messages) + foreach (var message in messages) { if (filterType == "links") { + var messageContent = message.Content; var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase); - var contentLinks = !string.IsNullOrEmpty(m.Content) ? linkRegex.Matches(m.Content).Select(match => match.Value).ToList() : new List(); - var mediaLinks = (m.Media ?? Enumerable.Empty()).Where(media => media.Type?.ToLower() == "link").Select(media => media.Url).ToList(); + var contentLinks = !string.IsNullOrEmpty(messageContent) ? linkRegex.Matches(messageContent).Select(match => match.Value).ToList() : new List(); + var messageMediaColl = message.Media; + var mediaLinks = (messageMediaColl ?? Enumerable.Empty()).Where(media => media.Type?.ToString().ToLower() == "link").Select(media => media.Url).ToList(); var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList(); if (allLinks.Any()) { - senders.TryGetValue(m.SenderId, out var sender); + senders.TryGetValue(message.SenderId, out var sender); result.Add(new SharedMediaDto( - m.Id, - m.Content, - m.CreatedAt, + message.Id, + messageContent, + message.CreatedAt, allLinks, sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null, null, null, null, null, null, null, null, null @@ -69,21 +71,17 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler + var filteredMedia = messageMedia.Where(media => { var mediaType = media.Type?.ToLower() ?? "file"; var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)); - if (filterType == "media") - { - return (mediaType == "image" || mediaType == "video") && !isGif; - } - if (filterType == "gifs") { return isGif; @@ -99,20 +97,20 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() )); } @@ -121,3 +119,4 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler x.CreatedAt).ToList()); } } + diff --git a/apps/server-net/src/Modules/Chats/Application/Messages/SearchMessages/SearchMessagesQuery.cs b/apps/server-net/src/Modules/Chats/Application/Messages/SearchMessages/SearchMessagesQuery.cs index f19cc7c..e332ec7 100644 --- a/apps/server-net/src/Modules/Chats/Application/Messages/SearchMessages/SearchMessagesQuery.cs +++ b/apps/server-net/src/Modules/Chats/Application/Messages/SearchMessages/SearchMessagesQuery.cs @@ -26,35 +26,36 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken) { var messages = await _messageRepository.SearchMessagesAsync(request.Query, request.ChatId, request.UserId, cancellationToken); - messages = messages.Where(m => !m.DeletedByUsers.Contains(request.UserId)).ToList(); + messages = messages.Where(message => !message.IsDeletedForUser(request.UserId)).ToList(); - var userIds = messages.Select(m => m.SenderId).ToList(); - userIds.AddRange(messages.Where(m => m.ForwardedFromId.HasValue).Select(m => m.ForwardedFromId!.Value)); + var userIds = messages.Select(message => message.SenderId).ToList(); + userIds.AddRange(messages.Where(message => message.ForwardedFromId.HasValue).Select(message => message.ForwardedFromId!.Value)); var senders = await _userProvider.GetUsersInfoAsync(userIds.Distinct(), cancellationToken); - var result = messages.Select(m => new SearchMessageDto( - m.Id, - m.ChatId, - m.SenderId, - m.Content, - m.Type, - m.ReplyToId, - m.Quote, - m.IsEdited, - m.IsDeleted, - m.CreatedAt, - m.ForwardedFromId, - m.ForwardedFromId.HasValue && senders.TryGetValue(m.ForwardedFromId.Value, out var fwd) ? new MessageSenderDto(fwd.Id, fwd.Username, fwd.DisplayName, fwd.Avatar) : null, - m.StoryId, - m.StoryMediaUrl, - m.StoryMediaType, - m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), - senders.TryGetValue(m.SenderId, out var s) ? new MessageSenderDto(s.Id, s.Username, s.DisplayName, s.Avatar) : new MessageSenderDto(m.SenderId, "unknown", "Unknown", null), - m.Reactions.Select(r => new SimpleReactionDto(r.UserId, r.Emoji)).ToList(), - m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList() + var result = messages.Select(message => new SearchMessageDto( + message.Id, + message.ChatId, + message.SenderId, + message.Content, + message.Type, + message.ReplyToId, + message.Quote, + message.IsEdited, + message.IsDeleted, + message.CreatedAt, + message.ForwardedFromId, + null, + message.StoryId, + message.StoryMediaUrl, + message.StoryMediaType, + message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), + senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null), + message.Reactions.Select(reaction => new SimpleReactionDto(reaction.UserId, reaction.Emoji)).ToList(), + message.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList() )).ToList(); return Result.Success(result); } } + diff --git a/apps/server-net/src/Modules/Chats/Application/Messages/Send/SendMessageCommandHandler.cs b/apps/server-net/src/Modules/Chats/Application/Messages/Send/SendMessageCommandHandler.cs index bb7adc2..aa14891 100644 --- a/apps/server-net/src/Modules/Chats/Application/Messages/Send/SendMessageCommandHandler.cs +++ b/apps/server-net/src/Modules/Chats/Application/Messages/Send/SendMessageCommandHandler.cs @@ -27,15 +27,18 @@ public sealed class SendMessageCommandHandler : ICommandHandler> Handle(SendMessageCommand request, CancellationToken cancellationToken) @@ -54,30 +57,71 @@ public sealed class SendMessageCommandHandler : ICommandHandler(request.StoryMediaType, true, out var sTypeEnum) ? sTypeEnum : MediaType.Image; + message = new StoryMessage( + Guid.NewGuid(), + request.ChatId, + request.SenderId, + request.StoryId ?? Guid.Empty, + request.StoryMediaUrl ?? string.Empty, + parsedStoryMediaType, + request.Content, + request.ReplyToId, + request.ForwardedFromId, + DateTime.UtcNow, + false); + } + else if (request.Attachments != null && request.Attachments.Any()) + { + var firstAtt = request.Attachments.First(); + var parsedType = Enum.TryParse(firstAtt.Type, true, out var mTypeEnum) ? mTypeEnum : MediaType.File; + + message = new MediaMessage( + Guid.NewGuid(), + request.ChatId, + request.SenderId, + parsedType, + request.Content, + request.ReplyToId, + request.ForwardedFromId, + DateTime.UtcNow, + false); + foreach (var att in request.Attachments) { - message.AddMedia(att.Type, att.Url, att.FileName, att.FileSize); + var pType = Enum.TryParse(att.Type, true, out var tEnum) ? tEnum : MediaType.File; + ((MediaMessage)message).AddMedia(pType, att.Url, att.FileName, att.FileSize); } } + else + { + message = new TextMessage( + Guid.NewGuid(), + request.ChatId, + request.SenderId, + request.Content ?? string.Empty, + request.ReplyToId, + request.Quote, + request.ForwardedFromId, + DateTime.UtcNow, + false); + } // 4. Сохраняем _messageRepository.Add(message); await _unitOfWork.SaveChangesAsync(cancellationToken); + await _mediator.Publish(new MessageSentDomainEvent( + message.Id, + message.ChatId, + message.SenderId, + message.Content), + cancellationToken); + return Result.Success(message.Id); } } + diff --git a/apps/server-net/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs b/apps/server-net/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs index b1d5638..b98d81a 100644 --- a/apps/server-net/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs +++ b/apps/server-net/src/Modules/Chats/Application/TelegramImport/ExecuteImportCommand.cs @@ -350,16 +350,24 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler 0; Message? targetMessage = null; - if (isJoined && isMediaOnly && lastSavedMessage != null && Math.Abs((createdAt - lastSavedMessage.CreatedAt).TotalSeconds) <= 60 && lastSavedMessage.SenderId == senderGuid) + if (isJoined && isMediaOnly && lastSavedMessage is MediaMessage && Math.Abs((createdAt - lastSavedMessage.CreatedAt).TotalSeconds) <= 60 && lastSavedMessage.SenderId == senderGuid) { targetMessage = lastSavedMessage; } else { string finalContent = content; - targetMessage = Message.Import(chatId, senderGuid, finalContent, messageType, createdAt, replyToId, forwardedFromId); + if (hasMedia) + { + targetMessage = new MediaMessage(Guid.NewGuid(), chatId, senderGuid, MediaType.File, finalContent, replyToId, forwardedFromId, createdAt, true); + } + else + { + targetMessage = new TextMessage(Guid.NewGuid(), chatId, senderGuid, finalContent, replyToId, null, forwardedFromId, createdAt, true); + } var idAttr = node.GetAttribute("id"); if (!string.IsNullOrEmpty(idAttr)) @@ -368,9 +376,9 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler(finalMType, true, out var mTypeEnum) ? mTypeEnum : MediaType.File; + if (targetMessage is MediaMessage mm) + { + var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType); + mm.AddMedia(parsedType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length); + } } } } @@ -440,3 +452,4 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler(options => options.UseNpgsql(connectionString)); - // Регистрация Unit of Work и Репозиториев + // MongoDB Setup for Messages + MongoDbMapConfigurator.Configure(); + + var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017"; + services.AddSingleton(new MongoClient(mongoConnectionString)); + services.AddScoped(sp => + sp.GetRequiredService().GetDatabase("forkmessager_chats")); + + // Registration services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); - // Регистрация MediatR для этого модуля + // MediatR services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); diff --git a/apps/server-net/src/Modules/Chats/Domain/DeletedMessage.cs b/apps/server-net/src/Modules/Chats/Domain/DeletedMessage.cs new file mode 100644 index 0000000..d58c9ac --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/DeletedMessage.cs @@ -0,0 +1,21 @@ +using System; +using Knot.Shared.Kernel; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Запись о том, что конкретный пользователь удалил у себя сообщение. +/// +public sealed class DeletedMessage : Entity +{ + public Guid MessageId { get; private set; } + public Guid UserId { get; private set; } + + internal DeletedMessage(Guid messageId, Guid userId) : base(Guid.NewGuid()) + { + MessageId = messageId; + UserId = userId; + } + + private DeletedMessage() : base(Guid.Empty) { } +} diff --git a/apps/server-net/src/Modules/Chats/Domain/IMessageRepository.cs b/apps/server-net/src/Modules/Chats/Domain/IMessageRepository.cs index 2f82d65..3c02423 100644 --- a/apps/server-net/src/Modules/Chats/Domain/IMessageRepository.cs +++ b/apps/server-net/src/Modules/Chats/Domain/IMessageRepository.cs @@ -7,6 +7,7 @@ public interface IMessageRepository void Add(Message message); Task GetByIdAsync(Guid id, CancellationToken cancellationToken); Task> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken); + Task GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken); Task> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken); Task AddReadReceiptsAsync(Guid userId, List messageIds, CancellationToken cancellationToken); Task AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken); @@ -14,4 +15,5 @@ public interface IMessageRepository Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken); Task GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken); Task GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken); + Task UpdateAsync(Message message, CancellationToken cancellationToken); } diff --git a/apps/server-net/src/Modules/Chats/Domain/Media.cs b/apps/server-net/src/Modules/Chats/Domain/Media.cs new file mode 100644 index 0000000..64f83eb --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/Media.cs @@ -0,0 +1,31 @@ +using System; +using Knot.Shared.Kernel; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Медиа-файл, прикрепленный к сообщению. +/// +public sealed class Media : Entity +{ + public Guid MessageId { get; private set; } + public string Type { get; private set; } + public string Url { get; private set; } + public string? Filename { get; private set; } + public long? Size { get; private set; } + + internal Media(Guid messageId, string type, string url, string? filename, long? size) : base(Guid.NewGuid()) + { + MessageId = messageId; + Type = type; + Url = url; + Filename = filename; + Size = size; + } + + private Media() : base(Guid.Empty) + { + Type = string.Empty; + Url = string.Empty; + } +} diff --git a/apps/server-net/src/Modules/Chats/Domain/MediaMessage.cs b/apps/server-net/src/Modules/Chats/Domain/MediaMessage.cs new file mode 100644 index 0000000..467eb27 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/MediaMessage.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace Knot.Modules.Chats.Domain; + +public class MediaMessage : Message +{ + public override string Type => MediaType.ToString().ToLower(); + public override string? Content { get; protected set; } // Map to Caption + public string? Caption { get => Content; private set => Content = value; } + public MediaType MediaType { get; private set; } // image, video, file, voice + + private readonly List _media = new(); + public override IReadOnlyCollection Media => _media.AsReadOnly(); + + private MediaMessage() : base() + { + MediaType = MediaType.File; + } + + public MediaMessage( + Guid id, + Guid chatId, + Guid senderId, + MediaType mediaType, + string? caption, + Guid? replyToId, + Guid? forwardedFromId, + DateTime createdAt, + bool isImported) + : base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) + { + MediaType = mediaType; + Caption = caption; + + if (!isImported) + { + RaiseDomainEvent(new MessageSentDomainEvent(Id, ChatId, SenderId, Caption)); + } + } + + public void AddMedia(MediaType type, string url, string? filename, long? size) + { + _media.Add(new Domain.Media(Id, type.ToString().ToLower(), url, filename, size)); + } + + public void Edit(string newCaption) + { + Caption = newCaption; + AddState(MessageState.IsEdited); + } + + public override void Delete() + { + Caption = null; + base.Delete(); + } +} diff --git a/apps/server-net/src/Modules/Chats/Domain/MediaType.cs b/apps/server-net/src/Modules/Chats/Domain/MediaType.cs new file mode 100644 index 0000000..29f41d7 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/MediaType.cs @@ -0,0 +1,12 @@ +namespace Knot.Modules.Chats.Domain; + +/// +/// Тип медиа-контента. +/// +public enum MediaType +{ + Image, + Video, + Voice, + File +} diff --git a/apps/server-net/src/Modules/Chats/Domain/Message.cs b/apps/server-net/src/Modules/Chats/Domain/Message.cs index ad62c34..8318245 100644 --- a/apps/server-net/src/Modules/Chats/Domain/Message.cs +++ b/apps/server-net/src/Modules/Chats/Domain/Message.cs @@ -1,89 +1,77 @@ +using System; +using System.Collections.Generic; using Knot.Shared.Kernel; namespace Knot.Modules.Chats.Domain; /// -/// Доменное событие: сообщение отправлено. +/// Абстрактная база агрегата Сообщение. /// -public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent; - -/// -/// Сущность сообщения (Агрегат). -/// -public sealed class Message : AggregateRoot +public abstract class Message : AggregateRoot { - public Guid ChatId { get; private set; } - public Guid SenderId { get; private set; } - public string? Content { get; private set; } - public string Type { get; private set; } // text, image, video, voice, file - public Guid? ReplyToId { get; private set; } - public string? Quote { get; private set; } - public bool IsEdited { get; private set; } - public bool IsDeleted { get; private set; } - public Guid? ForwardedFromId { get; private set; } - public Guid? StoryId { get; private set; } - public string? StoryMediaUrl { get; private set; } - public string? StoryMediaType { get; private set; } - public DateTime CreatedAt { get; private set; } - public bool IsImported { get; private set; } + // ================== Базовые поля ================== + public Guid ChatId { get; protected set; } + public Guid SenderId { get; protected set; } + public DateTime CreatedAt { get; protected set; } + + // ================== Опциональные метаданные (общего назначения) ================== + public Guid? ReplyToId { get; protected set; } + public Guid? ForwardedFromId { get; protected set; } + + // ================== Флаги ================== + public MessageState State { get; protected set; } + + // ================== Абстрактные / Виртуальные свойства ================== + public abstract string Type { get; } + public abstract string? Content { get; protected set; } - private readonly List _media = new(); - public IReadOnlyCollection Media => _media.AsReadOnly(); + public virtual string? Quote { get; protected set; } = null; + public virtual Guid? StoryId => null; + public virtual string? StoryMediaUrl => null; + public virtual string? StoryMediaType => null; + public virtual IReadOnlyCollection Media => Array.Empty(); - private readonly List _readBy = new(); + public bool IsEdited => HasState(MessageState.IsEdited); + public bool IsDeleted => HasState(MessageState.IsDeleted); + public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId); + + // ================== Связанные коллекции (общего назначения) ================== + protected readonly List _readBy = new(); public IReadOnlyCollection ReadBy => _readBy.AsReadOnly(); - private readonly List _deletedByUsers = new(); - public IReadOnlyCollection DeletedByUsers => _deletedByUsers.AsReadOnly(); + protected readonly List _deletedFor = new(); + public IReadOnlyCollection DeletedFor => _deletedFor.AsReadOnly(); - private Message() : base(Guid.Empty) { Type = "text"; } + protected readonly List _reactions = new(); + public IReadOnlyCollection Reactions => _reactions.AsReadOnly(); - private Message(Guid id, Guid chatId, Guid senderId, string? content, string type, Guid? replyToId, string? quote, Guid? forwardedFromId, Guid? storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt, bool isImported) : base(id) + // ================== Инфраструктурный конструктор EF ================== + protected Message() : base(Guid.Empty) { } + + protected Message( + Guid id, + Guid chatId, + Guid senderId, + Guid? replyToId, + Guid? forwardedFromId, + DateTime createdAt, + bool isImported) : base(id) { ChatId = chatId; SenderId = senderId; - Content = content; - Type = type; ReplyToId = replyToId; - Quote = quote; ForwardedFromId = forwardedFromId; - StoryId = storyId; - StoryMediaUrl = storyMediaUrl; - StoryMediaType = storyMediaType; CreatedAt = createdAt; - IsImported = isImported; - - if (!isImported) // Don't trigger realtime events for historic messages - { - RaiseDomainEvent(new MessageSentDomainEvent(Id, ChatId, SenderId, Content)); - } + + if (isImported) AddState(MessageState.IsImported); } - public static Message Create(Guid chatId, Guid senderId, string? content, string type, Guid? replyToId = null, string? quote = null, Guid? forwardedFromId = null, Guid? storyId = null, string? storyMediaUrl = null, string? storyMediaType = null) - { - return new Message(Guid.NewGuid(), chatId, senderId, content, type, replyToId, quote, forwardedFromId, storyId, storyMediaUrl, storyMediaType, DateTime.UtcNow, false); - } - - public static Message Import( - Guid chatId, - Guid senderId, - string? content, - string type, - DateTime createdAt, - Guid? replyToId = null, - Guid? forwardedFromId = null) - { - return new Message(Guid.NewGuid(), chatId, senderId, content, type, replyToId, null, forwardedFromId, null, null, null, createdAt, true); - } - - public void AddMedia(string type, string url, string? filename, long? size) - { - _media.Add(new Media(Id, type, url, filename, size)); - } - - private readonly List _reactions = new(); - public IReadOnlyCollection Reactions => _reactions.AsReadOnly(); + // ================== Управление Состоянием ================== + public void AddState(MessageState state) => State |= state; + public void RemoveState(MessageState state) => State &= ~state; + public bool HasState(MessageState state) => (State & state) == state; + // ================== Общие операции ================== public void AddReaction(Guid userId, string emoji) { var existing = _reactions.Find(r => r.UserId == userId && r.Emoji == emoji); @@ -102,73 +90,17 @@ public sealed class Message : AggregateRoot } } - public void Edit(string newContent) + public virtual void Delete() { - Content = newContent; - IsEdited = true; - } - - public void Delete() - { - Content = null; - IsDeleted = true; - // _media.Clear(); // DO NOT CLEAR! Data cleanup needs to know the URLs to delete from S3 + AddState(MessageState.IsDeleted); _reactions.Clear(); } public void DeleteForUser(Guid userId) { - if (!_deletedByUsers.Contains(userId)) + if (!_deletedFor.Exists(x => x.UserId == userId)) { - _deletedByUsers.Add(userId); + _deletedFor.Add(new DeletedMessage(Id, userId)); } } } -/// -/// Реакция на сообщение. -/// -public sealed class Reaction : Entity -{ - public Guid MessageId { get; private set; } - public Guid UserId { get; private set; } - public string Emoji { get; private set; } - - internal Reaction(Guid messageId, Guid userId, string emoji) : base(Guid.NewGuid()) - { - MessageId = messageId; - UserId = userId; - Emoji = emoji; - } - - private Reaction() : base(Guid.Empty) - { - Emoji = string.Empty; - } -} - -/// -/// Медиа-файл, прикрепленный к сообщению. -/// -public sealed class Media : Entity -{ - public Guid MessageId { get; private set; } - public string Type { get; private set; } - public string Url { get; private set; } - public string? Filename { get; private set; } - public long? Size { get; private set; } - - internal Media(Guid messageId, string type, string url, string? filename, long? size) : base(Guid.NewGuid()) - { - MessageId = messageId; - Type = type; - Url = url; - Filename = filename; - Size = size; - } - - private Media() : base(Guid.Empty) - { - Type = string.Empty; - Url = string.Empty; - } -} diff --git a/apps/server-net/src/Modules/Chats/Domain/MessageFlags.cs b/apps/server-net/src/Modules/Chats/Domain/MessageFlags.cs new file mode 100644 index 0000000..1fcfc6c --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/MessageFlags.cs @@ -0,0 +1,16 @@ +using System; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Флаги состояния сообщения +/// +[Flags] +public enum MessageFlags +{ + None = 0, + IsEdited = 1, + IsDeleted = 2, + IsImported = 4, + IsPinned = 8 +} diff --git a/apps/server-net/src/Modules/Chats/Domain/MessageSentDomainEvent.cs b/apps/server-net/src/Modules/Chats/Domain/MessageSentDomainEvent.cs new file mode 100644 index 0000000..dabe511 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/MessageSentDomainEvent.cs @@ -0,0 +1,9 @@ +using System; +using Knot.Shared.Kernel; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Доменное событие: сообщение отправлено. +/// +public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent; diff --git a/apps/server-net/src/Modules/Chats/Domain/MessageState.cs b/apps/server-net/src/Modules/Chats/Domain/MessageState.cs new file mode 100644 index 0000000..efae2ba --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/MessageState.cs @@ -0,0 +1,16 @@ +using System; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Состояние сообщения +/// +[Flags] +public enum MessageState +{ + None = 0, + IsEdited = 1, + IsDeleted = 2, + IsImported = 4, + IsPinned = 8 +} diff --git a/apps/server-net/src/Modules/Chats/Domain/Reaction.cs b/apps/server-net/src/Modules/Chats/Domain/Reaction.cs new file mode 100644 index 0000000..637e70c --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/Reaction.cs @@ -0,0 +1,26 @@ +using System; +using Knot.Shared.Kernel; + +namespace Knot.Modules.Chats.Domain; + +/// +/// Реакция на сообщение. +/// +public sealed class Reaction : Entity +{ + public Guid MessageId { get; private set; } + public Guid UserId { get; private set; } + public string Emoji { get; private set; } + + internal Reaction(Guid messageId, Guid userId, string emoji) : base(Guid.NewGuid()) + { + MessageId = messageId; + UserId = userId; + Emoji = emoji; + } + + private Reaction() : base(Guid.Empty) + { + Emoji = string.Empty; + } +} diff --git a/apps/server-net/src/Modules/Chats/Domain/StoryMessage.cs b/apps/server-net/src/Modules/Chats/Domain/StoryMessage.cs new file mode 100644 index 0000000..154e1cb --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/StoryMessage.cs @@ -0,0 +1,55 @@ +using System; + +namespace Knot.Modules.Chats.Domain; + +public class StoryMessage : Message +{ + public override string Type => "story"; + public override string? Content { get; protected set; } + + public Guid InternalStoryId { get; private set; } + public override Guid? StoryId => InternalStoryId; + + public string InternalStoryMediaUrl { get; private set; } + public override string? StoryMediaUrl => InternalStoryMediaUrl; + + public MediaType InternalStoryMediaType { get; private set; } + public override string? StoryMediaType => InternalStoryMediaType.ToString().ToLower(); + + private StoryMessage() : base() + { + InternalStoryMediaUrl = string.Empty; + InternalStoryMediaType = MediaType.Image; + } + + public StoryMessage( + Guid id, + Guid chatId, + Guid senderId, + Guid storyId, + string storyMediaUrl, + MediaType storyMediaType, + string? content, + Guid? replyToId, + Guid? forwardedFromId, + DateTime createdAt, + bool isImported) + : base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) + { + InternalStoryId = storyId; + InternalStoryMediaUrl = storyMediaUrl; + InternalStoryMediaType = storyMediaType; + Content = content; + + if (!isImported) + { + RaiseDomainEvent(new MessageSentDomainEvent(Id, ChatId, SenderId, Content)); + } + } + + public override void Delete() + { + Content = null; + base.Delete(); + } +} diff --git a/apps/server-net/src/Modules/Chats/Domain/TextMessage.cs b/apps/server-net/src/Modules/Chats/Domain/TextMessage.cs new file mode 100644 index 0000000..d424fa3 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Domain/TextMessage.cs @@ -0,0 +1,48 @@ +using System; + +namespace Knot.Modules.Chats.Domain; + +public class TextMessage : Message +{ + public override string Type => "text"; + public override string? Content { get; protected set; } + public override string? Quote { get; protected set; } + + private TextMessage() : base() + { + Content = string.Empty; + } + + public TextMessage( + Guid id, + Guid chatId, + Guid senderId, + string content, + Guid? replyToId, + string? quote, + Guid? forwardedFromId, + DateTime createdAt, + bool isImported) + : base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) + { + Content = content; + Quote = quote; + + if (!isImported) + { + RaiseDomainEvent(new MessageSentDomainEvent(Id, ChatId, SenderId, Content)); + } + } + + public void Edit(string newContent) + { + Content = newContent; + AddState(MessageState.IsEdited); + } + + public override void Delete() + { + Content = string.Empty; + base.Delete(); + } +} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs index 7f42e7b..7d7ee0f 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs @@ -77,14 +77,14 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler new { m.Id, @@ -95,9 +95,10 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler(), - message.StoryId, - message.StoryMediaUrl, - message.StoryMediaType + StoryId = message.StoryId, + StoryMediaUrl = message.StoryMediaUrl, + StoryMediaType = message.StoryMediaType }, cancellationToken); } } + diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/ChatsDbContext.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/ChatsDbContext.cs index 4af77d8..abd0289 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/ChatsDbContext.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/ChatsDbContext.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Knot.Modules.Chats.Application.Abstractions; using Knot.Modules.Chats.Domain; using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Security; namespace Knot.Modules.Chats.Infrastructure.Persistence; @@ -13,9 +14,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence; public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork { private readonly IMediator _mediator; - private readonly Knot.Shared.Kernel.Security.IEncryptionService _encryptionService; + private readonly IEncryptionService _encryptionService; - public ChatsDbContext(DbContextOptions options, IMediator mediator, Knot.Shared.Kernel.Security.IEncryptionService encryptionService) + public ChatsDbContext(DbContextOptions options, IMediator mediator, IEncryptionService encryptionService) : base(options) { _mediator = mediator; @@ -23,9 +24,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork } public DbSet Chats => Set(); - public DbSet Messages => Set(); - public DbSet ReadReceipts => Set(); - public DbSet Reactions => Set(); + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -54,61 +53,6 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork }).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field); }); - modelBuilder.Entity(builder => - { - builder.ToTable("Messages"); - builder.HasKey(m => m.Id); - builder.HasIndex(m => m.ChatId); - builder.HasIndex(m => new { m.ChatId, m.CreatedAt }); - - builder.Property(m => m.Content) - .HasConversion( - v => v == null ? null : _encryptionService.EncryptMessage(v), - v => v == null ? null : _encryptionService.DecryptMessage(v) - ); - - - builder.OwnsMany(m => m.Media, mb => - { - mb.ToTable("MessageMedia"); - mb.WithOwner().HasForeignKey(x => x.MessageId); - }).Navigation(m => m.Media).UsePropertyAccessMode(PropertyAccessMode.Field); - - builder.HasMany(m => m.Reactions) - .WithOne() - .HasForeignKey(x => x.MessageId) - .OnDelete(DeleteBehavior.Cascade); - - - builder.Navigation(m => m.Reactions).UsePropertyAccessMode(PropertyAccessMode.Field); - - builder.PrimitiveCollection(m => m.DeletedByUsers) - .HasColumnName("DeletedByUsers") - .UsePropertyAccessMode(PropertyAccessMode.Field); - - - builder.HasMany(m => m.ReadBy) - .WithOne() - .HasForeignKey(r => r.MessageId) - .OnDelete(DeleteBehavior.Cascade); - - - builder.Navigation(m => m.ReadBy).UsePropertyAccessMode(PropertyAccessMode.Field); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("MessageReactions"); - builder.HasKey(r => r.Id); - builder.HasIndex(r => new { r.MessageId, r.UserId, r.Emoji }).IsUnique(); - }); - - modelBuilder.Entity(builder => - { - builder.ToTable("ReadReceipts"); - builder.HasKey(r => r.Id); - builder.HasIndex(r => new { r.MessageId, r.UserId }).IsUnique(); - }); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/MessageRepository.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/MessageRepository.cs index 9925d96..9ca74c1 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/MessageRepository.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/MessageRepository.cs @@ -1,162 +1,207 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; +using MongoDB.Driver.Linq; using Knot.Modules.Chats.Domain; +using Knot.Shared.Kernel; +using System.Text.RegularExpressions; +using MongoDB.Bson; namespace Knot.Modules.Chats.Infrastructure.Persistence; public sealed class MessageRepository : IMessageRepository { + private readonly IMongoCollection _messages; private readonly ChatsDbContext _dbContext; + private readonly MediatR.IMediator _mediator; - public MessageRepository(ChatsDbContext dbContext) + public MessageRepository(IMongoDatabase mongoDatabase, ChatsDbContext dbContext, MediatR.IMediator mediator) { + _messages = mongoDatabase.GetCollection("messages"); _dbContext = dbContext; + _mediator = mediator; } public void Add(Message message) { - _dbContext.Messages.Add(message); + _messages.InsertOne(message); + + // Publish domain events manualy for mongo entities + var events = message.GetDomainEvents().ToList(); + message.ClearDomainEvents(); + + // This runs synchronously or without waiting, better to run async but Add is void + // In this implementation setting, fire and forget or wrap sync + foreach (var domainEvent in events) + { + _mediator.Publish(domainEvent).GetAwaiter().GetResult(); + } } public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) { - return await _dbContext.Messages - .Include(m => m.Media) - .Include(m => m.ReadBy) - .Include(m => m.Reactions) - .FirstOrDefaultAsync(m => m.Id == id, cancellationToken); + var filter = Builders.Filter.Eq(m => m.Id, id); + return await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken); } public async Task> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken) { - return await _dbContext.Messages - .Where(m => m.ChatId == chatId) - .OrderByDescending(m => m.CreatedAt) + var filter = Builders.Filter.Eq(m => m.ChatId, chatId); + return await _messages.Find(filter) + .SortByDescending(m => m.CreatedAt) .Skip(offset) - .Take(limit) - .Include(m => m.Media) - .Include(m => m.ReadBy) - .Include(m => m.Reactions) + .Limit(limit) .ToListAsync(cancellationToken); } + public async Task GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken) + { + var filter = Builders.Filter.Eq(m => m.ChatId, chatId); + return await _messages.Find(filter) + .SortByDescending(m => m.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); + } + public async Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken) { - var query = _dbContext.Messages - .Where(m => m.ChatId == chatId); - + var builder = Builders.Filter; + var filter = builder.Eq(m => m.ChatId, chatId); + if (cursor.HasValue) { - query = query.Where(m => m.CreatedAt < cursor.Value); + filter &= builder.Lt(m => m.CreatedAt, cursor.Value); } - return await query - .OrderByDescending(m => m.CreatedAt) - .Take(limit) - .Include(m => m.Media) - .Include(m => m.ReadBy) - .Include(m => m.Reactions) + return await _messages.Find(filter) + .SortByDescending(m => m.CreatedAt) + .Limit(limit) .ToListAsync(cancellationToken); } public async Task> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken) { + // Not ideal for SQL/Mongo combination but keeping the signature var validChatIdsQuery = _dbContext.Chats .Where(c => c.Members.Any(m => m.UserId == requestingUserId)) - .Select(c => c.Id); + .Select(c => c.Id) + .ToList(); - var q = _dbContext.Messages.Where(m => validChatIdsQuery.Contains(m.ChatId)); + var builder = Builders.Filter; + var filter = builder.In(m => m.ChatId, validChatIdsQuery); if (chatId.HasValue) { - q = q.Where(m => m.ChatId == chatId.Value); + filter &= builder.Eq(m => m.ChatId, chatId.Value); } + var textFilter = Builders.Filter.Regex("Content", new BsonRegularExpression(Regex.Escape(query), "i")); + filter &= textFilter; - return await q.Where(m => m.Content != null && m.Content.Contains(query)) - .OrderByDescending(m => m.CreatedAt) - .Take(ChatConstants.SearchMessagesLimit) - .Include(m => m.Media) - .Include(m => m.ReadBy) - .Include(m => m.Reactions) + return await _messages.Find(filter) + .SortByDescending(m => m.CreatedAt) + .Limit(ChatConstants.SearchMessagesLimit) .ToListAsync(cancellationToken); } public async Task AddReadReceiptsAsync(Guid userId, List messageIds, CancellationToken cancellationToken) { - var existingReceipts = await _dbContext.ReadReceipts - .Where(r => r.UserId == userId && messageIds.Contains(r.MessageId)) - .Select(r => r.MessageId) - .ToListAsync(cancellationToken); + var filter = Builders.Filter.In(m => m.Id, messageIds); - var newReceipts = messageIds - .Except(existingReceipts) - .Select(id => new ReadReceipt(id, userId)); + var messages = await _messages.Find(filter).ToListAsync(cancellationToken); + + var writes = new List>(); + foreach (var msg in messages) + { + if (!msg.ReadBy.Any(r => r.UserId == userId)) + { + var receipt = new ReadReceipt(msg.Id, userId); + var pushUpdate = Builders.Update.Push("ReadBy", receipt); + var updateModel = new UpdateOneModel(Builders.Filter.Eq(m => m.Id, msg.Id), pushUpdate); + writes.Add(updateModel); + } + } - _dbContext.ReadReceipts.AddRange(newReceipts); - // SaveChangesAsync будет вызван в handlers или через UnitOfWork, но если мы здесь - await _dbContext.SaveChangesAsync(cancellationToken); + if (writes.Any()) + { + await _messages.BulkWriteAsync(writes, cancellationToken: cancellationToken); + } } public async Task AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken) { - // Проверяем, существует ли сообщение - var messageExists = await _dbContext.Messages - .AnyAsync(m => m.Id == messageId, cancellationToken); + var filter = Builders.Filter.Eq(m => m.Id, messageId); + var msg = await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken); + if (msg == null) return false; - - if (!messageExists) + if (msg.Reactions.Any(r => r.UserId == userId && r.Emoji == emoji)) { - return false; + return true; } - // Проверяем, есть ли уже такая реакция - var existingReaction = await _dbContext.Reactions - .FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken); - - - if (existingReaction != null) - { - return true; // Уже существует - } - - // Добавляем новую реакцию напрямую var reaction = new Reaction(messageId, userId, emoji); - _dbContext.Reactions.Add(reaction); - - + var update = Builders.Update.Push("Reactions", reaction); + await _messages.UpdateOneAsync(filter, update, cancellationToken: cancellationToken); return true; } public async Task RemoveReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken) { - // Ищем реакцию - var reaction = await _dbContext.Reactions - .FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken); - - - if (reaction == null) - { - return false; - } - - _dbContext.Reactions.Remove(reaction); + var filter = Builders.Filter.Eq(m => m.Id, messageId); + var msg = await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken); + if (msg == null) return false; + var reaction = msg.Reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji); + if (reaction == null) return false; + var update = Builders.Update.PullFilter("Reactions", + Builders.Filter.And( + Builders.Filter.Eq("UserId", userId), + Builders.Filter.Eq("Emoji", emoji) + )); + + await _messages.UpdateOneAsync(filter, update, cancellationToken: cancellationToken); return true; } public async Task GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken) { - return await _dbContext.Messages - .Where(m => m.ChatId == chatId && m.StoryId == storyId) - .OrderByDescending(m => m.CreatedAt) + var filter = Builders.Filter.And( + Builders.Filter.Eq(m => m.ChatId, chatId), + Builders.Filter.Eq("_t", "StoryMessage"), + Builders.Filter.Eq("StoryId", storyId) + ); + + return await _messages.Find(filter) + .SortByDescending(m => m.CreatedAt) .FirstOrDefaultAsync(cancellationToken); } public async Task GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken) { - return await _dbContext.Messages - .Where(m => m.ChatId == chatId && m.SenderId != userId && !m.ReadBy.Any(r => r.UserId == userId)) - .CountAsync(cancellationToken); + var notReadFilter = Builders.Filter.Not( + Builders.Filter.ElemMatch("ReadBy", + Builders.Filter.Eq("UserId", userId)) + ); + + var finalFilter = Builders.Filter.And( + Builders.Filter.Eq(m => m.ChatId, chatId), + Builders.Filter.Ne(m => m.SenderId, userId), + notReadFilter + ); + + return (int)await _messages.CountDocumentsAsync(finalFilter, cancellationToken: cancellationToken); + } + + public async Task UpdateAsync(Message message, CancellationToken cancellationToken) + { + var filter = Builders.Filter.Eq(m => m.Id, message.Id); + await _messages.ReplaceOneAsync(filter, message, new ReplaceOptions { IsUpsert = true }, cancellationToken); + + // Publish domain events + var events = message.GetDomainEvents().ToList(); + message.ClearDomainEvents(); + foreach (var domainEvent in events) + { + await _mediator.Publish(domainEvent, cancellationToken); + } } } diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs deleted file mode 100644 index 1a65891..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.Designer.cs +++ /dev/null @@ -1,207 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260311215929_UpdateChatModel")] - partial class UpdateChatModel - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs deleted file mode 100644 index 0897202..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311215929_UpdateChatModel.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class UpdateChatModel : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ReadReceipts", - schema: "chats", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MessageId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - ReadAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ReadReceipts", x => x.Id); - table.ForeignKey( - name: "FK_ReadReceipts_Messages_MessageId", - column: x => x.MessageId, - principalSchema: "chats", - principalTable: "Messages", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ReadReceipts_MessageId_UserId", - schema: "chats", - table: "ReadReceipts", - columns: new[] { "MessageId", "UserId" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ReadReceipts", - schema: "chats"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs deleted file mode 100644 index fc681e9..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.Designer.cs +++ /dev/null @@ -1,210 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260312094130_AddForwardedFromToMessages")] - partial class AddForwardedFromToMessages - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs deleted file mode 100644 index 24c7894..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312094130_AddForwardedFromToMessages.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddForwardedFromToMessages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ForwardedFromId", - schema: "chats", - table: "Messages", - type: "uuid", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ForwardedFromId", - schema: "chats", - table: "Messages"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs deleted file mode 100644 index 880b952..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.Designer.cs +++ /dev/null @@ -1,239 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260312100324_AddReactionsToMessages")] - partial class AddReactionsToMessages - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.OwnsMany("Knot.Modules.Chats.Domain.Reaction", "Reactions", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b1.ToTable("MessageReactions", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - - b.Navigation("Reactions"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs deleted file mode 100644 index 409f229..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312100324_AddReactionsToMessages.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddReactionsToMessages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "MessageReactions", - schema: "chats", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MessageId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Emoji = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MessageReactions", x => x.Id); - table.ForeignKey( - name: "FK_MessageReactions_Messages_MessageId", - column: x => x.MessageId, - principalSchema: "chats", - principalTable: "Messages", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_MessageReactions_MessageId_UserId_Emoji", - schema: "chats", - table: "MessageReactions", - columns: new[] { "MessageId", "UserId", "Emoji" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "MessageReactions", - schema: "chats"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs deleted file mode 100644 index 67f4e52..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.Designer.cs +++ /dev/null @@ -1,244 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260312174910_UpdateChatsSchema")] - partial class UpdateChatsSchema - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.OwnsMany("Knot.Modules.Chats.Domain.Reaction", "Reactions", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b1.ToTable("MessageReactions", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - - b.Navigation("Reactions"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs deleted file mode 100644 index 59c4da4..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312174910_UpdateChatsSchema.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class UpdateChatsSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DeletedByUsers", - schema: "chats", - table: "Messages", - type: "uuid[]", - nullable: false, - defaultValue: new Guid[0]); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DeletedByUsers", - schema: "chats", - table: "Messages"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs deleted file mode 100644 index 92d4049..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.Designer.cs +++ /dev/null @@ -1,251 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260312204842_SupportPinningAndMuting")] - partial class SupportPinningAndMuting - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs deleted file mode 100644 index df87a35..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260312204842_SupportPinningAndMuting.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class SupportPinningAndMuting : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_MessageMedia", - schema: "chats", - table: "MessageMedia"); - - migrationBuilder.DropIndex( - name: "IX_MessageMedia_MessageId", - schema: "chats", - table: "MessageMedia"); - - migrationBuilder.AddColumn( - name: "IsPinned", - schema: "chats", - table: "ChatMembers", - type: "boolean", - nullable: false, - defaultValue: false); - - migrationBuilder.AddPrimaryKey( - name: "PK_MessageMedia", - schema: "chats", - table: "MessageMedia", - columns: new[] { "MessageId", "Id" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_MessageMedia", - schema: "chats", - table: "MessageMedia"); - - migrationBuilder.DropColumn( - name: "IsPinned", - schema: "chats", - table: "ChatMembers"); - - migrationBuilder.AddPrimaryKey( - name: "PK_MessageMedia", - schema: "chats", - table: "MessageMedia", - column: "Id"); - - migrationBuilder.CreateIndex( - name: "IX_MessageMedia_MessageId", - schema: "chats", - table: "MessageMedia", - column: "MessageId"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs deleted file mode 100644 index f521fb8..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.Designer.cs +++ /dev/null @@ -1,254 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260313183634_AddStoryIdToMessages")] - partial class AddStoryIdToMessages - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs deleted file mode 100644 index 8c657f5..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313183634_AddStoryIdToMessages.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddStoryIdToMessages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "StoryId", - schema: "chats", - table: "Messages", - type: "uuid", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "StoryId", - schema: "chats", - table: "Messages"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs deleted file mode 100644 index 9e9d74f..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.Designer.cs +++ /dev/null @@ -1,260 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260313201532_AddStoryMediaInfoToMessages")] - partial class AddStoryMediaInfoToMessages - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("StoryMediaType") - .HasColumnType("text"); - - b.Property("StoryMediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs deleted file mode 100644 index 7aec783..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313201532_AddStoryMediaInfoToMessages.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddStoryMediaInfoToMessages : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "StoryMediaType", - schema: "chats", - table: "Messages", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "StoryMediaUrl", - schema: "chats", - table: "Messages", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "StoryMediaType", - schema: "chats", - table: "Messages"); - - migrationBuilder.DropColumn( - name: "StoryMediaUrl", - schema: "chats", - table: "Messages"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs deleted file mode 100644 index 06969ac..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs +++ /dev/null @@ -1,263 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260313204219_AddChatDescription")] - partial class AddChatDescription - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("StoryMediaType") - .HasColumnType("text"); - - b.Property("StoryMediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs deleted file mode 100644 index 3d32323..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddChatDescription : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Description", - schema: "chats", - table: "Chats", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Description", - schema: "chats", - table: "Chats"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.Designer.cs deleted file mode 100644 index 7c6ba72..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.Designer.cs +++ /dev/null @@ -1,266 +0,0 @@ -// -using System; -using Knot.Modules.Chats.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.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260316142303_AddIsImportedToMessage")] - partial class AddIsImportedToMessage - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("IsImported") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("StoryMediaType") - .HasColumnType("text"); - - b.Property("StoryMediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.cs deleted file mode 100644 index 2350d9d..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316142303_AddIsImportedToMessage.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddIsImportedToMessage : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsImported", - schema: "chats", - table: "Messages", - type: "boolean", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsImported", - schema: "chats", - table: "Messages"); - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.Designer.cs deleted file mode 100644 index 732d4c7..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.Designer.cs +++ /dev/null @@ -1,266 +0,0 @@ -// -using System; -using Knot.Modules.Chats.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.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - [Migration("20260316143533_RemoveIsImportedDefault")] - partial class RemoveIsImportedDefault - { - /// - 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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("IsImported") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("StoryMediaType") - .HasColumnType("text"); - - b.Property("StoryMediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.cs deleted file mode 100644 index eb0162e..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260316143533_RemoveIsImportedDefault.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - /// - public partial class RemoveIsImportedDefault : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs deleted file mode 100644 index 128b775..0000000 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs +++ /dev/null @@ -1,263 +0,0 @@ -// -using System; -using Knot.Modules.Chats.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ChatsDbContext))] - partial class ChatsDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(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.Modules.Chats.Domain.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Chats", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.PrimitiveCollection("DeletedByUsers") - .IsRequired() - .HasColumnType("uuid[]") - .HasColumnName("DeletedByUsers"); - - b.Property("ForwardedFromId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("IsImported") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("StoryId") - .HasColumnType("uuid"); - - b.Property("StoryMediaType") - .HasColumnType("text"); - - b.Property("StoryMediaUrl") - .HasColumnType("text"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Emoji") - .IsRequired() - .HasColumnType("text"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId", "Emoji") - .IsUnique(); - - b.ToTable("MessageReactions", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("MessageId") - .HasColumnType("uuid"); - - b.Property("ReadAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("MessageId", "UserId") - .IsUnique(); - - b.ToTable("ReadReceipts", "chats"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("ChatId") - .HasColumnType("uuid"); - - b1.Property("IsMuted") - .HasColumnType("boolean"); - - b1.Property("IsPinned") - .HasColumnType("boolean"); - - b1.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b1.Property("Role") - .IsRequired() - .HasColumnType("text"); - - b1.Property("UserId") - .HasColumnType("uuid"); - - b1.HasKey("Id"); - - b1.HasIndex("ChatId", "UserId") - .IsUnique(); - - b1.ToTable("ChatMembers", "chats"); - - b1.WithOwner() - .HasForeignKey("ChatId"); - }); - - b.Navigation("Members"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("MessageId", "Id"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("Reactions") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b => - { - b.HasOne("Knot.Modules.Chats.Domain.Message", null) - .WithMany("ReadBy") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Navigation("Reactions"); - - b.Navigation("ReadBy"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs new file mode 100644 index 0000000..bf11f28 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/EncryptedStringSerializer.cs @@ -0,0 +1,49 @@ +using System; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Knot.Shared.Kernel.Security; + +namespace Knot.Modules.Chats.Infrastructure.Persistence.Mongo; + +public class EncryptedStringSerializer : SerializerBase +{ + public static IEncryptionService? EncryptionService { get; set; } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value) + { + if (string.IsNullOrEmpty(value) || EncryptionService == null) + { + context.Writer.WriteString(value ?? string.Empty); + return; + } + + try + { + var encrypted = EncryptionService.EncryptMessage(value); + context.Writer.WriteString(encrypted); + } + catch + { + context.Writer.WriteString(value); + } + } + + public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var value = context.Reader.ReadString(); + if (string.IsNullOrEmpty(value) || EncryptionService == null) + { + return value; + } + + try + { + return EncryptionService.DecryptMessage(value); + } + catch + { + // Fallback for already unencrypted, or failed to decrypt + return value; + } + } +} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs new file mode 100644 index 0000000..5d29df6 --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs @@ -0,0 +1,73 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Knot.Modules.Chats.Domain; +using Knot.Shared.Kernel; + +namespace Knot.Modules.Chats.Infrastructure.Persistence.Mongo; + +public static class MongoDbMapConfigurator +{ + private static bool _initialized; + + public static void Configure() + { + if (_initialized) return; + + BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); + + BsonSerializer.RegisterSerializer(new EnumSerializer(BsonType.String)); + BsonSerializer.RegisterSerializer(new EnumSerializer(BsonType.String)); + + BsonClassMap.RegisterClassMap>(cm => + { + cm.AutoMap(); + cm.MapIdProperty(e => e.Id); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapField("_deletedFor").SetElementName("DeletedFor"); + cm.MapField("_readBy").SetElementName("ReadBy"); + cm.MapField("_reactions").SetElementName("Reactions"); + cm.SetIsRootClass(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetDiscriminator("TextMessage"); + cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer()); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapField("_media").SetElementName("Media"); + cm.SetDiscriminator("MediaMessage"); + cm.MapProperty(c => c.Caption).SetSerializer(new EncryptedStringSerializer()); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetDiscriminator("StoryMessage"); + cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer()); + cm.MapProperty(c => c.InternalStoryMediaUrl).SetSerializer(new EncryptedStringSerializer()); + }); + + BsonClassMap.RegisterClassMap(cm => cm.AutoMap()); + BsonClassMap.RegisterClassMap(cm => cm.AutoMap()); + BsonClassMap.RegisterClassMap(cm => cm.AutoMap()); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapProperty(c => c.Url).SetSerializer(new EncryptedStringSerializer()); + cm.MapProperty(c => c.Filename).SetSerializer(new EncryptedStringSerializer()); + }); + + _initialized = true; + } +} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs index c5c48ad..5bc16e7 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Knot.Modules.Chats.Application.Messages.Send; +using Knot.Modules.Chats.Application.Messages.Read; +using Knot.Modules.Chats.Application.Messages.Delete; +using Knot.Modules.Chats.Application.Messages.React; using Knot.Modules.Chats.Domain; using Knot.Shared.Kernel; using Microsoft.Extensions.Caching.Memory; @@ -124,7 +127,7 @@ public sealed class ChatHub : Hub if (parsedIds.Any()) { - var command = new Knot.Modules.Chats.Application.Messages.Read.ReadMessagesCommand( + var command = new ReadMessagesCommand( request.ChatId, _userContext.UserId, parsedIds); await _sender.Send(command); } @@ -148,7 +151,7 @@ public sealed class ChatHub : Hub if (parsedIds.Any()) { - var command = new Knot.Modules.Chats.Application.Messages.Delete.DeleteMessagesCommand( + var command = new DeleteMessagesCommand( request.ChatId, _userContext.UserId, parsedIds, request.DeleteForAll); await _sender.Send(command); } @@ -188,7 +191,7 @@ public sealed class ChatHub : Hub request.MessageId, request.ChatId, request.Emoji, _userContext.UserId); - var command = new Knot.Modules.Chats.Application.Messages.React.AddReactionCommand( + var command = new AddReactionCommand( request.MessageId, _userContext.UserId, request.Emoji, request.ChatId); var result = await _sender.Send(command); if (result.IsFailure) @@ -209,7 +212,7 @@ public sealed class ChatHub : Hub request.MessageId, request.ChatId, request.Emoji, _userContext.UserId); - var command = new Knot.Modules.Chats.Application.Messages.React.RemoveReactionCommand( + var command = new RemoveReactionCommand( request.MessageId, _userContext.UserId, request.Emoji, request.ChatId); var result = await _sender.Send(command); if (result.IsFailure) diff --git a/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj b/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj index a16e4d5..71d2902 100644 --- a/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj +++ b/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj @@ -13,6 +13,7 @@ + diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs b/apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs similarity index 53% rename from apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs rename to apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs index 23743a0..4aeba6c 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.Designer.cs +++ b/apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.Designer.cs @@ -1,18 +1,18 @@ -// +// using System; +using Knot.Modules.Chats.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Knot.Modules.Chats.Infrastructure.Persistence; #nullable disable -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations +namespace Knot.Modules.Chats.Migrations { [DbContext(typeof(ChatsDbContext))] - [Migration("20260311180825_InitialChats")] + [Migration("20260319124845_InitialChats")] partial class InitialChats { /// @@ -38,6 +38,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("Description") + .HasColumnType("text"); + b.Property("Name") .HasColumnType("text"); @@ -50,45 +53,6 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations b.ToTable("Chats", "chats"); }); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .HasColumnType("boolean"); - - b.Property("IsEdited") - .HasColumnType("boolean"); - - b.Property("Quote") - .HasColumnType("text"); - - b.Property("ReplyToId") - .HasColumnType("uuid"); - - b.Property("SenderId") - .HasColumnType("uuid"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Messages", "chats"); - }); - modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => { b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => @@ -103,6 +67,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations b1.Property("IsMuted") .HasColumnType("boolean"); + b1.Property("IsPinned") + .HasColumnType("boolean"); + b1.Property("JoinedAt") .HasColumnType("timestamp with time zone"); @@ -126,44 +93,6 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations b.Navigation("Members"); }); - - modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b => - { - b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 => - { - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b1.Property("Filename") - .HasColumnType("text"); - - b1.Property("MessageId") - .HasColumnType("uuid"); - - b1.Property("Size") - .HasColumnType("bigint"); - - b1.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b1.Property("Url") - .IsRequired() - .HasColumnType("text"); - - b1.HasKey("Id"); - - b1.HasIndex("MessageId"); - - b1.ToTable("MessageMedia", "chats"); - - b1.WithOwner() - .HasForeignKey("MessageId"); - }); - - b.Navigation("Media"); - }); #pragma warning restore 612, 618 } } diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs b/apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs similarity index 50% rename from apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs rename to apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs index a4f70f4..d32bc50 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260311180825_InitialChats.cs +++ b/apps/server-net/src/Modules/Chats/Migrations/20260319124845_InitialChats.cs @@ -1,9 +1,9 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations +namespace Knot.Modules.Chats.Migrations { /// public partial class InitialChats : Migration @@ -22,6 +22,7 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), Type = table.Column(type: "text", nullable: false), Name = table.Column(type: "text", nullable: true), + Description = table.Column(type: "text", nullable: true), Avatar = table.Column(type: "text", nullable: true), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -30,27 +31,6 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations table.PrimaryKey("PK_Chats", x => x.Id); }); - migrationBuilder.CreateTable( - name: "Messages", - schema: "chats", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ChatId = table.Column(type: "uuid", nullable: false), - SenderId = table.Column(type: "uuid", nullable: false), - Content = table.Column(type: "text", nullable: true), - Type = table.Column(type: "text", nullable: false), - ReplyToId = table.Column(type: "uuid", nullable: true), - Quote = table.Column(type: "text", nullable: true), - IsEdited = table.Column(type: "boolean", nullable: false), - IsDeleted = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Messages", x => x.Id); - }); - migrationBuilder.CreateTable( name: "ChatMembers", schema: "chats", @@ -61,6 +41,7 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations UserId = table.Column(type: "uuid", nullable: false), Role = table.Column(type: "text", nullable: false), JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), + IsPinned = table.Column(type: "boolean", nullable: false), IsMuted = table.Column(type: "boolean", nullable: false) }, constraints: table => @@ -75,42 +56,12 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "MessageMedia", - schema: "chats", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - MessageId = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "text", nullable: false), - Url = table.Column(type: "text", nullable: false), - Filename = table.Column(type: "text", nullable: true), - Size = table.Column(type: "bigint", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MessageMedia", x => x.Id); - table.ForeignKey( - name: "FK_MessageMedia_Messages_MessageId", - column: x => x.MessageId, - principalSchema: "chats", - principalTable: "Messages", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - migrationBuilder.CreateIndex( name: "IX_ChatMembers_ChatId_UserId", schema: "chats", table: "ChatMembers", columns: new[] { "ChatId", "UserId" }, unique: true); - - migrationBuilder.CreateIndex( - name: "IX_MessageMedia_MessageId", - schema: "chats", - table: "MessageMedia", - column: "MessageId"); } /// @@ -120,17 +71,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations name: "ChatMembers", schema: "chats"); - migrationBuilder.DropTable( - name: "MessageMedia", - schema: "chats"); - migrationBuilder.DropTable( name: "Chats", schema: "chats"); - - migrationBuilder.DropTable( - name: "Messages", - schema: "chats"); } } } diff --git a/apps/server-net/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs b/apps/server-net/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs new file mode 100644 index 0000000..b99022e --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Migrations/ChatsDbContextModelSnapshot.cs @@ -0,0 +1,96 @@ +// +using System; +using Knot.Modules.Chats.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Chats.Migrations +{ + [DbContext(typeof(ChatsDbContext))] + partial class ChatsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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.Modules.Chats.Domain.Chat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Chats", "chats"); + }); + + modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b => + { + b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("ChatId") + .HasColumnType("uuid"); + + b1.Property("IsMuted") + .HasColumnType("boolean"); + + b1.Property("IsPinned") + .HasColumnType("boolean"); + + b1.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b1.Property("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 + } + } +} diff --git a/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs index 26650dc..ca25045 100644 --- a/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs +++ b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using MediatR; using NSubstitute; using Xunit; using Knot.Shared.Kernel; @@ -18,6 +19,7 @@ public class SendMessageCommandHandlerTests private readonly IChatRepository _chatRepository; private readonly IMessageRepository _messageRepository; private readonly IChatsUnitOfWork _unitOfWork; + private readonly IMediator _mediator; private readonly SendMessageCommandHandler _handler; public SendMessageCommandHandlerTests() @@ -25,8 +27,9 @@ public class SendMessageCommandHandlerTests _chatRepository = Substitute.For(); _messageRepository = Substitute.For(); _unitOfWork = Substitute.For(); + _mediator = Substitute.For(); - _handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork); + _handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator); } [Fact] diff --git a/docker-compose.yml b/docker-compose.yml index fa2ba66..c76ab96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: depends_on: - db - minio + - mongo ports: - "5059:8080" @@ -36,6 +37,14 @@ services: command: server /data --console-address ":9001" volumes: - minio_data:/data + mongo: + image: mongo:6-jammy + container_name: knot-mongo + restart: always + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db web: build: