Рефакторинг и монго
This commit is contained in:
@@ -8,6 +8,8 @@ using Knot.Shared.Kernel;
|
|||||||
using Knot.Shared.Kernel.Storage;
|
using Knot.Shared.Kernel.Storage;
|
||||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||||
|
using Knot.Modules.Chats.Domain;
|
||||||
|
using MongoDB.Driver;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Host.Application.Admin.Commands;
|
namespace Host.Application.Admin.Commands;
|
||||||
@@ -17,33 +19,36 @@ public record CleanRunCommand(IFileStorageService FileStorage, IdentityDbContext
|
|||||||
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||||
{
|
{
|
||||||
private readonly ChatsDbContext _chatsDbContext;
|
private readonly ChatsDbContext _chatsDbContext;
|
||||||
|
private readonly IMongoCollection<Message> _messages;
|
||||||
|
|
||||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext)
|
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||||
{
|
{
|
||||||
_chatsDbContext = chatsDbContext;
|
_chatsDbContext = chatsDbContext;
|
||||||
|
_messages = mongoDb.GetCollection<Message>("Messages");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orphanMessages = await _chatsDbContext.Messages
|
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
.Include(m => m.Media)
|
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||||
.Where(m => m.IsDeleted || !_chatsDbContext.Chats.Any(c => c.Id == m.ChatId))
|
|
||||||
.ToListAsync(cancellationToken);
|
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 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 allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var validUrls = new HashSet<string>();
|
var validUrls = new HashSet<string>();
|
||||||
|
|
||||||
var activeMessageUrls = keptMessages
|
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||||
.Where(m => m.Media != null)
|
.Where(m => m.Media != null)
|
||||||
.SelectMany(m => m.Media)
|
.SelectMany(m => m.Media)
|
||||||
.Select(me => me.Url)
|
.Select(me => me.Url)
|
||||||
@@ -85,8 +90,9 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
|||||||
|
|
||||||
if (orphanMessages.Any())
|
if (orphanMessages.Any())
|
||||||
{
|
{
|
||||||
_chatsDbContext.Messages.RemoveRange(orphanMessages);
|
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||||
await _chatsDbContext.SaveChangesAsync(cancellationToken);
|
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||||
|
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||||
|
|||||||
+18
-13
@@ -8,6 +8,8 @@ using Knot.Shared.Kernel;
|
|||||||
using Knot.Shared.Kernel.Storage;
|
using Knot.Shared.Kernel.Storage;
|
||||||
using Knot.Modules.Identity.Infrastructure.Persistence;
|
using Knot.Modules.Identity.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||||
|
using Knot.Modules.Chats.Domain;
|
||||||
|
using MongoDB.Driver;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Host.Models;
|
using Host.Models;
|
||||||
|
|
||||||
@@ -18,35 +20,38 @@ public record CleanDryRunQuery(IFileStorageService FileStorage, IdentityDbContex
|
|||||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||||
{
|
{
|
||||||
private readonly ChatsDbContext _chatsDbContext;
|
private readonly ChatsDbContext _chatsDbContext;
|
||||||
|
private readonly IMongoCollection<Message> _messages;
|
||||||
|
|
||||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext)
|
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb)
|
||||||
{
|
{
|
||||||
_chatsDbContext = chatsDbContext;
|
_chatsDbContext = chatsDbContext;
|
||||||
|
_messages = mongoDb.GetCollection<Message>("Messages");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orphanedMessages = await _chatsDbContext.Messages
|
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
.Include(m => m.Media)
|
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||||
.Where(m => m.IsDeleted || !_chatsDbContext.Chats.Any(c => c.Id == m.ChatId))
|
|
||||||
.ToListAsync(cancellationToken);
|
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 orphanedMessagesCount = orphanedMessages.Count;
|
||||||
|
|
||||||
var allMinioFiles = (await request.FileStorage.ListFilesAsync()).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 allUsers = await request.IdentityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var validUrls = new HashSet<string>();
|
var validUrls = new HashSet<string>();
|
||||||
|
|
||||||
var activeMessageUrls = keptMessages
|
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||||
.Where(m => m.Media != null)
|
.Where(m => m.Media != null)
|
||||||
.SelectMany(m => m.Media)
|
.SelectMany(m => m.Media)
|
||||||
.Select(me => me.Url)
|
.Select(me => me.Url)
|
||||||
|
|||||||
+13
-16
@@ -6,8 +6,8 @@ using MediatR;
|
|||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Host.Models;
|
using Host.Models;
|
||||||
using Knot.Modules.Identity.Domain;
|
using Knot.Modules.Identity.Domain;
|
||||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
using Knot.Modules.Chats.Domain;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
namespace Host.Application.Admin.Queries;
|
namespace Host.Application.Admin.Queries;
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@ public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDetailsDto>;
|
|||||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
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;
|
_userRepository = userRepository;
|
||||||
_chatsDbContext = chatsDbContext;
|
_messages = mongoDatabase.GetCollection<Message>("Messages");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
|
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);
|
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
|
var messagesCount = userMessages.Count;
|
||||||
.AsNoTracking()
|
|
||||||
.Where(m => m.SenderId == request.UserId)
|
var allUserMedia = userMessages.OfType<MediaMessage>().SelectMany(m => m.Media).ToList();
|
||||||
.SelectMany(m => m.Media)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video");
|
var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video");
|
||||||
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
||||||
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
||||||
|
|
||||||
var userContents = await _chatsDbContext.Messages
|
var userContents = userMessages.OfType<TextMessage>().Select(m => m.Content)
|
||||||
.AsNoTracking()
|
.Concat(userMessages.OfType<MediaMessage>().Where(m => m.Caption != null).Select(m => m.Caption))
|
||||||
.Where(m => m.SenderId == request.UserId)
|
.ToList();
|
||||||
.Select(m => m.Content)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var linksCount = userContents.Count(c => !string.IsNullOrEmpty(c) && c.Contains("http"));
|
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?>
|
var envMappings = new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"],
|
["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"],
|
||||||
|
["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"],
|
||||||
["Jwt:Secret"] = builder.Configuration["JWT_SECRET"],
|
["Jwt:Secret"] = builder.Configuration["JWT_SECRET"],
|
||||||
["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"],
|
["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"],
|
||||||
["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"],
|
["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>();
|
var systemDb = scope.ServiceProvider.GetRequiredService<Knot.Shared.Infrastructure.Persistence.SystemDbContext>();
|
||||||
await systemDb.Database.MigrateAsync();
|
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
|
// Initialize Global Settings Cache
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Configuration.ISettingsService>();
|
var settingsService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Configuration.ISettingsService>();
|
||||||
if (settingsService is Knot.Shared.Infrastructure.Configuration.SettingsService concreteSettings)
|
if (settingsService is Knot.Shared.Infrastructure.Configuration.SettingsService concreteSettings)
|
||||||
|
|||||||
@@ -40,33 +40,32 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userIdsToFetch = new HashSet<Guid>();
|
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 latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken);
|
||||||
var mFirst = chatMessages.FirstOrDefault();
|
if (latestMessage != null)
|
||||||
if (mFirst != null)
|
|
||||||
{
|
{
|
||||||
userIdsToFetch.Add(mFirst.SenderId);
|
userIdsToFetch.Add(latestMessage.SenderId);
|
||||||
foreach (var r in mFirst.Reactions)
|
foreach (var reaction in latestMessage.Reactions)
|
||||||
{
|
{
|
||||||
userIdsToFetch.Add(r.UserId);
|
userIdsToFetch.Add(reaction.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||||
|
|
||||||
var members = new List<ChatMemberDto>();
|
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(
|
members.Add(new ChatMemberDto(
|
||||||
m.Id,
|
member.Id,
|
||||||
m.UserId,
|
member.UserId,
|
||||||
m.Role,
|
member.Role,
|
||||||
m.IsPinned,
|
member.IsPinned,
|
||||||
user != null ? new ChatUserDto(
|
user != null ? new ChatUserDto(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Username,
|
user.Username,
|
||||||
@@ -79,48 +78,47 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var messagesList = new List<ChatMessageDto>();
|
var messagesList = new List<ChatMessageDto>();
|
||||||
if (chatMessages.Any())
|
if (latestMessage != null)
|
||||||
{
|
{
|
||||||
var m = mFirst;
|
usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj);
|
||||||
usersInfo.TryGetValue(m.SenderId, out var senderObj);
|
|
||||||
|
|
||||||
var reactionsWithUser = new List<ReactionDto>();
|
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(
|
reactionsWithUser.Add(new ReactionDto(
|
||||||
r.Id,
|
reaction.Id,
|
||||||
r.Emoji,
|
reaction.Emoji,
|
||||||
r.UserId,
|
reaction.UserId,
|
||||||
rUser != null
|
reactionUser != null
|
||||||
? new MessageSenderDto(rUser.Id, rUser.Username, rUser.DisplayName, rUser.Avatar)
|
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
||||||
: new MessageSenderDto(r.UserId, "unknown", "Unknown", null)
|
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesList.Add(new ChatMessageDto(
|
messagesList.Add(new ChatMessageDto(
|
||||||
m.Id,
|
latestMessage.Id,
|
||||||
m.ChatId,
|
latestMessage.ChatId,
|
||||||
m.SenderId,
|
latestMessage.SenderId,
|
||||||
m.Content,
|
latestMessage.Content,
|
||||||
m.Type,
|
latestMessage.Type,
|
||||||
m.ReplyToId,
|
latestMessage.ReplyToId,
|
||||||
m.Quote,
|
latestMessage.Quote,
|
||||||
m.StoryId,
|
latestMessage.StoryId,
|
||||||
m.StoryMediaUrl,
|
latestMessage.StoryMediaUrl,
|
||||||
m.StoryMediaType,
|
latestMessage.StoryMediaType,
|
||||||
m.IsEdited,
|
latestMessage.IsEdited,
|
||||||
m.IsDeleted,
|
latestMessage.IsDeleted,
|
||||||
m.CreatedAt,
|
latestMessage.CreatedAt,
|
||||||
m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
||||||
senderObj != null ? new MessageSenderDto(
|
senderObj != null ? new MessageSenderDto(
|
||||||
senderObj.Id,
|
senderObj.Id,
|
||||||
senderObj.Username,
|
senderObj.Username,
|
||||||
senderObj.DisplayName,
|
senderObj.DisplayName,
|
||||||
senderObj.Avatar
|
senderObj.Avatar
|
||||||
) : new MessageSenderDto(m.SenderId, "unknown", "Unknown", null),
|
) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null),
|
||||||
reactionsWithUser,
|
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);
|
return Result.Success<ChatDto?>(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,36 +33,38 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
|||||||
var dtos = new List<ChatDto>();
|
var dtos = new List<ChatDto>();
|
||||||
bool hasFavorites = false;
|
bool hasFavorites = false;
|
||||||
|
|
||||||
foreach (var c in userChats)
|
foreach (var chat in userChats)
|
||||||
{
|
{
|
||||||
var userIdsToFetch = new HashSet<Guid>();
|
var latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken);
|
||||||
foreach (var m in c.Members)
|
|
||||||
|
if (latestMessage == null)
|
||||||
{
|
{
|
||||||
userIdsToFetch.Add(m.UserId);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chatMessages = await _messageRepository.GetChatMessagesAsync(c.Id, 1, 0, cancellationToken);
|
var userIdsToFetch = new HashSet<Guid>();
|
||||||
var mFirst = chatMessages.FirstOrDefault();
|
foreach (var member in chat.Members)
|
||||||
if (mFirst != null)
|
|
||||||
{
|
{
|
||||||
userIdsToFetch.Add(mFirst.SenderId);
|
userIdsToFetch.Add(member.UserId);
|
||||||
foreach (var r in mFirst.Reactions)
|
}
|
||||||
{
|
|
||||||
userIdsToFetch.Add(r.UserId);
|
userIdsToFetch.Add(latestMessage.SenderId);
|
||||||
}
|
foreach (var r in latestMessage.Reactions)
|
||||||
|
{
|
||||||
|
userIdsToFetch.Add(r.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||||
|
|
||||||
var members = new List<ChatMemberDto>();
|
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(
|
members.Add(new ChatMemberDto(
|
||||||
m.Id,
|
member.Id,
|
||||||
m.UserId,
|
member.UserId,
|
||||||
m.Role,
|
member.Role,
|
||||||
m.IsPinned,
|
member.IsPinned,
|
||||||
user != null ? new ChatUserDto(
|
user != null ? new ChatUserDto(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Username,
|
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(reaction.UserId, out var reactionUser);
|
||||||
usersInfo.TryGetValue(m.SenderId, out var senderObj);
|
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>();
|
var messagesList = new List<ChatMessageDto>
|
||||||
foreach (var r in m.Reactions)
|
{
|
||||||
{
|
new ChatMessageDto(
|
||||||
usersInfo.TryGetValue(r.UserId, out var rUser);
|
latestMessage.Id,
|
||||||
reactionsWithUser.Add(new ReactionDto(
|
latestMessage.ChatId,
|
||||||
r.Id,
|
latestMessage.SenderId,
|
||||||
r.Emoji,
|
latestMessage.Content,
|
||||||
r.UserId,
|
latestMessage.Type,
|
||||||
rUser != null
|
latestMessage.ReplyToId,
|
||||||
? new MessageSenderDto(rUser.Id, rUser.Username, rUser.DisplayName, rUser.Avatar)
|
latestMessage.Quote,
|
||||||
: new MessageSenderDto(r.UserId, "unknown", "Unknown", null)
|
latestMessage.StoryId,
|
||||||
));
|
latestMessage.StoryMediaUrl,
|
||||||
}
|
latestMessage.StoryMediaType,
|
||||||
|
latestMessage.IsEdited,
|
||||||
messagesList.Add(new ChatMessageDto(
|
latestMessage.IsDeleted,
|
||||||
m.Id,
|
latestMessage.CreatedAt,
|
||||||
m.ChatId,
|
latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
||||||
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(),
|
|
||||||
senderObj != null ? new MessageSenderDto(
|
senderObj != null ? new MessageSenderDto(
|
||||||
senderObj.Id,
|
senderObj.Id,
|
||||||
senderObj.Username,
|
senderObj.Username,
|
||||||
senderObj.DisplayName,
|
senderObj.DisplayName,
|
||||||
senderObj.Avatar
|
senderObj.Avatar
|
||||||
) : new MessageSenderDto(m.SenderId, "unknown", "Unknown", null),
|
) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null),
|
||||||
reactionsWithUser,
|
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(
|
dtos.Add(new ChatDto(
|
||||||
c.Id,
|
chat.Id,
|
||||||
c.Type.ToString().ToLowerInvariant(),
|
chat.Type.ToString().ToLowerInvariant(),
|
||||||
c.Type == ChatType.Favorites ? "Избранное" : (c.Type == ChatType.Personal ? null : c.Name),
|
chat.Type == ChatType.Favorites ? "Избранное" : (chat.Type == ChatType.Personal ? null : chat.Name),
|
||||||
c.Description,
|
chat.Description,
|
||||||
c.Avatar,
|
chat.Avatar,
|
||||||
c.CreatedAt,
|
chat.CreatedAt,
|
||||||
members,
|
members,
|
||||||
messagesList,
|
messagesList,
|
||||||
unreadCount
|
unreadCount
|
||||||
@@ -146,3 +145,4 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
|||||||
return Result.Success(sorted);
|
return Result.Success(sorted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -50,9 +50,9 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
|
|||||||
{
|
{
|
||||||
message.DeleteForUser(request.UserId);
|
message.DeleteForUser(request.UserId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
await _messageRepository.UpdateAsync(message, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
if (request.DeleteForAll)
|
if (request.DeleteForAll)
|
||||||
{
|
{
|
||||||
|
|||||||
+44
-50
@@ -47,46 +47,39 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
|||||||
var userIdsToFetch = new HashSet<Guid>();
|
var userIdsToFetch = new HashSet<Guid>();
|
||||||
var replyMessages = new Dictionary<Guid, Message>();
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
userIdsToFetch.Add(m.SenderId);
|
var replyMsg = await _messageRepository.GetByIdAsync(m.ReplyToId.Value, cancellationToken);
|
||||||
if (m.ForwardedFromId.HasValue)
|
if (replyMsg == null)
|
||||||
{
|
{
|
||||||
userIdsToFetch.Add(m.ForwardedFromId.Value);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var r in m.Reactions)
|
replyMessages[replyMsg.Id] = replyMsg;
|
||||||
{
|
userIdsToFetch.Add(replyMsg.SenderId);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var senders = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplyToMessageDto? replyToObj = null;
|
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)
|
var senderObj = senders.TryGetValue(replyMsg.SenderId, out var rs)
|
||||||
? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar)
|
? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar)
|
||||||
@@ -94,48 +87,48 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
|||||||
|
|
||||||
replyToObj = new ReplyToMessageDto(
|
replyToObj = new ReplyToMessageDto(
|
||||||
replyMsg.Id,
|
replyMsg.Id,
|
||||||
replyMsg.Content,
|
replyMsg.Content,
|
||||||
replyMsg.IsDeleted,
|
replyMsg.IsDeleted,
|
||||||
replyMsg.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList(),
|
replyMsg.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList(),
|
||||||
senderObj
|
senderObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reactionsWithUser = new List<MessageReactionDto>();
|
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)
|
var userObj = senders.TryGetValue(reaction.UserId, out var reactionUser)
|
||||||
? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar)
|
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
||||||
: new MessageSenderDto(r.UserId, "unknown", "Unknown", null);
|
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null);
|
||||||
|
|
||||||
reactionsWithUser.Add(new MessageReactionDto(
|
reactionsWithUser.Add(new MessageReactionDto(
|
||||||
r.Id,
|
reaction.Id,
|
||||||
r.Emoji,
|
reaction.Emoji,
|
||||||
r.UserId,
|
reaction.UserId,
|
||||||
userObj
|
userObj
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(new MessageDetailDto(
|
result.Add(new MessageDetailDto(
|
||||||
m.Id,
|
message.Id,
|
||||||
m.ChatId,
|
message.ChatId,
|
||||||
m.SenderId,
|
message.SenderId,
|
||||||
m.Content,
|
message.Content,
|
||||||
m.Type,
|
message.Type,
|
||||||
m.ReplyToId,
|
message.ReplyToId,
|
||||||
replyToObj,
|
replyToObj,
|
||||||
m.Quote,
|
message.Quote,
|
||||||
m.IsEdited,
|
message.IsEdited,
|
||||||
m.IsDeleted,
|
message.IsDeleted,
|
||||||
m.CreatedAt,
|
message.CreatedAt,
|
||||||
m.ForwardedFromId,
|
message.ForwardedFromId,
|
||||||
m.ForwardedFromId.HasValue && senders.TryGetValue(m.ForwardedFromId.Value, out var fwd) ? new MessageSenderDto(fwd.Id, fwd.Username, fwd.DisplayName, fwd.Avatar) : null,
|
message.ForwardedFromId.HasValue && senders.TryGetValue(message.ForwardedFromId.Value, out var fwdUser) ? new MessageSenderDto(fwdUser.Id, fwdUser.Username, fwdUser.DisplayName, fwdUser.Avatar) : null,
|
||||||
m.StoryId,
|
message.StoryId,
|
||||||
m.StoryMediaUrl,
|
message.StoryMediaUrl,
|
||||||
m.StoryMediaType,
|
message.StoryMediaType,
|
||||||
m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
message.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,
|
senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : null,
|
||||||
m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList(),
|
message.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList(),
|
||||||
reactionsWithUser
|
reactionsWithUser
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -143,3 +136,4 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
|||||||
return Result.Success(result);
|
return Result.Success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-28
@@ -31,36 +31,38 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
|||||||
public async Task<Result<List<SharedMediaDto>>> Handle(GetSharedMediaQuery request, CancellationToken cancellationToken)
|
public async Task<Result<List<SharedMediaDto>>> Handle(GetSharedMediaQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, 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);
|
return Result.Failure<List<SharedMediaDto>>(ChatErrors.ChatsForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
var messages = await _messageRepository.GetChatMessagesAsync(request.ChatId, ChatConstants.MaxSharedMediaQueryLimit, 0, cancellationToken);
|
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 result = new List<SharedMediaDto>();
|
||||||
var filterType = request.Type?.ToLower();
|
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);
|
var senders = await _userProvider.GetUsersInfoAsync(userIds, cancellationToken);
|
||||||
|
|
||||||
foreach (var m in messages)
|
foreach (var message in messages)
|
||||||
{
|
{
|
||||||
if (filterType == "links")
|
if (filterType == "links")
|
||||||
{
|
{
|
||||||
|
var messageContent = message.Content;
|
||||||
var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase);
|
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 contentLinks = !string.IsNullOrEmpty(messageContent) ? linkRegex.Matches(messageContent).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 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();
|
var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList();
|
||||||
|
|
||||||
if (allLinks.Any())
|
if (allLinks.Any())
|
||||||
{
|
{
|
||||||
senders.TryGetValue(m.SenderId, out var sender);
|
senders.TryGetValue(message.SenderId, out var sender);
|
||||||
result.Add(new SharedMediaDto(
|
result.Add(new SharedMediaDto(
|
||||||
m.Id,
|
message.Id,
|
||||||
m.Content,
|
messageContent,
|
||||||
m.CreatedAt,
|
message.CreatedAt,
|
||||||
allLinks,
|
allLinks,
|
||||||
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
|
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
|
||||||
null, null, null, null, null, null, null, null
|
null, null, null, null, null, null, null, null
|
||||||
@@ -69,21 +71,17 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m.Media == null || !m.Media.Any())
|
var messageMedia = message.Media;
|
||||||
|
if (messageMedia == null || !messageMedia.Any())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filteredMedia = m.Media.Where(media =>
|
var filteredMedia = messageMedia.Where(media =>
|
||||||
{
|
{
|
||||||
var mediaType = media.Type?.ToLower() ?? "file";
|
var mediaType = media.Type?.ToLower() ?? "file";
|
||||||
var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
|
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")
|
if (filterType == "gifs")
|
||||||
{
|
{
|
||||||
return isGif;
|
return isGif;
|
||||||
@@ -99,20 +97,20 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
|||||||
|
|
||||||
if (filteredMedia.Any())
|
if (filteredMedia.Any())
|
||||||
{
|
{
|
||||||
senders.TryGetValue(m.SenderId, out var sender);
|
senders.TryGetValue(message.SenderId, out var sender);
|
||||||
result.Add(new SharedMediaDto(
|
result.Add(new SharedMediaDto(
|
||||||
m.Id,
|
message.Id,
|
||||||
m.Content,
|
message.Content,
|
||||||
m.CreatedAt,
|
message.CreatedAt,
|
||||||
null,
|
null,
|
||||||
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
|
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
|
||||||
m.ReplyToId,
|
message.ReplyToId,
|
||||||
m.Quote,
|
message.Quote,
|
||||||
m.StoryId,
|
message.StoryId,
|
||||||
m.StoryMediaUrl,
|
message.StoryMediaUrl,
|
||||||
m.StoryMediaType,
|
message.StoryMediaType,
|
||||||
m.IsEdited,
|
message.IsEdited,
|
||||||
m.Type,
|
message.Type,
|
||||||
filteredMedia.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList()
|
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());
|
return Result.Success(result.OrderByDescending(x => x.CreatedAt).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-23
@@ -26,35 +26,36 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
|
|||||||
public async Task<Result<List<SearchMessageDto>>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken)
|
public async Task<Result<List<SearchMessageDto>>> Handle(SearchMessagesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var messages = await _messageRepository.SearchMessagesAsync(request.Query, request.ChatId, request.UserId, 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();
|
var userIds = messages.Select(message => message.SenderId).ToList();
|
||||||
userIds.AddRange(messages.Where(m => m.ForwardedFromId.HasValue).Select(m => m.ForwardedFromId!.Value));
|
userIds.AddRange(messages.Where(message => message.ForwardedFromId.HasValue).Select(message => message.ForwardedFromId!.Value));
|
||||||
|
|
||||||
var senders = await _userProvider.GetUsersInfoAsync(userIds.Distinct(), cancellationToken);
|
var senders = await _userProvider.GetUsersInfoAsync(userIds.Distinct(), cancellationToken);
|
||||||
|
|
||||||
var result = messages.Select(m => new SearchMessageDto(
|
var result = messages.Select(message => new SearchMessageDto(
|
||||||
m.Id,
|
message.Id,
|
||||||
m.ChatId,
|
message.ChatId,
|
||||||
m.SenderId,
|
message.SenderId,
|
||||||
m.Content,
|
message.Content,
|
||||||
m.Type,
|
message.Type,
|
||||||
m.ReplyToId,
|
message.ReplyToId,
|
||||||
m.Quote,
|
message.Quote,
|
||||||
m.IsEdited,
|
message.IsEdited,
|
||||||
m.IsDeleted,
|
message.IsDeleted,
|
||||||
m.CreatedAt,
|
message.CreatedAt,
|
||||||
m.ForwardedFromId,
|
message.ForwardedFromId,
|
||||||
m.ForwardedFromId.HasValue && senders.TryGetValue(m.ForwardedFromId.Value, out var fwd) ? new MessageSenderDto(fwd.Id, fwd.Username, fwd.DisplayName, fwd.Avatar) : null,
|
null,
|
||||||
m.StoryId,
|
message.StoryId,
|
||||||
m.StoryMediaUrl,
|
message.StoryMediaUrl,
|
||||||
m.StoryMediaType,
|
message.StoryMediaType,
|
||||||
m.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
|
message.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),
|
senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null),
|
||||||
m.Reactions.Select(r => new SimpleReactionDto(r.UserId, r.Emoji)).ToList(),
|
message.Reactions.Select(reaction => new SimpleReactionDto(reaction.UserId, reaction.Emoji)).ToList(),
|
||||||
m.ReadBy.Select(r => new ReadByDto(r.UserId)).ToList()
|
message.ReadBy.Select(readReceipt => new ReadByDto(readReceipt.UserId)).ToList()
|
||||||
)).ToList();
|
)).ToList();
|
||||||
|
|
||||||
return Result.Success(result);
|
return Result.Success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-15
@@ -27,15 +27,18 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
private readonly IChatRepository _chatRepository;
|
private readonly IChatRepository _chatRepository;
|
||||||
private readonly IMessageRepository _messageRepository;
|
private readonly IMessageRepository _messageRepository;
|
||||||
private readonly IChatsUnitOfWork _unitOfWork;
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly MediatR.IMediator _mediator;
|
||||||
|
|
||||||
public SendMessageCommandHandler(
|
public SendMessageCommandHandler(
|
||||||
IChatRepository chatRepository,
|
IChatRepository chatRepository,
|
||||||
IMessageRepository messageRepository,
|
IMessageRepository messageRepository,
|
||||||
IChatsUnitOfWork unitOfWork)
|
IChatsUnitOfWork unitOfWork,
|
||||||
|
MediatR.IMediator mediator)
|
||||||
{
|
{
|
||||||
_chatRepository = chatRepository;
|
_chatRepository = chatRepository;
|
||||||
_messageRepository = messageRepository;
|
_messageRepository = messageRepository;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
||||||
@@ -54,30 +57,71 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Создаем сообщение
|
// 3. Создаем сообщение
|
||||||
var message = Message.Create(
|
Message message;
|
||||||
request.ChatId,
|
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
|
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)
|
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. Сохраняем
|
// 4. Сохраняем
|
||||||
_messageRepository.Add(message);
|
_messageRepository.Add(message);
|
||||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await _mediator.Publish(new MessageSentDomainEvent(
|
||||||
|
message.Id,
|
||||||
|
message.ChatId,
|
||||||
|
message.SenderId,
|
||||||
|
message.Content),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
return Result.Success(message.Id);
|
return Result.Success(message.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -350,16 +350,24 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
|
|||||||
|
|
||||||
bool isJoined = fromNameNode == null;
|
bool isJoined = fromNameNode == null;
|
||||||
bool isMediaOnly = string.IsNullOrEmpty(content) && forwardedNode == null && replyToId == null;
|
bool isMediaOnly = string.IsNullOrEmpty(content) && forwardedNode == null && replyToId == null;
|
||||||
|
bool hasMedia = mediaNodes != null && mediaNodes.Count > 0;
|
||||||
Message? targetMessage = null;
|
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;
|
targetMessage = lastSavedMessage;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string finalContent = content;
|
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");
|
var idAttr = node.GetAttribute("id");
|
||||||
if (!string.IsNullOrEmpty(idAttr))
|
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");
|
string? href = mediaNode.GetAttribute("href") ?? mediaNode.GetAttribute("src");
|
||||||
if (!string.IsNullOrEmpty(href) && !href.StartsWith("http"))
|
if (!string.IsNullOrEmpty(href) && !href.StartsWith("http"))
|
||||||
@@ -391,8 +399,12 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
|
|||||||
finalMType = "image";
|
finalMType = "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType);
|
var parsedType = Enum.TryParse<MediaType>(finalMType, true, out var mTypeEnum) ? mTypeEnum : MediaType.File;
|
||||||
targetMessage.AddMedia(finalMType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length);
|
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));
|
return Result.Success(new ExecuteImportResponseDto(true, importedCount, chatId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
|
||||||
using Knot.Modules.Chats.Domain;
|
using Knot.Modules.Chats.Domain;
|
||||||
using Knot.Shared.Kernel;
|
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;
|
using Knot.Modules.Chats.Application.Abstractions;
|
||||||
|
|
||||||
@@ -24,12 +26,20 @@ public static class DependencyInjection
|
|||||||
services.AddDbContext<ChatsDbContext>(options =>
|
services.AddDbContext<ChatsDbContext>(options =>
|
||||||
options.UseNpgsql(connectionString));
|
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<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
|
||||||
services.AddScoped<IChatRepository, ChatRepository>();
|
services.AddScoped<IChatRepository, ChatRepository>();
|
||||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||||
|
|
||||||
// Регистрация MediatR для этого модуля
|
// MediatR
|
||||||
services.AddMediatR(config =>
|
services.AddMediatR(config =>
|
||||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
void Add(Message message);
|
||||||
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
|
||||||
Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, 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<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
|
||||||
Task AddReadReceiptsAsync(Guid userId, List<Guid> messageIds, CancellationToken cancellationToken);
|
Task AddReadReceiptsAsync(Guid userId, List<Guid> messageIds, CancellationToken cancellationToken);
|
||||||
Task<bool> AddReactionAsync(Guid messageId, Guid userId, string emoji, 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<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken);
|
||||||
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
|
||||||
Task<int> GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken);
|
Task<int> GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken);
|
||||||
|
Task UpdateAsync(Message message, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
using Knot.Shared.Kernel;
|
||||||
|
|
||||||
namespace Knot.Modules.Chats.Domain;
|
namespace Knot.Modules.Chats.Domain;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Доменное событие: сообщение отправлено.
|
/// Абстрактная база агрегата Сообщение.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record MessageSentDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : IDomainEvent;
|
public abstract class Message : AggregateRoot<Guid>
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Сущность сообщения (Агрегат).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Message : AggregateRoot<Guid>
|
|
||||||
{
|
{
|
||||||
public Guid ChatId { get; private set; }
|
// ================== Базовые поля ==================
|
||||||
public Guid SenderId { get; private set; }
|
public Guid ChatId { get; protected set; }
|
||||||
public string? Content { get; private set; }
|
public Guid SenderId { get; protected set; }
|
||||||
public string Type { get; private set; } // text, image, video, voice, file
|
public DateTime CreatedAt { get; protected set; }
|
||||||
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; }
|
|
||||||
|
|
||||||
private readonly List<Media> _media = new();
|
// ================== Опциональные метаданные (общего назначения) ==================
|
||||||
public IReadOnlyCollection<Media> Media => _media.AsReadOnly();
|
public Guid? ReplyToId { get; protected set; }
|
||||||
|
public Guid? ForwardedFromId { get; protected set; }
|
||||||
|
|
||||||
private readonly List<ReadReceipt> _readBy = new();
|
// ================== Флаги ==================
|
||||||
|
public MessageState State { get; protected set; }
|
||||||
|
|
||||||
|
// ================== Абстрактные / Виртуальные свойства ==================
|
||||||
|
public abstract string Type { get; }
|
||||||
|
public abstract string? Content { get; protected set; }
|
||||||
|
|
||||||
|
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>();
|
||||||
|
|
||||||
|
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();
|
public IReadOnlyCollection<ReadReceipt> ReadBy => _readBy.AsReadOnly();
|
||||||
|
|
||||||
private readonly List<Guid> _deletedByUsers = new();
|
protected readonly List<DeletedMessage> _deletedFor = new();
|
||||||
public IReadOnlyCollection<Guid> DeletedByUsers => _deletedByUsers.AsReadOnly();
|
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;
|
ChatId = chatId;
|
||||||
SenderId = senderId;
|
SenderId = senderId;
|
||||||
Content = content;
|
|
||||||
Type = type;
|
|
||||||
ReplyToId = replyToId;
|
ReplyToId = replyToId;
|
||||||
Quote = quote;
|
|
||||||
ForwardedFromId = forwardedFromId;
|
ForwardedFromId = forwardedFromId;
|
||||||
StoryId = storyId;
|
|
||||||
StoryMediaUrl = storyMediaUrl;
|
|
||||||
StoryMediaType = storyMediaType;
|
|
||||||
CreatedAt = createdAt;
|
CreatedAt = createdAt;
|
||||||
IsImported = isImported;
|
|
||||||
|
|
||||||
if (!isImported) // Don't trigger realtime events for historic messages
|
if (isImported) AddState(MessageState.IsImported);
|
||||||
{
|
|
||||||
RaiseDomainEvent(new MessageSentDomainEvent(Id, ChatId, SenderId, Content));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
// ================== Управление Состоянием ==================
|
||||||
{
|
public void AddState(MessageState state) => State |= state;
|
||||||
return new Message(Guid.NewGuid(), chatId, senderId, content, type, replyToId, quote, forwardedFromId, storyId, storyMediaUrl, storyMediaType, DateTime.UtcNow, false);
|
public void RemoveState(MessageState state) => State &= ~state;
|
||||||
}
|
public bool HasState(MessageState state) => (State & state) == state;
|
||||||
|
|
||||||
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 AddReaction(Guid userId, string emoji)
|
public void AddReaction(Guid userId, string emoji)
|
||||||
{
|
{
|
||||||
var existing = _reactions.Find(r => r.UserId == userId && r.Emoji == 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;
|
AddState(MessageState.IsDeleted);
|
||||||
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
|
|
||||||
_reactions.Clear();
|
_reactions.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteForUser(Guid userId)
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-6
@@ -77,14 +77,14 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
|||||||
message.Id,
|
message.Id,
|
||||||
message.ChatId,
|
message.ChatId,
|
||||||
message.SenderId,
|
message.SenderId,
|
||||||
message.Content,
|
Content = message.Content,
|
||||||
message.Type,
|
Type = message.Type,
|
||||||
message.CreatedAt,
|
message.CreatedAt,
|
||||||
message.ForwardedFromId,
|
message.ForwardedFromId,
|
||||||
ForwardedFrom = forwardedFromObj,
|
ForwardedFrom = forwardedFromObj,
|
||||||
message.ReplyToId,
|
message.ReplyToId,
|
||||||
ReplyTo = replyToObj,
|
ReplyTo = replyToObj,
|
||||||
message.Quote,
|
Quote = message.Quote,
|
||||||
Media = message.Media.Select(m => new
|
Media = message.Media.Select(m => new
|
||||||
{
|
{
|
||||||
m.Id,
|
m.Id,
|
||||||
@@ -95,9 +95,10 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
|||||||
}).ToList(),
|
}).ToList(),
|
||||||
Sender = senderObj,
|
Sender = senderObj,
|
||||||
ReadBy = new List<object>(),
|
ReadBy = new List<object>(),
|
||||||
message.StoryId,
|
StoryId = message.StoryId,
|
||||||
message.StoryMediaUrl,
|
StoryMediaUrl = message.StoryMediaUrl,
|
||||||
message.StoryMediaType
|
StoryMediaType = message.StoryMediaType
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
|
|||||||
using Knot.Modules.Chats.Application.Abstractions;
|
using Knot.Modules.Chats.Application.Abstractions;
|
||||||
using Knot.Modules.Chats.Domain;
|
using Knot.Modules.Chats.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
|
using Knot.Shared.Kernel.Security;
|
||||||
|
|
||||||
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
|||||||
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
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)
|
: base(options)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -23,9 +24,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Chat> Chats => Set<Chat>();
|
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)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
@@ -54,61 +53,6 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
|
|||||||
}).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field);
|
}).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)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
|||||||
+124
-79
@@ -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.Modules.Chats.Domain;
|
||||||
|
using Knot.Shared.Kernel;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
|
||||||
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
namespace Knot.Modules.Chats.Infrastructure.Persistence;
|
||||||
|
|
||||||
public sealed class MessageRepository : IMessageRepository
|
public sealed class MessageRepository : IMessageRepository
|
||||||
{
|
{
|
||||||
|
private readonly IMongoCollection<Message> _messages;
|
||||||
private readonly ChatsDbContext _dbContext;
|
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;
|
_dbContext = dbContext;
|
||||||
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(Message message)
|
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)
|
public async Task<Message?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _dbContext.Messages
|
var filter = Builders<Message>.Filter.Eq(m => m.Id, id);
|
||||||
.Include(m => m.Media)
|
return await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken);
|
||||||
.Include(m => m.ReadBy)
|
|
||||||
.Include(m => m.Reactions)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken)
|
public async Task<List<Message>> GetChatMessagesAsync(Guid chatId, int limit, int offset, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _dbContext.Messages
|
var filter = Builders<Message>.Filter.Eq(m => m.ChatId, chatId);
|
||||||
.Where(m => m.ChatId == chatId)
|
return await _messages.Find(filter)
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
.SortByDescending(m => m.CreatedAt)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(limit)
|
.Limit(limit)
|
||||||
.Include(m => m.Media)
|
|
||||||
.Include(m => m.ReadBy)
|
|
||||||
.Include(m => m.Reactions)
|
|
||||||
.ToListAsync(cancellationToken);
|
.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)
|
public async Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _dbContext.Messages
|
var builder = Builders<Message>.Filter;
|
||||||
.Where(m => m.ChatId == chatId);
|
var filter = builder.Eq(m => m.ChatId, chatId);
|
||||||
|
|
||||||
if (cursor.HasValue)
|
if (cursor.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(m => m.CreatedAt < cursor.Value);
|
filter &= builder.Lt(m => m.CreatedAt, cursor.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query
|
return await _messages.Find(filter)
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
.SortByDescending(m => m.CreatedAt)
|
||||||
.Take(limit)
|
.Limit(limit)
|
||||||
.Include(m => m.Media)
|
|
||||||
.Include(m => m.ReadBy)
|
|
||||||
.Include(m => m.Reactions)
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken 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
|
var validChatIdsQuery = _dbContext.Chats
|
||||||
.Where(c => c.Members.Any(m => m.UserId == requestingUserId))
|
.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)
|
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))
|
return await _messages.Find(filter)
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
.SortByDescending(m => m.CreatedAt)
|
||||||
.Take(ChatConstants.SearchMessagesLimit)
|
.Limit(ChatConstants.SearchMessagesLimit)
|
||||||
.Include(m => m.Media)
|
|
||||||
.Include(m => m.ReadBy)
|
|
||||||
.Include(m => m.Reactions)
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddReadReceiptsAsync(Guid userId, List<Guid> messageIds, CancellationToken cancellationToken)
|
public async Task AddReadReceiptsAsync(Guid userId, List<Guid> messageIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var existingReceipts = await _dbContext.ReadReceipts
|
var filter = Builders<Message>.Filter.In(m => m.Id, messageIds);
|
||||||
.Where(r => r.UserId == userId && messageIds.Contains(r.MessageId))
|
|
||||||
.Select(r => r.MessageId)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var newReceipts = messageIds
|
var messages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||||
.Except(existingReceipts)
|
|
||||||
.Select(id => new ReadReceipt(id, userId));
|
|
||||||
|
|
||||||
_dbContext.ReadReceipts.AddRange(newReceipts);
|
var writes = new List<WriteModel<Message>>();
|
||||||
// SaveChangesAsync будет вызван в handlers или через UnitOfWork, но если мы здесь
|
foreach (var msg in messages)
|
||||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writes.Any())
|
||||||
|
{
|
||||||
|
await _messages.BulkWriteAsync(writes, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken)
|
public async Task<bool> AddReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Проверяем, существует ли сообщение
|
var filter = Builders<Message>.Filter.Eq(m => m.Id, messageId);
|
||||||
var messageExists = await _dbContext.Messages
|
var msg = await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken);
|
||||||
.AnyAsync(m => m.Id == messageId, cancellationToken);
|
if (msg == null) return false;
|
||||||
|
|
||||||
|
if (msg.Reactions.Any(r => r.UserId == userId && r.Emoji == emoji))
|
||||||
if (!messageExists)
|
|
||||||
{
|
{
|
||||||
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);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> RemoveReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken)
|
public async Task<bool> RemoveReactionAsync(Guid messageId, Guid userId, string emoji, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Ищем реакцию
|
var filter = Builders<Message>.Filter.Eq(m => m.Id, messageId);
|
||||||
var reaction = await _dbContext.Reactions
|
var msg = await _messages.Find(filter).FirstOrDefaultAsync(cancellationToken);
|
||||||
.FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken);
|
if (msg == null) return false;
|
||||||
|
|
||||||
|
var reaction = msg.Reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji);
|
||||||
|
if (reaction == null) return false;
|
||||||
|
|
||||||
if (reaction == null)
|
var update = Builders<Message>.Update.PullFilter("Reactions",
|
||||||
{
|
Builders<BsonDocument>.Filter.And(
|
||||||
return false;
|
Builders<BsonDocument>.Filter.Eq("UserId", userId),
|
||||||
}
|
Builders<BsonDocument>.Filter.Eq("Emoji", emoji)
|
||||||
|
));
|
||||||
_dbContext.Reactions.Remove(reaction);
|
|
||||||
|
|
||||||
|
|
||||||
|
await _messages.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken)
|
public async Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _dbContext.Messages
|
var filter = Builders<Message>.Filter.And(
|
||||||
.Where(m => m.ChatId == chatId && m.StoryId == storyId)
|
Builders<Message>.Filter.Eq(m => m.ChatId, chatId),
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
Builders<Message>.Filter.Eq("_t", "StoryMessage"),
|
||||||
|
Builders<Message>.Filter.Eq("StoryId", storyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return await _messages.Find(filter)
|
||||||
|
.SortByDescending(m => m.CreatedAt)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken)
|
public async Task<int> GetUnreadCountAsync(Guid chatId, Guid userId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _dbContext.Messages
|
var notReadFilter = Builders<Message>.Filter.Not(
|
||||||
.Where(m => m.ChatId == chatId && m.SenderId != userId && !m.ReadBy.Any(r => r.UserId == userId))
|
Builders<Message>.Filter.ElemMatch("ReadBy",
|
||||||
.CountAsync(cancellationToken);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-207
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-52
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-210
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-239
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-52
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-244
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-251
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-64
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-254
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-260
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-42
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-263
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-30
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-266
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-266
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -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)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-263
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+49
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -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.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Knot.Modules.Chats.Application.Messages.Send;
|
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.Modules.Chats.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
@@ -124,7 +127,7 @@ public sealed class ChatHub : Hub
|
|||||||
|
|
||||||
if (parsedIds.Any())
|
if (parsedIds.Any())
|
||||||
{
|
{
|
||||||
var command = new Knot.Modules.Chats.Application.Messages.Read.ReadMessagesCommand(
|
var command = new ReadMessagesCommand(
|
||||||
request.ChatId, _userContext.UserId, parsedIds);
|
request.ChatId, _userContext.UserId, parsedIds);
|
||||||
await _sender.Send(command);
|
await _sender.Send(command);
|
||||||
}
|
}
|
||||||
@@ -148,7 +151,7 @@ public sealed class ChatHub : Hub
|
|||||||
|
|
||||||
if (parsedIds.Any())
|
if (parsedIds.Any())
|
||||||
{
|
{
|
||||||
var command = new Knot.Modules.Chats.Application.Messages.Delete.DeleteMessagesCommand(
|
var command = new DeleteMessagesCommand(
|
||||||
request.ChatId, _userContext.UserId, parsedIds, request.DeleteForAll);
|
request.ChatId, _userContext.UserId, parsedIds, request.DeleteForAll);
|
||||||
await _sender.Send(command);
|
await _sender.Send(command);
|
||||||
}
|
}
|
||||||
@@ -188,7 +191,7 @@ public sealed class ChatHub : Hub
|
|||||||
request.MessageId, request.ChatId, request.Emoji, _userContext.UserId);
|
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);
|
request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
|
||||||
var result = await _sender.Send(command);
|
var result = await _sender.Send(command);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
@@ -209,7 +212,7 @@ public sealed class ChatHub : Hub
|
|||||||
request.MessageId, request.ChatId, request.Emoji, _userContext.UserId);
|
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);
|
request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
|
||||||
var result = await _sender.Send(command);
|
var result = await _sender.Send(command);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.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="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
+10
-81
@@ -1,18 +1,18 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
|
using Knot.Modules.Chats.Infrastructure.Persistence;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Knot.Modules.Chats.Infrastructure.Persistence;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
namespace Knot.Modules.Chats.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ChatsDbContext))]
|
[DbContext(typeof(ChatsDbContext))]
|
||||||
[Migration("20260311180825_InitialChats")]
|
[Migration("20260319124845_InitialChats")]
|
||||||
partial class InitialChats
|
partial class InitialChats
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -38,6 +38,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
@@ -50,45 +53,6 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("Chats", "chats");
|
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 =>
|
modelBuilder.Entity("Knot.Modules.Chats.Domain.Chat", b =>
|
||||||
{
|
{
|
||||||
b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 =>
|
b.OwnsMany("Knot.Modules.Chats.Domain.ChatMember", "Members", b1 =>
|
||||||
@@ -103,6 +67,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
b1.Property<bool>("IsMuted")
|
b1.Property<bool>("IsMuted")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsPinned")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b1.Property<DateTime>("JoinedAt")
|
b1.Property<DateTime>("JoinedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -126,44 +93,6 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.Navigation("Members");
|
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
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-61
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
namespace Knot.Modules.Chats.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class InitialChats : Migration
|
public partial class InitialChats : Migration
|
||||||
@@ -22,6 +22,7 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
Type = table.Column<string>(type: "text", nullable: false),
|
Type = table.Column<string>(type: "text", nullable: false),
|
||||||
Name = table.Column<string>(type: "text", nullable: true),
|
Name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: true),
|
||||||
Avatar = 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)
|
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);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "ChatMembers",
|
name: "ChatMembers",
|
||||||
schema: "chats",
|
schema: "chats",
|
||||||
@@ -61,6 +41,7 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
Role = table.Column<string>(type: "text", nullable: false),
|
Role = table.Column<string>(type: "text", nullable: false),
|
||||||
JoinedAt = table.Column<DateTime>(type: "timestamp with time zone", 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)
|
IsMuted = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
@@ -75,42 +56,12 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
onDelete: ReferentialAction.Cascade);
|
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(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_ChatMembers_ChatId_UserId",
|
name: "IX_ChatMembers_ChatId_UserId",
|
||||||
schema: "chats",
|
schema: "chats",
|
||||||
table: "ChatMembers",
|
table: "ChatMembers",
|
||||||
columns: new[] { "ChatId", "UserId" },
|
columns: new[] { "ChatId", "UserId" },
|
||||||
unique: true);
|
unique: true);
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_MessageMedia_MessageId",
|
|
||||||
schema: "chats",
|
|
||||||
table: "MessageMedia",
|
|
||||||
column: "MessageId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -120,17 +71,9 @@ namespace Knot.Modules.Chats.Infrastructure.Persistence.Migrations
|
|||||||
name: "ChatMembers",
|
name: "ChatMembers",
|
||||||
schema: "chats");
|
schema: "chats");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "MessageMedia",
|
|
||||||
schema: "chats");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Chats",
|
name: "Chats",
|
||||||
schema: "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
-1
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using MediatR;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
@@ -18,6 +19,7 @@ public class SendMessageCommandHandlerTests
|
|||||||
private readonly IChatRepository _chatRepository;
|
private readonly IChatRepository _chatRepository;
|
||||||
private readonly IMessageRepository _messageRepository;
|
private readonly IMessageRepository _messageRepository;
|
||||||
private readonly IChatsUnitOfWork _unitOfWork;
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
private readonly SendMessageCommandHandler _handler;
|
private readonly SendMessageCommandHandler _handler;
|
||||||
|
|
||||||
public SendMessageCommandHandlerTests()
|
public SendMessageCommandHandlerTests()
|
||||||
@@ -25,8 +27,9 @@ public class SendMessageCommandHandlerTests
|
|||||||
_chatRepository = Substitute.For<IChatRepository>();
|
_chatRepository = Substitute.For<IChatRepository>();
|
||||||
_messageRepository = Substitute.For<IMessageRepository>();
|
_messageRepository = Substitute.For<IMessageRepository>();
|
||||||
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||||
|
_mediator = Substitute.For<IMediator>();
|
||||||
|
|
||||||
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork);
|
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- minio
|
- minio
|
||||||
|
- mongo
|
||||||
ports:
|
ports:
|
||||||
- "5059:8080"
|
- "5059:8080"
|
||||||
|
|
||||||
@@ -36,6 +37,14 @@ services:
|
|||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
mongo:
|
||||||
|
image: mongo:6-jammy
|
||||||
|
container_name: knot-mongo
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user