Рефакторинг и монго
This commit is contained in:
@@ -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<CleanRunCommand, MessageResponse>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext)
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("Messages");
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> 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<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
@@ -85,8 +90,9 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
|
||||
if (orphanMessages.Any())
|
||||
{
|
||||
_chatsDbContext.Messages.RemoveRange(orphanMessages);
|
||||
await _chatsDbContext.SaveChangesAsync(cancellationToken);
|
||||
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||
}
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
|
||||
@@ -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<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext)
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("Messages");
|
||||
}
|
||||
|
||||
public async Task<Result<CleanupDryRunResultDto>> 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<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.Select(me => me.Url)
|
||||
|
||||
@@ -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<AdminUserDetailsDto>;
|
||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
public GetUserDetailsQueryHandler(IUserRepository userRepository, ChatsDbContext chatsDbContext)
|
||||
public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDatabase.GetCollection<Message>("Messages");
|
||||
}
|
||||
|
||||
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
|
||||
@@ -32,23 +32,20 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
|
||||
return Result.Failure<AdminUserDetailsDto>(IdentityErrors.UserNotFound);
|
||||
}
|
||||
|
||||
var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken);
|
||||
var filter = Builders<Message>.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<MediaMessage>().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<TextMessage>().Select(m => m.Content)
|
||||
.Concat(userMessages.OfType<MediaMessage>().Where(m => m.Caption != null).Select(m => m.Caption))
|
||||
.ToList();
|
||||
|
||||
var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http"));
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
var envMappings = new Dictionary<string, string?>
|
||||
{
|
||||
["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<Knot.Shared.Infrastructure.Persistence.SystemDbContext>();
|
||||
await systemDb.Database.MigrateAsync();
|
||||
|
||||
// Set Encryption Service for MongoDB serializers
|
||||
var encryptionService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Security.IEncryptionService>();
|
||||
Knot.Modules.Chats.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService;
|
||||
|
||||
// Initialize Global Settings Cache
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Configuration.ISettingsService>();
|
||||
if (settingsService is Knot.Shared.Infrastructure.Configuration.SettingsService concreteSettings)
|
||||
|
||||
@@ -40,33 +40,32 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
}
|
||||
|
||||
var userIdsToFetch = new HashSet<Guid>();
|
||||
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<ChatMemberDto>();
|
||||
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<GetChatByIdQuery,
|
||||
}
|
||||
|
||||
var messagesList = new List<ChatMessageDto>();
|
||||
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<ReactionDto>();
|
||||
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<GetChatByIdQuery,
|
||||
return Result.Success<ChatDto?>(dto);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,36 +33,38 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
var dtos = new List<ChatDto>();
|
||||
bool hasFavorites = false;
|
||||
|
||||
foreach (var c in userChats)
|
||||
foreach (var chat in userChats)
|
||||
{
|
||||
var userIdsToFetch = new HashSet<Guid>();
|
||||
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<Guid>();
|
||||
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<ChatMemberDto>();
|
||||
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<GetChatsQuery, List<C
|
||||
));
|
||||
}
|
||||
|
||||
var messagesList = new List<ChatMessageDto>();
|
||||
usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj);
|
||||
|
||||
if (chatMessages.Any())
|
||||
var reactionsWithUser = new List<ReactionDto>();
|
||||
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<ReactionDto>();
|
||||
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<ChatMessageDto>
|
||||
{
|
||||
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<GetChatsQuery, List<C
|
||||
return Result.Success(sorted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
|
||||
{
|
||||
message.DeleteForUser(request.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _messageRepository.UpdateAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
if (request.DeleteForAll)
|
||||
{
|
||||
|
||||
@@ -47,46 +47,39 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
||||
var userIdsToFetch = new HashSet<Guid>();
|
||||
var replyMessages = new Dictionary<Guid, Message>();
|
||||
|
||||
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<GetMessagesQuery,
|
||||
|
||||
replyToObj = new ReplyToMessageDto(
|
||||
replyMsg.Id,
|
||||
replyMsg.Content,
|
||||
replyMsg.IsDeleted,
|
||||
replyMsg.Media.Select(rm => 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<MessageReactionDto>();
|
||||
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<GetMessagesQuery,
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,36 +31,38 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
public async Task<Result<List<SharedMediaDto>>> 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<List<SharedMediaDto>>(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<SharedMediaDto>();
|
||||
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<string>();
|
||||
var mediaLinks = (m.Media ?? Enumerable.Empty<Media>()).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<string>();
|
||||
var messageMediaColl = message.Media;
|
||||
var mediaLinks = (messageMediaColl ?? Enumerable.Empty<Media>()).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<GetSharedMediaQ
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.Media == null || !m.Media.Any())
|
||||
var messageMedia = message.Media;
|
||||
if (messageMedia == null || !messageMedia.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filteredMedia = m.Media.Where(media =>
|
||||
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<GetSharedMediaQ
|
||||
|
||||
if (filteredMedia.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,
|
||||
message.Content,
|
||||
message.CreatedAt,
|
||||
null,
|
||||
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
|
||||
m.ReplyToId,
|
||||
m.Quote,
|
||||
m.StoryId,
|
||||
m.StoryMediaUrl,
|
||||
m.StoryMediaType,
|
||||
m.IsEdited,
|
||||
m.Type,
|
||||
message.ReplyToId,
|
||||
message.Quote,
|
||||
message.StoryId,
|
||||
message.StoryMediaUrl,
|
||||
message.StoryMediaType,
|
||||
message.IsEdited,
|
||||
message.Type,
|
||||
filteredMedia.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList()
|
||||
));
|
||||
}
|
||||
@@ -121,3 +119,4 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
return Result.Success(result.OrderByDescending(x => x.CreatedAt).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,35 +26,36 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
|
||||
public async Task<Result<List<SearchMessageDto>>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,15 +27,18 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
private readonly IChatRepository _chatRepository;
|
||||
private readonly IMessageRepository _messageRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly MediatR.IMediator _mediator;
|
||||
|
||||
public SendMessageCommandHandler(
|
||||
IChatRepository chatRepository,
|
||||
IMessageRepository messageRepository,
|
||||
IChatsUnitOfWork unitOfWork)
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
MediatR.IMediator mediator)
|
||||
{
|
||||
_chatRepository = chatRepository;
|
||||
_messageRepository = messageRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
||||
@@ -54,30 +57,71 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
}
|
||||
|
||||
// 3. Создаем сообщение
|
||||
var message = Message.Create(
|
||||
request.ChatId,
|
||||
request.SenderId,
|
||||
request.Content,
|
||||
request.Type,
|
||||
request.ReplyToId,
|
||||
request.Quote,
|
||||
request.ForwardedFromId,
|
||||
request.StoryId,
|
||||
request.StoryMediaUrl,
|
||||
request.StoryMediaType);
|
||||
|
||||
if (request.Attachments != null && request.Attachments.Any())
|
||||
Message message;
|
||||
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
||||
{
|
||||
var parsedStoryMediaType = Enum.TryParse<MediaType>(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<MediaType>(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<MediaType>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -350,16 +350,24 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
|
||||
|
||||
bool isJoined = fromNameNode == null;
|
||||
bool isMediaOnly = string.IsNullOrEmpty(content) && forwardedNode == null && replyToId == null;
|
||||
bool hasMedia = mediaNodes != null && mediaNodes.Count > 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<ExecuteImpor
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaNodes != null)
|
||||
if (hasMedia)
|
||||
{
|
||||
foreach (var mediaNode in mediaNodes)
|
||||
foreach (var mediaNode in mediaNodes!)
|
||||
{
|
||||
string? href = mediaNode.GetAttribute("href") ?? mediaNode.GetAttribute("src");
|
||||
if (!string.IsNullOrEmpty(href) && !href.StartsWith("http"))
|
||||
@@ -391,8 +399,12 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
|
||||
finalMType = "image";
|
||||
}
|
||||
|
||||
var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType);
|
||||
targetMessage.AddMedia(finalMType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length);
|
||||
var parsedType = Enum.TryParse<MediaType>(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<ExecuteImpor
|
||||
return Result.Success(new ExecuteImportResponseDto(true, importedCount, chatId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
using Knot.Modules.Chats.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MongoDB.Driver;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||
using Knot.Modules.Chats.Infrastructure.Persistence.Mongo;
|
||||
|
||||
using Knot.Modules.Chats.Application.Abstractions;
|
||||
|
||||
@@ -24,12 +26,20 @@ public static class DependencyInjection
|
||||
services.AddDbContext<ChatsDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Регистрация Unit of Work и Репозиториев
|
||||
// MongoDB Setup for Messages
|
||||
MongoDbMapConfigurator.Configure();
|
||||
|
||||
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
|
||||
services.AddSingleton<IMongoClient>(new MongoClient(mongoConnectionString));
|
||||
services.AddScoped<IMongoDatabase>(sp =>
|
||||
sp.GetRequiredService<IMongoClient>().GetDatabase("forkmessager_chats"));
|
||||
|
||||
// Registration
|
||||
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
|
||||
services.AddScoped<IChatRepository, ChatRepository>();
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
|
||||
// Регистрация MediatR для этого модуля
|
||||
// MediatR
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
|
||||
21
apps/server-net/src/Modules/Chats/Domain/DeletedMessage.cs
Normal file
21
apps/server-net/src/Modules/Chats/Domain/DeletedMessage.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Запись о том, что конкретный пользователь удалил у себя сообщение.
|
||||
/// </summary>
|
||||
public sealed class DeletedMessage : Entity<Guid>
|
||||
{
|
||||
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) { }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ public interface IMessageRepository
|
||||
void Add(Message message);
|
||||
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken);
|
||||
Task<Message?> GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken);
|
||||
Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
|
||||
Task AddReadReceiptsAsync(Guid userId, List<Guid> messageIds, CancellationToken cancellationToken);
|
||||
Task<bool> AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken);
|
||||
@@ -14,4 +15,5 @@ public interface IMessageRepository
|
||||
Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken);
|
||||
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
||||
Task<int> GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken);
|
||||
Task UpdateAsync(Message message, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
31
apps/server-net/src/Modules/Chats/Domain/Media.cs
Normal file
31
apps/server-net/src/Modules/Chats/Domain/Media.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Медиа-файл, прикрепленный к сообщению.
|
||||
/// </summary>
|
||||
public sealed class Media : Entity<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
58
apps/server-net/src/Modules/Chats/Domain/MediaMessage.cs
Normal file
58
apps/server-net/src/Modules/Chats/Domain/MediaMessage.cs
Normal file
@@ -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> _media = new();
|
||||
public override IReadOnlyCollection<Media> 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();
|
||||
}
|
||||
}
|
||||
12
apps/server-net/src/Modules/Chats/Domain/MediaType.cs
Normal file
12
apps/server-net/src/Modules/Chats/Domain/MediaType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Тип медиа-контента.
|
||||
/// </summary>
|
||||
public enum MediaType
|
||||
{
|
||||
Image,
|
||||
Video,
|
||||
Voice,
|
||||
File
|
||||
}
|
||||
@@ -1,89 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: сообщение отправлено.
|
||||
/// Абстрактная база агрегата Сообщение.
|
||||
/// </summary>
|
||||
public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Сущность сообщения (Агрегат).
|
||||
/// </summary>
|
||||
public sealed class Message : AggregateRoot<Guid>
|
||||
public abstract class Message : AggregateRoot<Guid>
|
||||
{
|
||||
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> _media = new();
|
||||
public IReadOnlyCollection<Media> 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> Media => Array.Empty<Media>();
|
||||
|
||||
private readonly List<ReadReceipt> _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<ReadReceipt> _readBy = new();
|
||||
public IReadOnlyCollection<ReadReceipt> ReadBy => _readBy.AsReadOnly();
|
||||
|
||||
private readonly List<Guid> _deletedByUsers = new();
|
||||
public IReadOnlyCollection<Guid> DeletedByUsers => _deletedByUsers.AsReadOnly();
|
||||
protected readonly List<DeletedMessage> _deletedFor = new();
|
||||
public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly();
|
||||
|
||||
private Message() : base(Guid.Empty) { Type = "text"; }
|
||||
protected readonly List<Reaction> _reactions = new();
|
||||
public IReadOnlyCollection<Reaction> 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<Reaction> _reactions = new();
|
||||
public IReadOnlyCollection<Reaction> 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<Guid>
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Реакция на сообщение.
|
||||
/// </summary>
|
||||
public sealed class Reaction : Entity<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Медиа-файл, прикрепленный к сообщению.
|
||||
/// </summary>
|
||||
public sealed class Media : Entity<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
16
apps/server-net/src/Modules/Chats/Domain/MessageFlags.cs
Normal file
16
apps/server-net/src/Modules/Chats/Domain/MessageFlags.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Флаги состояния сообщения
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MessageFlags
|
||||
{
|
||||
None = 0,
|
||||
IsEdited = 1,
|
||||
IsDeleted = 2,
|
||||
IsImported = 4,
|
||||
IsPinned = 8
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: сообщение отправлено.
|
||||
/// </summary>
|
||||
public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent;
|
||||
16
apps/server-net/src/Modules/Chats/Domain/MessageState.cs
Normal file
16
apps/server-net/src/Modules/Chats/Domain/MessageState.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние сообщения
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MessageState
|
||||
{
|
||||
None = 0,
|
||||
IsEdited = 1,
|
||||
IsDeleted = 2,
|
||||
IsImported = 4,
|
||||
IsPinned = 8
|
||||
}
|
||||
26
apps/server-net/src/Modules/Chats/Domain/Reaction.cs
Normal file
26
apps/server-net/src/Modules/Chats/Domain/Reaction.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Chats.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Реакция на сообщение.
|
||||
/// </summary>
|
||||
public sealed class Reaction : Entity<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
55
apps/server-net/src/Modules/Chats/Domain/StoryMessage.cs
Normal file
55
apps/server-net/src/Modules/Chats/Domain/StoryMessage.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
48
apps/server-net/src/Modules/Chats/Domain/TextMessage.cs
Normal file
48
apps/server-net/src/Modules/Chats/Domain/TextMessage.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -77,14 +77,14 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
message.Id,
|
||||
message.ChatId,
|
||||
message.SenderId,
|
||||
message.Content,
|
||||
message.Type,
|
||||
Content = message.Content,
|
||||
Type = message.Type,
|
||||
message.CreatedAt,
|
||||
message.ForwardedFromId,
|
||||
ForwardedFrom = forwardedFromObj,
|
||||
message.ReplyToId,
|
||||
ReplyTo = replyToObj,
|
||||
message.Quote,
|
||||
Quote = message.Quote,
|
||||
Media = message.Media.Select(m => new
|
||||
{
|
||||
m.Id,
|
||||
@@ -95,9 +95,10 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
}).ToList(),
|
||||
Sender = senderObj,
|
||||
ReadBy = new List<object>(),
|
||||
message.StoryId,
|
||||
message.StoryMediaUrl,
|
||||
message.StoryMediaType
|
||||
StoryId = message.StoryId,
|
||||
StoryMediaUrl = message.StoryMediaUrl,
|
||||
StoryMediaType = message.StoryMediaType
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ChatsDbContext> options, IMediator mediator, Knot.Shared.Kernel.Security.IEncryptionService encryptionService)
|
||||
public ChatsDbContext(DbContextOptions<ChatsDbContext> options, IMediator mediator, IEncryptionService encryptionService)
|
||||
: base(options)
|
||||
{
|
||||
_mediator = mediator;
|
||||
@@ -23,9 +24,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
||||
}
|
||||
|
||||
public DbSet<Chat> Chats => Set<Chat>();
|
||||
public DbSet<Message> Messages => Set<Message>();
|
||||
public DbSet<ReadReceipt> ReadReceipts => Set<ReadReceipt>();
|
||||
public DbSet<Reaction> Reactions => Set<Reaction>();
|
||||
|
||||
|
||||
|
||||
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<Message>(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<Reaction>(builder =>
|
||||
{
|
||||
builder.ToTable("MessageReactions");
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.HasIndex(r => new { r.MessageId, r.UserId, r.Emoji }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ReadReceipt>(builder =>
|
||||
{
|
||||
builder.ToTable("ReadReceipts");
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.HasIndex(r => new { r.MessageId, r.UserId }).IsUnique();
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -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<Message> _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<Message>("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<Message?> 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<Message>.Filter.Eq(m => m.Id, id);
|
||||
return await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Message>> 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<Message>.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<Message?> GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<Message>.Filter.Eq(m => m.ChatId, chatId);
|
||||
return await _messages.Find(filter)
|
||||
.SortByDescending(m => m.CreatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _dbContext.Messages
|
||||
.Where(m => m.ChatId == chatId);
|
||||
|
||||
var builder = Builders<Message>.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<List<Message>> 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<Message>.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<Message>.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<Guid> 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<Message>.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<WriteModel<Message>>();
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (!msg.ReadBy.Any(r => r.UserId == userId))
|
||||
{
|
||||
var receipt = new ReadReceipt(msg.Id, userId);
|
||||
var pushUpdate = Builders<Message>.Update.Push("ReadBy", receipt);
|
||||
var updateModel = new UpdateOneModel<Message>(Builders<Message>.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<bool> AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken)
|
||||
{
|
||||
// Проверяем, существует ли сообщение
|
||||
var messageExists = await _dbContext.Messages
|
||||
.AnyAsync(m => m.Id == messageId, cancellationToken);
|
||||
var filter = Builders<Message>.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<Message>.Update.Push("Reactions", reaction);
|
||||
await _messages.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> 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<Message>.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<Message>.Update.PullFilter("Reactions",
|
||||
Builders<BsonDocument>.Filter.And(
|
||||
Builders<BsonDocument>.Filter.Eq("UserId", userId),
|
||||
Builders<BsonDocument>.Filter.Eq("Emoji", emoji)
|
||||
));
|
||||
|
||||
await _messages.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Message?> 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<Message>.Filter.And(
|
||||
Builders<Message>.Filter.Eq(m => m.ChatId, chatId),
|
||||
Builders<Message>.Filter.Eq("_t", "StoryMessage"),
|
||||
Builders<Message>.Filter.Eq("StoryId", storyId)
|
||||
);
|
||||
|
||||
return await _messages.Find(filter)
|
||||
.SortByDescending(m => m.CreatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> 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<Message>.Filter.Not(
|
||||
Builders<Message>.Filter.ElemMatch("ReadBy",
|
||||
Builders<BsonDocument>.Filter.Eq("UserId", userId))
|
||||
);
|
||||
|
||||
var finalFilter = Builders<Message>.Filter.And(
|
||||
Builders<Message>.Filter.Eq(m => m.ChatId, chatId),
|
||||
Builders<Message>.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<Message>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateChatModel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReadReceipts",
|
||||
schema: "chats",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MessageId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ReadAt = table.Column<DateTime>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReadReceipts",
|
||||
schema: "chats");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddForwardedFromToMessages : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ForwardedFromId",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ForwardedFromId",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddReactionsToMessages : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MessageReactions",
|
||||
schema: "chats",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MessageId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Emoji = table.Column<string>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "MessageReactions",
|
||||
schema: "chats");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.ReadReceipt", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateChatsSchema : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid[]>(
|
||||
name: "DeletedByUsers",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "uuid[]",
|
||||
nullable: false,
|
||||
defaultValue: new Guid[0]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedByUsers",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SupportPinningAndMuting : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<bool>(
|
||||
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" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStoryIdToMessages : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "StoryId",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StoryId",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StoryMediaType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StoryMediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStoryMediaInfoToMessages : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "StoryMediaType",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "StoryMediaUrl",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StoryMediaType",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StoryMediaUrl",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StoryMediaType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StoryMediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddChatDescription : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Description",
|
||||
schema: "chats",
|
||||
table: "Chats",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Description",
|
||||
schema: "chats",
|
||||
table: "Chats");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsImported")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StoryMediaType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StoryMediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsImportedToMessage : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsImported",
|
||||
schema: "chats",
|
||||
table: "Messages",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsImported",
|
||||
schema: "chats",
|
||||
table: "Messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("chats")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsImported")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StoryMediaType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StoryMediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveIsImportedDefault : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Chats", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]")
|
||||
.HasColumnName("DeletedByUsers");
|
||||
|
||||
b.Property<Guid?>("ForwardedFromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsImported")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StoryMediaType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StoryMediaUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Messages", "chats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Reaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Emoji")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Chats.Domain.Message", b =>
|
||||
{
|
||||
b.OwnsMany("Knot.Modules.Chats.Domain.Media", "Media", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MessageState>(BsonType.String));
|
||||
BsonSerializer.RegisterSerializer(new EnumSerializer<MediaType>(BsonType.String));
|
||||
|
||||
BsonClassMap.RegisterClassMap<Entity<Guid>>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.MapIdProperty(e => e.Id);
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<Message>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.MapField("_deletedFor").SetElementName("DeletedFor");
|
||||
cm.MapField("_readBy").SetElementName("ReadBy");
|
||||
cm.MapField("_reactions").SetElementName("Reactions");
|
||||
cm.SetIsRootClass(true);
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<TextMessage>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.SetDiscriminator("TextMessage");
|
||||
cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer());
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<MediaMessage>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.MapField("_media").SetElementName("Media");
|
||||
cm.SetDiscriminator("MediaMessage");
|
||||
cm.MapProperty(c => c.Caption).SetSerializer(new EncryptedStringSerializer());
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<StoryMessage>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.SetDiscriminator("StoryMessage");
|
||||
cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer());
|
||||
cm.MapProperty(c => c.InternalStoryMediaUrl).SetSerializer(new EncryptedStringSerializer());
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<DeletedMessage>(cm => cm.AutoMap());
|
||||
BsonClassMap.RegisterClassMap<ReadReceipt>(cm => cm.AutoMap());
|
||||
BsonClassMap.RegisterClassMap<Reaction>(cm => cm.AutoMap());
|
||||
|
||||
BsonClassMap.RegisterClassMap<Media>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
cm.MapProperty(c => c.Url).SetSerializer(new EncryptedStringSerializer());
|
||||
cm.MapProperty(c => c.Filename).SetSerializer(new EncryptedStringSerializer());
|
||||
});
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -38,6 +38,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEdited")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Quote")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("ReplyToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("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<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Filename")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("MessageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<long?>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialChats : Migration
|
||||
@@ -22,6 +22,7 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
Avatar = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
|
||||
ChatId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SenderId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Content = table.Column<string>(type: "text", nullable: true),
|
||||
Type = table.Column<string>(type: "text", nullable: false),
|
||||
ReplyToId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Quote = table.Column<string>(type: "text", nullable: true),
|
||||
IsEdited = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
|
||||
Role = table.Column<string>(type: "text", nullable: false),
|
||||
JoinedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsPinned = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsMuted = table.Column<bool>(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<Guid>(type: "uuid", nullable: false),
|
||||
MessageId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "text", nullable: false),
|
||||
Url = table.Column<string>(type: "text", nullable: false),
|
||||
Filename = table.Column<string>(type: "text", nullable: true),
|
||||
Size = table.Column<long>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<Guid>("ChatId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<bool>("IsMuted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsPinned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime>("JoinedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.HasKey("Id");
|
||||
|
||||
b1.HasIndex("ChatId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b1.ToTable("ChatMembers", "chats");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ChatId");
|
||||
});
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IChatRepository>();
|
||||
_messageRepository = Substitute.For<IMessageRepository>();
|
||||
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||
_mediator = Substitute.For<IMediator>();
|
||||
|
||||
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork);
|
||||
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user