Структура
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
@@ -37,7 +38,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
|
||||
public async Task<Result<MessageResponse>> Handle(CleanRunCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
@@ -55,7 +56,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
@@ -73,7 +74,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
var activeUserUrls = allUsers
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar!);
|
||||
|
||||
|
||||
var activeStoryUrls = allStories
|
||||
.Where(s => !string.IsNullOrEmpty(s.MediaUrl))
|
||||
.Select(s => s.MediaUrl!);
|
||||
|
||||
+17
-16
@@ -1,21 +1,22 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Domain;
|
||||
using Knot.Modules.Auth.Application.Auth.DTOs;
|
||||
using Knot.Modules.Auth.Application.Users;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
@@ -40,14 +41,14 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
||||
|
||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
// 1. Получаем ID активных чатов (SQL)
|
||||
var activeChats = await _chatsDbContext.Chats
|
||||
.AsNoTracking()
|
||||
.Select(c => new { c.Id, c.Avatar })
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
var activeChatIds = activeChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
// 2. Считаем сообщения подлежащие удалению (MongoDB)
|
||||
@@ -62,7 +63,7 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
||||
|
||||
// Аватары чатов и пользователей
|
||||
foreach (var chat in activeChats) AddFileIdIfValid(chat.Avatar, validFileIds);
|
||||
|
||||
|
||||
var userAvatars = await _identityDb.Users.AsNoTracking()
|
||||
.Where(u => !string.IsNullOrEmpty(u.Avatar))
|
||||
.Select(u => u.Avatar).ToListAsync(ct);
|
||||
@@ -78,9 +79,9 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
|
||||
Builders<Message>.Filter.BitsAllClear(m => m.State, (long)MessageState.IsDeleted),
|
||||
Builders<Message>.Filter.In(m => m.ChatId, activeChatIds)
|
||||
);
|
||||
|
||||
|
||||
var projection = Builders<Message>.Projection.Include("Media");
|
||||
|
||||
|
||||
using (var cursor = await _messages.Find(activeFilter).Project(projection).ToCursorAsync(ct))
|
||||
{
|
||||
while (await cursor.MoveNextAsync(ct))
|
||||
|
||||
@@ -10,11 +10,16 @@
|
||||
<ProjectReference Include="..\..\Contracts\Auth\Knot.Contracts.Auth.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Klipy\Knot.Contracts.Klipy.csproj" />
|
||||
<!-- Admin needs direct module access for cleanup operations -->
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
|
||||
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
|
||||
<ProjectReference Include="..\Klipy\Knot.Modules.Klipy.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
@@ -24,4 +29,4 @@
|
||||
<_Parameter1>Knot.Modules.Admin.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,17 +1,17 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using BCrypt.Net;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace Knot.Modules.Auth.Application.Users.Login;
|
||||
|
||||
/// <summary>
|
||||
/// Êîìàíäà äëÿ âõîäà ïîëüçîâàòåëÿ. Âîçâðàùàåò AuthResponseDto.
|
||||
/// ������� ��� ����� ������������. ���������� AuthResponseDto.
|
||||
/// </summary>
|
||||
public sealed record LoginUserCommand(string Username, string Password) : ICommand<AuthResponseDto>;
|
||||
|
||||
internal sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand, AuthResponseDto>
|
||||
public sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand, AuthResponseDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IJwtTokenProvider _tokenProvider;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Chats.GetChatById;
|
||||
|
||||
@@ -50,8 +50,8 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
}
|
||||
|
||||
var latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken);
|
||||
var latestReactions = latestMessage != null
|
||||
? await _reactionRepository.GetReactionsForMessageAsync(latestMessage.Id, cancellationToken)
|
||||
var latestReactions = latestMessage != null
|
||||
? await _reactionRepository.GetReactionsForMessageAsync(latestMessage.Id, cancellationToken)
|
||||
: new List<MessageReaction>();
|
||||
|
||||
if (latestMessage != null)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Chats.GetChats;
|
||||
@@ -39,12 +39,12 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
foreach (var chat in userChats)
|
||||
{
|
||||
var latestMessage = await _messageRepository.GetLatestChatMessageAsync(chat.Id, cancellationToken);
|
||||
var latestReactions = latestMessage != null
|
||||
var latestReactions = latestMessage != null
|
||||
? await _reactionRepository.GetReactionsForMessageAsync(latestMessage.Id, cancellationToken)
|
||||
: new List<MessageReaction>();
|
||||
|
||||
var userIdsToFetch = new HashSet<Guid>();
|
||||
|
||||
|
||||
foreach (var member in chat.Members)
|
||||
{
|
||||
userIdsToFetch.Add(member.UserId);
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using global::Knot.Modules.Conversations.Application.Abstractions;
|
||||
using global::Knot.Modules.Conversations.Domain;
|
||||
using global::Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using global::Knot.Shared.Kernel;
|
||||
using global::Knot.Modules.Conversations.Application.Abstractions;
|
||||
using MessagingMessageRepository = Knot.Modules.Messaging.Domain.IMessageRepository;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MessagingMessageRepository = Knot.Contracts.Messaging.Application.Abstractions.IMessageRepository;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Delete;
|
||||
|
||||
|
||||
+6
-7
@@ -1,16 +1,15 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.GetMessages;
|
||||
|
||||
@@ -74,7 +73,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
|
||||
}
|
||||
|
||||
var senders = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||
|
||||
|
||||
var messageIds = filteredMessages.Select(m => m.Id).ToList();
|
||||
var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken);
|
||||
var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
+5
-6
@@ -1,17 +1,16 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.GetSharedMedia;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.React;
|
||||
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.React;
|
||||
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.SearchMessages;
|
||||
|
||||
@@ -36,7 +36,7 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
|
||||
userIds.AddRange(messages.Where(message => message.ForwardedFromId.HasValue).Select(message => message.ForwardedFromId!.Value));
|
||||
|
||||
var senders = await _userProvider.GetUsersInfoAsync(userIds.Distinct(), cancellationToken);
|
||||
|
||||
|
||||
var messageIds = messages.Select(m => m.Id).ToList();
|
||||
var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken);
|
||||
var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList());
|
||||
@@ -54,7 +54,7 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
|
||||
message.CreatedAt,
|
||||
message.SequenceId,
|
||||
message.ForwardedFromId,
|
||||
null,
|
||||
null,
|
||||
message.StoryId,
|
||||
message.StoryMediaUrl,
|
||||
message.StoryMediaType,
|
||||
|
||||
+75
-44
@@ -1,14 +1,14 @@
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Send;
|
||||
|
||||
/// <summary>
|
||||
/// Êîìàíäà äëÿ îòïðàâêè ñîîáùåíèÿ â ÷àò.
|
||||
/// ������� ��� �������� ��������� � ���.
|
||||
/// </summary>
|
||||
public record AttachmentRequest(string Type, string Url, string? FileName, long? FileSize);
|
||||
|
||||
@@ -53,37 +53,47 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
|
||||
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Ïðîâåðÿåì ñóùåñòâîâàíèå ÷àòà
|
||||
// 1. ��������� ������������� ����
|
||||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
||||
if (chat is null)
|
||||
{
|
||||
return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
||||
}
|
||||
|
||||
// 2. Ïðîâåðÿåì, ÿâëÿåòñÿ ëè îòïðàâèòåëü ó÷àñòíèêîì
|
||||
// 2. ���������, �������� �� ����������� ����������
|
||||
if (!chat.Members.Any(m => m.UserId == request.SenderId))
|
||||
{
|
||||
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
||||
}
|
||||
|
||||
// 3. Ñîçäàåì ñîîáùåíèå
|
||||
// 3. ������� ���������
|
||||
Message message;
|
||||
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
||||
{
|
||||
if (!_messagesSettings.Current.AllowMedia) return Result.Failure<Guid>(ChatErrors.MediaDisabled);
|
||||
|
||||
|
||||
|
||||
var parsedStoryMediaType = Enum.TryParse<MediaType>(request.StoryMediaType, true, out var sTypeEnum) ? sTypeEnum : MediaType.Image;
|
||||
message = new StoryMessage(
|
||||
Guid.NewGuid(),
|
||||
request.ChatId,
|
||||
request.SenderId,
|
||||
request.StoryId ?? Guid.Empty,
|
||||
request.StoryMediaUrl ?? string.Empty,
|
||||
parsedStoryMediaType,
|
||||
request.Content,
|
||||
request.ReplyToId,
|
||||
request.ForwardedFromId,
|
||||
DateTime.UtcNow,
|
||||
Guid.NewGuid(),
|
||||
|
||||
request.ChatId,
|
||||
|
||||
request.SenderId,
|
||||
|
||||
request.StoryId ?? Guid.Empty,
|
||||
|
||||
request.StoryMediaUrl ?? string.Empty,
|
||||
request.StoryMediaType,
|
||||
|
||||
request.Content,
|
||||
|
||||
request.ReplyToId,
|
||||
|
||||
request.ForwardedFromId,
|
||||
|
||||
DateTime.UtcNow,
|
||||
|
||||
false);
|
||||
}
|
||||
else if (request.Attachments != null && request.Attachments.Any())
|
||||
@@ -92,22 +102,31 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
|
||||
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,
|
||||
Guid.NewGuid(),
|
||||
|
||||
request.ChatId,
|
||||
|
||||
request.SenderId,
|
||||
parsedType,
|
||||
|
||||
request.Content,
|
||||
|
||||
request.ReplyToId,
|
||||
|
||||
request.ForwardedFromId,
|
||||
|
||||
DateTime.UtcNow,
|
||||
|
||||
false);
|
||||
|
||||
|
||||
|
||||
foreach (var att in request.Attachments)
|
||||
{
|
||||
var pType = Enum.TryParse<MediaType>(att.Type, true, out var tEnum) ? tEnum : MediaType.File;
|
||||
((MediaMessage)message).AddMedia(pType, att.Url, att.FileName, att.FileSize);
|
||||
((MediaMessage)message).AddMedia(pType.ToString().ToLower(), att.Url, att.FileName, att.FileSize);
|
||||
}
|
||||
}
|
||||
else if (request.Type == "poll")
|
||||
@@ -131,18 +150,26 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
else
|
||||
{
|
||||
message = new TextMessage(
|
||||
Guid.NewGuid(),
|
||||
request.ChatId,
|
||||
request.SenderId,
|
||||
request.Content ?? string.Empty,
|
||||
request.ReplyToId,
|
||||
request.Quote,
|
||||
request.ForwardedFromId,
|
||||
DateTime.UtcNow,
|
||||
Guid.NewGuid(),
|
||||
|
||||
request.ChatId,
|
||||
|
||||
request.SenderId,
|
||||
|
||||
request.Content ?? string.Empty,
|
||||
|
||||
request.ReplyToId,
|
||||
|
||||
request.Quote,
|
||||
|
||||
request.ForwardedFromId,
|
||||
|
||||
DateTime.UtcNow,
|
||||
|
||||
false);
|
||||
}
|
||||
|
||||
// 4. Ïîñëåäîâàòåëüíîñòü ñîîáùåíèé High-Water Mark
|
||||
// 4. ������������������ ��������� High-Water Mark
|
||||
chat.IncrementSequenceId();
|
||||
message.SetSequenceId(chat.LastMessageSequenceId);
|
||||
|
||||
@@ -150,15 +177,19 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
||||
senderMember.UpdateReadCursor(message.Id, message.SequenceId);
|
||||
senderMember.UpdateDeliveredCursor(message.Id);
|
||||
|
||||
// 5. Ñîõðàíÿåì
|
||||
// 5. ���������
|
||||
_messageRepository.Add(message);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _mediator.Publish(new MessageSentDomainEvent(
|
||||
message.Id,
|
||||
message.ChatId,
|
||||
message.SenderId,
|
||||
message.Content),
|
||||
message.Id,
|
||||
|
||||
message.ChatId,
|
||||
|
||||
message.SenderId,
|
||||
|
||||
message.Content),
|
||||
|
||||
cancellationToken);
|
||||
|
||||
return Result.Success(message.Id);
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using System.Text.RegularExpressions;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using MediatR;
|
||||
using System.Text.RegularExpressions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||
|
||||
@@ -17,7 +17,7 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserComma
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserChatSettingsRepository _userChatSettingsRepository;
|
||||
private readonly IUserFolderSettingsRepository _userFolderSettingsRepository;
|
||||
private readonly Knot.Modules.Messaging.Domain.IMessageRepository _messageRepository;
|
||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IMessageRepository _messageRepository;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IAuthUnitOfWork _authUnitOfWork;
|
||||
@@ -26,7 +26,7 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserComma
|
||||
IUserRepository userRepository,
|
||||
IUserChatSettingsRepository userChatSettingsRepository,
|
||||
IUserFolderSettingsRepository userFolderSettingsRepository,
|
||||
Knot.Modules.Messaging.Domain.IMessageRepository messageRepository,
|
||||
Knot.Contracts.Messaging.Application.Abstractions.IMessageRepository messageRepository,
|
||||
IFileStorageService fileStorage,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IAuthUnitOfWork authUnitOfWork)
|
||||
@@ -55,9 +55,9 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler<DeleteUserComma
|
||||
{
|
||||
foreach (var media in mediaMsg.Media)
|
||||
{
|
||||
bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id &&
|
||||
bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id &&
|
||||
m is MediaMessage mm && mm.Media.Any(ame => ame.Url == media.Url));
|
||||
|
||||
|
||||
if (!isUsedElsewhere)
|
||||
{
|
||||
var fileId = ExtractFileId(media.Url);
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
|
||||
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Knot.Modules.Conversations;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// ╨а╨╡╨│╨╕╤Б╤В╤А╨░╤Ж╨╕╤П ╤Б╨╡╤А╨▓╨╕╤Б╨╛╨▓ ╨╝╨╛╨┤╤Г╨╗╤П Chats.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddConversationsModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// в•ЁР╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨▒╨░╨╖╤Л ╨┤╨░╨╜╨╜╤Л╤Е
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
services.AddDbContext<ChatsDbContext>(options =>
|
||||
@@ -29,7 +24,7 @@ public static class DependencyInjection
|
||||
|
||||
// MongoDB Setup for Messages
|
||||
ConversationsMongoDbMapConfigurator.Configure();
|
||||
|
||||
|
||||
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
|
||||
|
||||
// Registration
|
||||
@@ -38,17 +33,17 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFolderRepository, FolderRepository>();
|
||||
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
|
||||
services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>();
|
||||
|
||||
// Messaging Repository registration (might be redundant if already in Messaging module, but needed for specific commands in Conversations)
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
|
||||
// Messaging Repository - registered in Messaging module
|
||||
// services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
|
||||
// MediatR
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
services.AddScoped<Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>();
|
||||
services.AddScoped<Knot.Modules.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
|
||||
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>();
|
||||
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
|
||||
services.AddScoped<IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Services;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
public class ChatAccessProvider : IChatAccessProvider { private readonly ChatsDbContext _db; public ChatAccessProvider(ChatsDbContext db) { _db = db; } public Task<List<Guid>> GetValidChatIdsForUserAsync(Guid userId, CancellationToken ct) { return _db.Chats.Where(c => c.Members.Any(m => m.UserId == userId)).Select(c => c.Id).ToListAsync(ct); } }
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
public class MessageNotifier : IMessageNotifier { private readonly IHubContext<ChatHub> _hubContext; public MessageNotifier(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -10,8 +9,9 @@
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Auth\Knot.Contracts.Auth.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+44
-28
@@ -5,15 +5,15 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Constants;
|
||||
using MediatR;
|
||||
|
||||
namespace Host.Application.Federation.Commands;
|
||||
|
||||
@@ -29,7 +29,8 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public InboundFederationCommandHandler(
|
||||
ISettingsService settingsService,
|
||||
ISettingsService settingsService,
|
||||
|
||||
IMessageRepository messageRepository,
|
||||
IUserRepository userRepository,
|
||||
IMessageReactionRepository reactionRepository,
|
||||
@@ -52,7 +53,7 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
|
||||
var packet = request.Packet;
|
||||
|
||||
// 1. Ïðîâåðÿåì ïîäïèñü îòïðàâèòåëÿ
|
||||
// 1. ��������� ������� �����������
|
||||
var senderConfig = settings.Federation.AllowedDomains.FirstOrDefault(d => d.IsEnabled && d.Domain.Equals(packet.SenderDomain, StringComparison.OrdinalIgnoreCase));
|
||||
if (senderConfig == null || string.IsNullOrEmpty(senderConfig.PublicKey))
|
||||
return Result.Failure(new Error("Federation.SenderNotAllowed", "Sender domain not in allowlist."));
|
||||
@@ -67,22 +68,23 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ðàñøèôðîâûâàåì IV ñâîèì Private Key
|
||||
// 2. �������������� IV ����� Private Key
|
||||
byte[] aesKey;
|
||||
byte[] aesIv;
|
||||
using (var rsaDecrypt = RSA.Create())
|
||||
{
|
||||
rsaDecrypt.ImportPkcs8PrivateKey(Convert.FromBase64String(settings.Federation.PrivateKey!), out _);
|
||||
var decryptedKeys = rsaDecrypt.Decrypt(Convert.FromBase64String(packet.EncryptedIV), RSAEncryptionPadding.Pkcs1);
|
||||
|
||||
|
||||
// AES-256 Key (32 bytes) + IV (16 bytes)
|
||||
|
||||
aesKey = new byte[32];
|
||||
aesIv = new byte[16];
|
||||
Buffer.BlockCopy(decryptedKeys, 0, aesKey, 0, 32);
|
||||
Buffer.BlockCopy(decryptedKeys, 32, aesIv, 0, 16);
|
||||
}
|
||||
|
||||
// 3. Ðàñøèôðîâûâàåì ñàìî ñîîáùåíèå (AES-256)
|
||||
// 3. �������������� ���� ��������� (AES-256)
|
||||
string plainText;
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
@@ -96,7 +98,7 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ëîãèêà îáðàáîòêè òèïîâ ñîîáùåíèé
|
||||
// 4. ������ ��������� ����� ���������
|
||||
if (packet.Metadata.MessageType == "sync_capabilities")
|
||||
{
|
||||
var capabilities = JsonSerializer.Deserialize<RemoteCapabilities>(plainText);
|
||||
@@ -112,14 +114,16 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
{
|
||||
var statusData = JsonSerializer.Deserialize<JsonElement>(plainText);
|
||||
var isOnline = statusData.GetProperty("IsOnline").GetBoolean();
|
||||
|
||||
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(packet.Metadata.SenderId, cancellationToken);
|
||||
if (user != null && user.IsExternal)
|
||||
{
|
||||
user.IsOnline = isOnline;
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
|
||||
// Óâåäîìëÿåì ëîêàëüíûõ ïîëüçîâàòåëåé ÷åðåç SignalR
|
||||
|
||||
// ���������� ��������� ������������� ����� SignalR
|
||||
|
||||
await _notifier.NotifyNewMessageAsync(Guid.Empty, new { type = "presence_update", userId = user.Id, isOnline = user.IsOnline }, cancellationToken);
|
||||
}
|
||||
return Result.Success();
|
||||
@@ -163,12 +167,19 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
{
|
||||
await _reactionRepository.RemoveAsync(messageForReaction.Id, packet.Metadata.UserId, plainText, cancellationToken);
|
||||
}
|
||||
|
||||
await _notifier.NotifyNewMessageAsync(messageForReaction.ChatId, new {
|
||||
type = packet.Metadata.MessageType,
|
||||
messageId = messageForReaction.Id,
|
||||
userId = packet.Metadata.UserId,
|
||||
emoji = plainText
|
||||
|
||||
|
||||
await _notifier.NotifyNewMessageAsync(messageForReaction.ChatId, new
|
||||
{
|
||||
|
||||
type = packet.Metadata.MessageType,
|
||||
|
||||
messageId = messageForReaction.Id,
|
||||
|
||||
userId = packet.Metadata.UserId,
|
||||
|
||||
emoji = plainText
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
return Result.Success();
|
||||
@@ -183,21 +194,26 @@ internal sealed class InboundFederationCommandHandler : ICommandHandler<InboundF
|
||||
if (packet.Metadata.MessageType == "poll" && !settings.Messages.AllowPolls)
|
||||
return Result.Failure(new Error("Federation.PollsDisabled", "We do not accept polls."));
|
||||
|
||||
// 5. Ñîõðàíåíèå â áàçó ñîîáùåíèé (MongoDB)
|
||||
// 5. ���������� � ���� ��������� (MongoDB)
|
||||
var message = new TextMessage(
|
||||
Guid.NewGuid(),
|
||||
packet.Metadata.ChatId,
|
||||
packet.Metadata.SenderId,
|
||||
plainText,
|
||||
Guid.NewGuid(),
|
||||
|
||||
packet.Metadata.ChatId,
|
||||
|
||||
packet.Metadata.SenderId,
|
||||
plainText,
|
||||
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
packet.Metadata.CreatedAt,
|
||||
packet.Metadata.CreatedAt,
|
||||
|
||||
false);
|
||||
|
||||
|
||||
|
||||
_messageRepository.Add(message);
|
||||
|
||||
// 6. Óâåäîìëåíèå ïîëüçîâàòåëÿ ÷åðåç SignalR
|
||||
// 6. ����������� ������������ ����� SignalR
|
||||
await _notifier.NotifyNewMessageAsync(packet.Metadata.ChatId, message, cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
|
||||
+12
-21
@@ -2,21 +2,17 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик доменного события отправки сообщения.
|
||||
/// Если в чате есть внешние участники — инициирует федеративную рассылку.
|
||||
/// </summary>
|
||||
public sealed class MessageSentDomainEventHandler : INotificationHandler<MessageSentDomainEvent>
|
||||
{
|
||||
private readonly IChatRepository _chatRepository;
|
||||
@@ -26,7 +22,7 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
private readonly IFederationGateway _gateway;
|
||||
|
||||
public MessageSentDomainEventHandler(
|
||||
IChatRepository chatRepository,
|
||||
IChatRepository chatRepository,
|
||||
IUserRepository userRepository,
|
||||
ISettingsService settingsService,
|
||||
FederationPacketService packetService,
|
||||
@@ -44,14 +40,12 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
|
||||
if (!settings.Federation.Enabled) return;
|
||||
|
||||
// 1. Загружаем чат и проверяем наличие внешних участников
|
||||
var chat = await _chatRepository.GetByIdAsync(notification.ChatId, cancellationToken);
|
||||
if (chat == null) return;
|
||||
|
||||
var memberIds = chat.Members.Select(m => m.UserId).ToList();
|
||||
var members = await _userRepository.GetByIdsAsync(memberIds, cancellationToken);
|
||||
|
||||
// Находим уникальные домены внешних участников
|
||||
|
||||
var externalDomains = members
|
||||
.Where(u => u.IsExternal && !string.IsNullOrEmpty(u.Domain))
|
||||
.Select(u => u.Domain!)
|
||||
@@ -60,7 +54,6 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
|
||||
if (!externalDomains.Any()) return;
|
||||
|
||||
// 2. Получаем имя отправителя для метаданных
|
||||
var sender = await _userRepository.GetByIdAsync(notification.SenderId, cancellationToken);
|
||||
var senderUsername = sender?.Username ?? "unknown";
|
||||
|
||||
@@ -68,22 +61,20 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
notification.ChatId,
|
||||
notification.SenderId,
|
||||
senderUsername,
|
||||
"text", // Для начала поддерживаем только текст через ивент
|
||||
"text",
|
||||
DateTime.UtcNow
|
||||
);
|
||||
|
||||
// 3. Рассылка по доменам (Fan-out)
|
||||
foreach (var domain in externalDomains)
|
||||
{
|
||||
var packetResult = await _packetService.PreparePacketAsync(
|
||||
notification.Content ?? string.Empty,
|
||||
metadata,
|
||||
domain,
|
||||
notification.Content ?? string.Empty,
|
||||
metadata,
|
||||
domain,
|
||||
cancellationToken);
|
||||
|
||||
if (packetResult.IsSuccess)
|
||||
{
|
||||
// Отправляем асинхронно
|
||||
await _gateway.SendPacketAsync(packetResult.Value, domain, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Modules.Settings.Domain.Events;
|
||||
using Knot.Contracts.Settings.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
|
||||
+7
-8
@@ -4,8 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Modules.Auth.Domain;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Modules.Federation.Application.Abstractions;
|
||||
using Knot.Modules.Federation.Application.Federation.Services;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
@@ -14,8 +13,8 @@ using Knot.Contracts.Settings.Application.DTOs;
|
||||
namespace Knot.Modules.Federation.Application.Federation.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Îáðàáîò÷èê èçìåíåíèÿ ñòàòóñà ïîëüçîâàòåëÿ.
|
||||
/// Ðàññûëàåò íîâûé ñòàòóñ âñåì ñåðâåðàì, ãäå ó ïîëüçîâàòåëÿ åñòü ÷àòû.
|
||||
/// Обработчик изменения статуса пользователя.
|
||||
/// Отправляет статус всем внешним контактам, а также подписчикам чатов.
|
||||
/// </summary>
|
||||
public sealed class UserStatusChangedDomainEventHandler : INotificationHandler<UserStatusChangedDomainEvent>
|
||||
{
|
||||
@@ -44,11 +43,11 @@ public sealed class UserStatusChangedDomainEventHandler : INotificationHandler<U
|
||||
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
|
||||
if (!settings.Federation.Enabled) return;
|
||||
|
||||
// 1. Íàõîäèì âñå ÷àòû ïîëüçîâàòåëÿ
|
||||
// 1. Получаем все чаты пользователя
|
||||
var userChats = await _chatRepository.GetUserChatsAsync(notification.UserId, cancellationToken);
|
||||
if (!userChats.Any()) return;
|
||||
|
||||
// 2. Îïðåäåëÿåì óíèêàëüíûå âíåøíèå äîìåíû ó÷àñòíèêîâ ýòèõ ÷àòîâ
|
||||
// 2. Получаем уникальные ID участников всех чатов пользователя
|
||||
var allMemberIds = userChats.SelectMany(c => c.Members.Select(m => m.UserId)).Distinct().ToList();
|
||||
var members = await _userRepository.GetByIdsAsync(allMemberIds, cancellationToken);
|
||||
|
||||
@@ -60,7 +59,7 @@ public sealed class UserStatusChangedDomainEventHandler : INotificationHandler<U
|
||||
|
||||
if (!externalDomains.Any()) return;
|
||||
|
||||
// 3. Ôîðìèðóåì ïàêåò ñòàòóñà
|
||||
// 3. Формируем данные статуса
|
||||
var statusData = new {
|
||||
notification.IsOnline,
|
||||
notification.LastSeen
|
||||
@@ -75,7 +74,7 @@ public sealed class UserStatusChangedDomainEventHandler : INotificationHandler<U
|
||||
DateTime.UtcNow
|
||||
);
|
||||
|
||||
// 4. Ðàññûëêà ïî äîìåíàì
|
||||
// 4. Отправляем по доменам
|
||||
foreach (var domain in externalDomains)
|
||||
{
|
||||
var packetResult = await _packetService.PreparePacketAsync(payload, metadata, domain, cancellationToken);
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Temporarily disabled due to architecture violations - uses Knot.Modules.* instead of Contracts -->
|
||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Auth\Knot.Contracts.Auth.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MongoDB.Driver;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
|
||||
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Messaging;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// ╨а╨╡╨│╨╕╤Б╤В╤А╨░╤Ж╨╕╤П ╤Б╨╡╤А╨▓╨╕╤Б╨╛╨▓ ╨╝╨╛╨┤╤Г╨╗╤П Chats.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMessagingModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// ╨Э╨░╤Б╤В╤А╨╛╨╣╨║╨░ ╨▒╨░╨╖╤Л ╨┤╨░╨╜╨╜╤Л╤Е
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
|
||||
|
||||
// MongoDB Setup for Messages
|
||||
MongoDbMapConfigurator.Configure();
|
||||
|
||||
|
||||
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
|
||||
services.AddSingleton<IMongoClient>(new MongoClient(mongoConnectionString));
|
||||
services.AddScoped<IMongoDatabase>(sp =>
|
||||
services.AddScoped<IMongoDatabase>(sp =>
|
||||
sp.GetRequiredService<IMongoClient>().GetDatabase("forkmessager_chats"));
|
||||
|
||||
// Registration
|
||||
|
||||
+28
-37
@@ -1,15 +1,10 @@
|
||||
using MediatR;
|
||||
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик доменного события отправки сообщения.
|
||||
/// Отправляет уведомление через SignalR всем участникам чата.
|
||||
/// </summary>
|
||||
public sealed class MessageSentDomainEventHandler : INotificationHandler<MessageSentDomainEvent>
|
||||
{
|
||||
private readonly IMessageNotifier _hubContext;
|
||||
@@ -39,7 +34,6 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
? new { Id = userInfo.Id, Username = userInfo.Username, DisplayName = userInfo.DisplayName, Avatar = userInfo.Avatar }
|
||||
: new { Id = message.SenderId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null };
|
||||
|
||||
// Fetch forwarded from info if exists
|
||||
object? forwardedFromObj = null;
|
||||
if (message.ForwardedFromId.HasValue)
|
||||
{
|
||||
@@ -49,7 +43,6 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
: new { Id = message.ForwardedFromId.Value, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null };
|
||||
}
|
||||
|
||||
// Fetch reply info if exists
|
||||
object? replyToObj = null;
|
||||
if (message.ReplyToId.HasValue)
|
||||
{
|
||||
@@ -62,7 +55,7 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
Id = replyMsg.Id,
|
||||
Content = replyMsg.Content,
|
||||
Quote = message.Quote,
|
||||
media = replyMsg.Media.Select(rm => new { rm.Id, rm.Type, rm.Url }).ToList(),
|
||||
media = replyMsg.Media.Select(rm => new { rm.Type, rm.Url }).ToList(),
|
||||
Sender = replySenderInfo != null
|
||||
? new { Id = replySenderInfo.Id, Username = replySenderInfo.Username, DisplayName = replySenderInfo.DisplayName }
|
||||
: new { Id = replyMsg.SenderId, Username = "unknown", DisplayName = "Unknown" }
|
||||
@@ -70,34 +63,32 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем сообщение в "комнату" чата.
|
||||
await _hubContext.NotifyNewMessageAsync(notification.ChatId, new
|
||||
{
|
||||
id = message.Id,
|
||||
chatId = message.ChatId,
|
||||
senderId = message.SenderId,
|
||||
content = message.Content,
|
||||
type = message.Type,
|
||||
createdAt = message.CreatedAt,
|
||||
forwardedFromId = message.ForwardedFromId,
|
||||
forwardedFrom = forwardedFromObj,
|
||||
replyToId = message.ReplyToId,
|
||||
replyTo = replyToObj,
|
||||
quote = message.Quote,
|
||||
media = message.Media.Select(m => new
|
||||
{
|
||||
id = message.Id,
|
||||
chatId = message.ChatId,
|
||||
senderId = message.SenderId,
|
||||
content = message.Content,
|
||||
type = message.Type,
|
||||
createdAt = message.CreatedAt,
|
||||
forwardedFromId = message.ForwardedFromId,
|
||||
forwardedFrom = forwardedFromObj,
|
||||
replyToId = message.ReplyToId,
|
||||
replyTo = replyToObj,
|
||||
quote = message.Quote,
|
||||
media = message.Media.Select(m => new
|
||||
{
|
||||
id = m.Id,
|
||||
type = m.Type,
|
||||
url = m.Url,
|
||||
filename = m.Filename,
|
||||
size = m.Size
|
||||
}).ToList(),
|
||||
sender = senderObj,
|
||||
readBy = new List<object>(),
|
||||
storyId = message.StoryId,
|
||||
storyMediaUrl = message.StoryMediaUrl,
|
||||
storyMediaType = message.StoryMediaType
|
||||
}, cancellationToken);
|
||||
type = m.Type,
|
||||
url = m.Url,
|
||||
filename = m.FileId,
|
||||
size = m.Size
|
||||
}).ToList(),
|
||||
sender = senderObj,
|
||||
readBy = new List<object>(),
|
||||
storyId = message.StoryId,
|
||||
storyMediaUrl = message.StoryMediaUrl,
|
||||
storyMediaType = message.StoryMediaType
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-7
@@ -1,5 +1,6 @@
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using MongoDB.Driver;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
|
||||
@@ -14,7 +15,7 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
_reactions = mongoDatabase.GetCollection<MessageReaction>("message_reactions");
|
||||
_mediator = mediator;
|
||||
_messageRepository = messageRepository;
|
||||
|
||||
|
||||
// Ensure index for fast querying by message
|
||||
var indexKeysDefinition = Builders<MessageReaction>.IndexKeys.Ascending(r => r.MessageId);
|
||||
_reactions.Indexes.CreateOne(new CreateIndexModel<MessageReaction>(indexKeysDefinition));
|
||||
@@ -22,17 +23,14 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
|
||||
public async Task AddAsync(MessageReaction reaction, CancellationToken cancellationToken)
|
||||
{
|
||||
// Уникальный индекс или фильтр, чтобы не дублировать
|
||||
var filter = Builders<MessageReaction>.Filter.And(
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.MessageId, reaction.MessageId),
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.UserId, reaction.UserId),
|
||||
Builders<MessageReaction>.Filter.Eq(r => r.Emoji, reaction.Emoji)
|
||||
);
|
||||
|
||||
// Используем ReplaceOptions.IsUpsert = true для идемпотентности (нет гонок)
|
||||
|
||||
await _reactions.ReplaceOneAsync(filter, reaction, new ReplaceOptions { IsUpsert = true }, cancellationToken);
|
||||
|
||||
// Уведомляем систему (для Федерации)
|
||||
var message = await _messageRepository.GetByIdAsync(reaction.MessageId, cancellationToken);
|
||||
if (message != null)
|
||||
{
|
||||
@@ -50,7 +48,6 @@ public sealed class MessageReactionRepository : IMessageReactionRepository
|
||||
|
||||
await _reactions.DeleteOneAsync(filter, cancellationToken);
|
||||
|
||||
// Уведомляем систему (для Федерации)
|
||||
var message = await _messageRepository.GetByIdAsync(messageId, cancellationToken);
|
||||
if (message != null)
|
||||
{
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence;
|
||||
|
||||
public sealed class MessageRepository : IMessageRepository
|
||||
{
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider _chatAccessProvider;
|
||||
private readonly IChatAccessProvider _chatAccessProvider;
|
||||
private readonly MediatR.IMediator _mediator;
|
||||
|
||||
public MessageRepository(IMongoDatabase mongoDatabase, Knot.Modules.Messaging.Application.Abstractions.IChatAccessProvider chatAccessProvider, MediatR.IMediator mediator)
|
||||
public MessageRepository(IMongoDatabase mongoDatabase, IChatAccessProvider chatAccessProvider, MediatR.IMediator mediator)
|
||||
{
|
||||
_messages = mongoDatabase.GetCollection<Message>("messages");
|
||||
_chatAccessProvider = chatAccessProvider;
|
||||
@@ -28,7 +29,7 @@ public sealed class MessageRepository : IMessageRepository
|
||||
// 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)
|
||||
@@ -65,7 +66,7 @@ public sealed class MessageRepository : IMessageRepository
|
||||
{
|
||||
var builder = Builders<Message>.Filter;
|
||||
var filter = builder.Eq(m => m.ChatId, chatId);
|
||||
|
||||
|
||||
if (cursor.HasValue)
|
||||
{
|
||||
filter &= builder.Lt(m => m.CreatedAt, cursor.Value);
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using MongoDB.Bson.Serialization.Serializers;
|
||||
using Knot.Shared.Kernel;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class MongoDbMapConfigurator
|
||||
if (_initialized) return;
|
||||
|
||||
try { BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); } catch { /* Already registered */ }
|
||||
|
||||
|
||||
BsonSerializer.RegisterSerializer(new EnumSerializer<MessageState>(BsonType.Int32));
|
||||
BsonSerializer.RegisterSerializer(new EnumSerializer<MediaType>(BsonType.String));
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class MongoDbMapConfigurator
|
||||
cm.AutoMap();
|
||||
cm.MapField("_media").SetElementName("Media");
|
||||
cm.SetDiscriminator("MediaMessage");
|
||||
cm.UnmapProperty(c => c.Caption); // Avoid DB duplication, Content is already saved
|
||||
cm.UnmapProperty(c => c.Caption);
|
||||
});
|
||||
|
||||
BsonClassMap.RegisterClassMap<StoryMessage>(cm =>
|
||||
@@ -69,7 +69,7 @@ public static class MongoDbMapConfigurator
|
||||
BsonClassMap.RegisterClassMap<PollOption>(cm => cm.AutoMap());
|
||||
BsonClassMap.RegisterClassMap<PollVote>(cm => cm.AutoMap());
|
||||
|
||||
|
||||
|
||||
BsonClassMap.RegisterClassMap<Media>(cm =>
|
||||
{
|
||||
cm.AutoMap();
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
|
||||
@@ -24,7 +24,6 @@ public sealed class UserStatsService : IUserStatsService
|
||||
var userGuidList = userIds.ToList();
|
||||
if (!userGuidList.Any()) return new Dictionary<Guid, UserStats>();
|
||||
|
||||
// Эффективная агрегация: считаем количество и сумму Media.Size
|
||||
var stats = await _messages.Aggregate()
|
||||
.Match(Builders<Message>.Filter.In(m => m.SenderId, userGuidList))
|
||||
.Group(new BsonDocument {
|
||||
@@ -67,9 +66,6 @@ public sealed class UserStatsService : IUserStatsService
|
||||
|
||||
public async Task<long> GetOrphanedMediaSizeAsync(HashSet<string> validFileIds, CancellationToken ct = default)
|
||||
{
|
||||
// Для больших объемов правильнее собирать список ВСЕХ URL файлов из сообщений,
|
||||
// но здесь мы оптимизируем через проекцию, чтобы вернуть только нужные поля.
|
||||
// Этот метод может быть реализован в BackgroundTask для очень больших баз.
|
||||
return 0; // Временная заглушка, реальный подсчет через курсор в DryRun
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
Reference in New Issue
Block a user