Зависимости на контракты
This commit is contained in:
22
backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs
Normal file
22
backend/src/Contracts/Admin/Abstractions/AggregateRoot.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый класс для агрегатов - сущностей с бизнес-логикой и доменными событиями.
|
||||
/// </summary>
|
||||
public abstract class AggregateRoot<TId> : Entity<TId> where TId : notnull
|
||||
{
|
||||
private readonly List<IDomainEvent> _domainEvents = new();
|
||||
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
protected AggregateRoot(TId id) : base(id) { }
|
||||
|
||||
protected void RaiseDomainEvent(IDomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
43
backend/src/Contracts/Admin/Abstractions/Entity.cs
Normal file
43
backend/src/Contracts/Admin/Abstractions/Entity.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый класс для сущностей с уникальным идентификатором.
|
||||
/// </summary>
|
||||
public abstract class Entity<TId> where TId : notnull
|
||||
{
|
||||
public TId Id { get; protected set; }
|
||||
|
||||
protected Entity(TId id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not Entity<TId> other)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id.Equals(other.Id);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static bool operator ==(Entity<TId>? a, Entity<TId>? b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Entity<TId>? a, Entity<TId>? b) => !(a == b);
|
||||
}
|
||||
15
backend/src/Contracts/Admin/Abstractions/ICommand.cs
Normal file
15
backend/src/Contracts/Admin/Abstractions/ICommand.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
public interface ICommand : IRequest<Result> { }
|
||||
|
||||
public interface ICommand<TResponse> : IRequest<Result<TResponse>> { }
|
||||
|
||||
public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, Result>
|
||||
where TCommand : ICommand
|
||||
{ }
|
||||
|
||||
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
|
||||
where TCommand : ICommand<TResponse>
|
||||
{ }
|
||||
8
backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs
Normal file
8
backend/src/Contracts/Admin/Abstractions/IDomainEvent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для доменных событий.
|
||||
/// </summary>
|
||||
public interface IDomainEvent : INotification { }
|
||||
13
backend/src/Contracts/Admin/Abstractions/IQuery.cs
Normal file
13
backend/src/Contracts/Admin/Abstractions/IQuery.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
public interface IQuery<TResponse> : IRequest<Result<TResponse>>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IQueryHandler<TQuery, TResponse>
|
||||
: IRequestHandler<TQuery, Result<TResponse>>
|
||||
where TQuery : IQuery<TResponse>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
public record MessageResponse(string Message);
|
||||
63
backend/src/Contracts/Admin/Abstractions/Result.cs
Normal file
63
backend/src/Contracts/Admin/Abstractions/Result.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет ошибку в доменной логике.
|
||||
/// </summary>
|
||||
public sealed record Error(string Code, string Description)
|
||||
{
|
||||
public static readonly Error None = new(string.Empty, string.Empty);
|
||||
public static Error NotFound(string code, string description) => new(code, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Общая обертка для результата операции. Позволяет избегать использования исключений для управления потоком.
|
||||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public bool IsFailure => !IsSuccess;
|
||||
public Error Error { get; }
|
||||
|
||||
protected Result(bool isSuccess, Error error)
|
||||
{
|
||||
if (isSuccess && error != Error.None)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (!isSuccess && error == Error.None)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
IsSuccess = isSuccess;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public static Result Success() => new(true, Error.None);
|
||||
public static Result Failure(Error error) => new(false, error);
|
||||
|
||||
public static Result<TValue> Success<TValue>(TValue value) => Result<TValue>.Success(value);
|
||||
public static Result<TValue> Failure<TValue>(Error error) => Result<TValue>.Failure(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Результ операции, содержащий значение.
|
||||
/// </summary>
|
||||
public class Result<TValue> : Result
|
||||
{
|
||||
private readonly TValue? _value;
|
||||
|
||||
protected internal Result(TValue? value, bool isSuccess, Error error)
|
||||
: base(isSuccess, error)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public TValue Value => IsSuccess
|
||||
? _value!
|
||||
: throw new InvalidOperationException("Нельзя получить значение ошибочного результата.");
|
||||
|
||||
public static Result<TValue> Success(TValue value) => new(value, true, Error.None);
|
||||
public new static Result<TValue> Failure(Error error) => new(default, false, error);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
public record SuccessResponse(bool Success);
|
||||
@@ -6,10 +6,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
|
||||
namespace Knot.Contracts.Auth.Infrastructure.Persistence;
|
||||
|
||||
public interface IAuthDbContext
|
||||
{
|
||||
IQueryable<UserContract> Users { get; }
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
|
||||
namespace Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||
|
||||
public interface IChatsDbContext : IChatsUnitOfWork
|
||||
public interface IChatsDbContext
|
||||
{
|
||||
IQueryable<Chat> Chats { get; }
|
||||
}
|
||||
|
||||
public class Chat : AggregateRoot<Guid>
|
||||
/// <summary>
|
||||
/// Упрощенная проекция чата для запросов (не доменная сущность).
|
||||
/// </summary>
|
||||
public class Chat
|
||||
{
|
||||
public Chat() : base(Guid.NewGuid()) { }
|
||||
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Klipy.Application.Abstractions;
|
||||
|
||||
public interface IKlipyClient
|
||||
{
|
||||
Task<bool> TestConnectionAsync(string apiKey, string appName, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public interface IMessageQueryService
|
||||
{
|
||||
Task<List<MessageInfo>> GetAllMessagesAsync(CancellationToken cancellationToken);
|
||||
Task<List<MessageInfo>> GetOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken cancellationToken);
|
||||
Task DeleteMessagesAsync(List<Guid> messageIds, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public record MessageInfo(Guid Id, Guid ChatId, bool IsDeleted, string? MediaUrl, List<MediaInfo>? Media);
|
||||
|
||||
public record MediaInfo(string Url);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Messaging.Application.Abstractions;
|
||||
|
||||
public interface IUserStatsService
|
||||
{
|
||||
Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(List<Guid> userIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record UserStats(int MessageCount, long StorageSize);
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Settings.Abstractions;
|
||||
|
||||
public interface IStatisticsService
|
||||
{
|
||||
Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class DashboardStatsDto
|
||||
{
|
||||
public long StorageUsedBytes { get; set; }
|
||||
public long StorageLimitBytes { get; set; }
|
||||
public long OnlineUsers { get; set; }
|
||||
public long OfflineUsers { get; set; }
|
||||
public long TotalUsers { get; set; }
|
||||
public List<ActivityStatDto> ActivityTimeline { get; set; } = new();
|
||||
public List<TopUserDto> TopUsersByMessages { get; set; } = new();
|
||||
public List<TopUserDto> TopUsersByStorage { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ActivityStatDto
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public long Messages { get; set; }
|
||||
public long FilesSize { get; set; }
|
||||
}
|
||||
|
||||
public class TopUserDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public long Value { get; set; } // messages count or bytes
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Storage.Abstractions;
|
||||
|
||||
public interface IFileStorageService
|
||||
{
|
||||
// Загружает поток файла и возвращает его уникальный идентификатор (SHA256 хеш или GUID).
|
||||
Task<string> UploadFileAsync(Stream stream, string fileName, string contentType);
|
||||
|
||||
// Скачивает файл и возвращает его расшифрованный поток и тип содержимого.
|
||||
Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId);
|
||||
|
||||
// Удаляет файл из хранилища.
|
||||
Task DeleteFileAsync(string fileId);
|
||||
|
||||
// Получает список всех файлов в хранилище с их размерами.
|
||||
Task<IEnumerable<(string FileId, long Size)>> ListFilesAsync();
|
||||
|
||||
// Получает размер конкретного файла
|
||||
Task<long> GetFileSizeAsync(string fileId, CancellationToken ct = default);
|
||||
|
||||
// Получает размеры списка файлов
|
||||
Task<Dictionary<string, long>> GetFileSizesAsync(IEnumerable<string> fileIds, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
|
||||
public interface IStoryCollection
|
||||
{
|
||||
Task<List<StoryContract>> GetAllAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class StoryContract
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string? MediaUrl { get; set; }
|
||||
public string? MediaType { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record BanUserCommand(Guid UserId) : ICommand<Result>;
|
||||
public record BanUserCommand(Guid UserId) : ICommand<SuccessResponse>;
|
||||
|
||||
internal sealed class BanUserCommandHandler : ICommandHandler<BanUserCommand, Result>
|
||||
internal sealed class BanUserCommandHandler : ICommandHandler<BanUserCommand, SuccessResponse>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _unitOfWork;
|
||||
@@ -21,16 +21,16 @@ internal sealed class BanUserCommandHandler : ICommandHandler<BanUserCommand, Re
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Result>> Handle(BanUserCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<SuccessResponse>> Handle(BanUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null) return Result.Failure<Result>(Error.NotFound("User.NotFound", "User not found"));
|
||||
if (user == null) return Result.Failure<SuccessResponse>(Error.NotFound("User.NotFound", "User not found"));
|
||||
|
||||
user.IsBanned = true;
|
||||
user.BannedUntil = null; // Permanent ban
|
||||
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(Result.Success());
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Shared.Kernel;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
public record UnbanUserCommand(Guid UserId) : ICommand<Result>;
|
||||
public record UnbanUserCommand(Guid UserId) : ICommand<SuccessResponse>;
|
||||
|
||||
internal sealed class UnbanUserCommandHandler : ICommandHandler<UnbanUserCommand, Result>
|
||||
internal sealed class UnbanUserCommandHandler : ICommandHandler<UnbanUserCommand, SuccessResponse>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IAuthUnitOfWork _unitOfWork;
|
||||
@@ -21,16 +21,16 @@ internal sealed class UnbanUserCommandHandler : ICommandHandler<UnbanUserCommand
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<Result>> Handle(UnbanUserCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Result<SuccessResponse>> Handle(UnbanUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null) return Result.Failure<Result>(Error.NotFound("User.NotFound", "User not found"));
|
||||
if (user == null) return Result.Failure<SuccessResponse>(Error.NotFound("User.NotFound", "User not found"));
|
||||
|
||||
user.IsBanned = false;
|
||||
user.BannedUntil = null;
|
||||
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success(Result.Success());
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Infrastructure.Persistence;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Storage.Abstractions;
|
||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
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;
|
||||
|
||||
@@ -21,17 +20,22 @@ public record CleanRunCommand() : ICommand<MessageResponse>;
|
||||
|
||||
internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand, MessageResponse>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly IMongoCollection<Story> _stories;
|
||||
private readonly Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext _chatsDbContext;
|
||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService _messageQueryService;
|
||||
private readonly Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection _storyCollection;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly AuthDbContext _identityDb;
|
||||
private readonly Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext _identityDb;
|
||||
|
||||
public CleanRunCommandHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb)
|
||||
public CleanRunCommandHandler(
|
||||
Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext chatsDbContext,
|
||||
Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageQueryService,
|
||||
Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection storyCollection,
|
||||
IFileStorageService fileStorage,
|
||||
Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext identityDb)
|
||||
{
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
_stories = mongoDb.GetCollection<Story>("stories");
|
||||
_messageQueryService = messageQueryService;
|
||||
_storyCollection = storyCollection;
|
||||
_fileStorage = fileStorage;
|
||||
_identityDb = identityDb;
|
||||
}
|
||||
@@ -40,30 +44,27 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
{
|
||||
try
|
||||
{
|
||||
var allChats = await _chatsDbContext.Chats.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var allChats = await _chatsDbContext.Chats.ToListAsync(cancellationToken);
|
||||
var activeChatIds = allChats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
|
||||
|
||||
var orphanMessages = allMessages
|
||||
.Where(m => !activeChatIds.Contains(m.ChatId) || m.IsDeleted)
|
||||
.ToList();
|
||||
var allMessages = await _messageQueryService.GetAllMessagesAsync(cancellationToken);
|
||||
var orphanMessages = await _messageQueryService.GetOrphanedMessagesAsync(activeChatIds, cancellationToken);
|
||||
|
||||
var keptMessages = allMessages
|
||||
.Where(m => activeChatIds.Contains(m.ChatId) && !m.IsDeleted)
|
||||
.Where(m => !orphanMessages.Any(om => om.Id == m.Id))
|
||||
.ToList();
|
||||
|
||||
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
|
||||
|
||||
var allUsers = await _identityDb.Users.AsNoTracking().ToListAsync(cancellationToken);
|
||||
var allUsers = await _identityDb.Users.ToListAsync(cancellationToken);
|
||||
|
||||
var allStories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||
var allStories = await _storyCollection.GetAllAsync(cancellationToken);
|
||||
|
||||
var validUrls = new HashSet<string>();
|
||||
|
||||
var activeMessageUrls = keptMessages.OfType<MediaMessage>()
|
||||
var activeMessageUrls = keptMessages
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.SelectMany(m => m.Media!)
|
||||
.Select(me => me.Url)
|
||||
.Where(u => !string.IsNullOrEmpty(u));
|
||||
|
||||
@@ -100,8 +101,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
|
||||
if (orphanMessages.Any())
|
||||
{
|
||||
var orphanIds = orphanMessages.Select(m => m.Id).ToList();
|
||||
var filter = Builders<Message>.Filter.In(m => m.Id, orphanIds);
|
||||
await _messages.DeleteManyAsync(filter, cancellationToken);
|
||||
await _messageQueryService.DeleteMessagesAsync(orphanIds, cancellationToken);
|
||||
}
|
||||
|
||||
return Result.Success(new MessageResponse("Cleanup completed successfully"));
|
||||
|
||||
@@ -6,7 +6,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
@@ -28,10 +28,10 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Failure<SuccessResponse>(AuthErrors.UserNotFound);
|
||||
return Result.Failure<SuccessResponse>(Error.NotFound("Auth.UserNotFound", "User not found"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
return Result.Failure<SuccessResponse>(new Error("Admin.InvalidPassword", "Invalid password"));
|
||||
|
||||
var hash = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
|
||||
@@ -42,3 +42,4 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
|
||||
return Result.Success(new SuccessResponse(true));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Klipy.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using Knot.Modules.Klipy.Application.Abstractions;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
|
||||
@@ -13,9 +13,9 @@ public record TestKlipyConnectionCommand(string ApiKey, string AppName) : IComma
|
||||
internal sealed class TestKlipyConnectionCommandHandler : ICommandHandler<TestKlipyConnectionCommand, bool>
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IKlipyClient _klipyClient;
|
||||
private readonly Knot.Contracts.Klipy.Application.Abstractions.IKlipyClient _klipyClient;
|
||||
|
||||
public TestKlipyConnectionCommandHandler(ISettingsService settingsService, IKlipyClient klipyClient)
|
||||
public TestKlipyConnectionCommandHandler(ISettingsService settingsService, Knot.Contracts.Klipy.Application.Abstractions.IKlipyClient klipyClient)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_klipyClient = klipyClient;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using System.Security.Cryptography;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Commands;
|
||||
|
||||
@@ -26,19 +26,19 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSetti
|
||||
public async Task<Result<SystemSettingsDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Если федерация включена, но ключи еще не сгенерированы — создаем их
|
||||
if (request.Settings.Federation.Enabled &&
|
||||
(string.IsNullOrEmpty(request.Settings.Federation.PrivateKey) ||
|
||||
if (request.Settings.Federation.Enabled &&
|
||||
(string.IsNullOrEmpty(request.Settings.Federation.PrivateKey) ||
|
||||
string.IsNullOrEmpty(request.Settings.Federation.PublicKey)))
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
|
||||
|
||||
// Экспорт в формате PKCS#8 (Private) и PKCS#1 (Public) в Base64 PEM-строки
|
||||
request.Settings.Federation.PrivateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());
|
||||
request.Settings.Federation.PublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
|
||||
}
|
||||
|
||||
await _settingsService.UpdateSettingsAsync(request.Settings, cancellationToken);
|
||||
|
||||
|
||||
// Уведомляем другие модули (в т.ч. Федерацию) через Доменное Событие
|
||||
await _mediator.Publish(new Knot.Modules.Admin.Domain.Events.SystemSettingsUpdatedDomainEvent(request.Settings), cancellationToken);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
|
||||
public record CleanDryRunResult(
|
||||
int OrphanedMessagesCount,
|
||||
int OrphanedMediaCount,
|
||||
long OrphanedMediaBytes,
|
||||
int ExpiredStoriesCount,
|
||||
long ExpiredStoriesSize
|
||||
);
|
||||
@@ -1,136 +1,77 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Infrastructure.Persistence;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
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.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;
|
||||
|
||||
public record CleanDryRunQuery() : IQuery<CleanupDryRunResultDto>;
|
||||
public record CleanDryRunQuery() : IQuery<CleanDryRunResult>;
|
||||
|
||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanupDryRunResultDto>
|
||||
internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery, CleanDryRunResult>
|
||||
{
|
||||
private readonly ChatsDbContext _chatsDbContext;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly IMongoCollection<Story> _stories;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
private readonly AuthDbContext _identityDb;
|
||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService _messageService;
|
||||
private readonly IAuthDbContext _authDbContext;
|
||||
private readonly IChatsDbContext _chatsDbContext;
|
||||
private readonly IStoryCollection _storyCollection;
|
||||
|
||||
public CleanDryRunQueryHandler(ChatsDbContext chatsDbContext, IMongoDatabase mongoDb, IFileStorageService fileStorage, AuthDbContext identityDb)
|
||||
public CleanDryRunQueryHandler(
|
||||
Knot.Contracts.Messaging.Application.Abstractions.IMessageQueryService messageService,
|
||||
IAuthDbContext authDbContext,
|
||||
IChatsDbContext chatsDbContext,
|
||||
IStoryCollection storyCollection)
|
||||
{
|
||||
_messageService = messageService;
|
||||
_authDbContext = authDbContext;
|
||||
_chatsDbContext = chatsDbContext;
|
||||
_messages = mongoDb.GetCollection<Message>("messages");
|
||||
_stories = mongoDb.GetCollection<Story>("stories");
|
||||
_fileStorage = fileStorage;
|
||||
_identityDb = identityDb;
|
||||
_storyCollection = storyCollection;
|
||||
}
|
||||
|
||||
public async Task<Result<CleanupDryRunResultDto>> Handle(CleanDryRunQuery request, CancellationToken ct)
|
||||
public async Task<Result<CleanDryRunResult>> Handle(CleanDryRunQuery request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Получаем ID активных чатов (SQL)
|
||||
var activeChats = await _chatsDbContext.Chats
|
||||
.AsNoTracking()
|
||||
.Select(c => new { c.Id, c.Avatar })
|
||||
.ToListAsync(ct);
|
||||
var activeChatIds = _chatsDbContext.Chats.Select(c => c.Id).ToHashSet();
|
||||
|
||||
var activeChatIds = activeChats.Select(c => c.Id).ToHashSet();
|
||||
var orphanedMessages = await _messageService.GetOrphanedMessagesAsync(activeChatIds, ct);
|
||||
var orphanedMediaCount = orphanedMessages.Count(m => m.MediaUrl != null);
|
||||
var orphanedMessageCount = orphanedMessages.Count;
|
||||
|
||||
// 2. Считаем сообщения подлежащие удалению (MongoDB)
|
||||
var orphanedFilter = Builders<Message>.Filter.Or(
|
||||
Builders<Message>.Filter.BitsAnySet(m => m.State, (long)MessageState.IsDeleted),
|
||||
Builders<Message>.Filter.Nin(m => m.ChatId, activeChatIds)
|
||||
);
|
||||
var orphanedMessagesCount = await _messages.CountDocumentsAsync(orphanedFilter, cancellationToken: ct);
|
||||
|
||||
// 3. Собираем ID всех используемых файлов
|
||||
var validFileIds = new HashSet<string>();
|
||||
|
||||
// Аватары чатов и пользователей
|
||||
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);
|
||||
foreach (var avatar in userAvatars) AddFileIdIfValid(avatar, validFileIds);
|
||||
|
||||
// Медиа из актуальных сторис
|
||||
var storiesMedia = await _stories.Find(s => s.CreatedAt > DateTime.UtcNow.AddHours(-24))
|
||||
.Project(s => s.MediaUrl).ToListAsync(ct);
|
||||
foreach (var url in storiesMedia) AddFileIdIfValid(url, validFileIds);
|
||||
|
||||
// Медиа из активных сообщений (Проекция для скорости)
|
||||
var activeFilter = Builders<Message>.Filter.And(
|
||||
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))
|
||||
var validIds = new HashSet<string>();
|
||||
foreach (var msg in orphanedMessages.Where(m => m.MediaUrl != null))
|
||||
{
|
||||
while (await cursor.MoveNextAsync(ct))
|
||||
var parts = msg.MediaUrl.Split('/');
|
||||
var fileId = parts.LastOrDefault();
|
||||
if (!string.IsNullOrEmpty(fileId))
|
||||
{
|
||||
foreach (var doc in cursor.Current)
|
||||
{
|
||||
if (doc.Contains("Media") && doc["Media"].IsBsonArray)
|
||||
{
|
||||
foreach (var media in doc["Media"].AsBsonArray)
|
||||
{
|
||||
if (media.IsBsonDocument && media.AsBsonDocument.Contains("Url"))
|
||||
AddFileIdIfValid(media.AsBsonDocument["Url"].AsString, validFileIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
validIds.Add(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Анализ физического хранилища
|
||||
var allStoredFiles = await _fileStorage.ListFilesAsync();
|
||||
long orphanedBytes = allStoredFiles
|
||||
.Where(file => !validFileIds.Contains(file.FileId))
|
||||
.Sum(file => file.Size);
|
||||
var stories = await _storyCollection.GetAllAsync(ct);
|
||||
var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow);
|
||||
var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0);
|
||||
|
||||
return Result.Success(new CleanupDryRunResultDto
|
||||
{
|
||||
OrphanedMessagesCount = (int)orphanedMessagesCount,
|
||||
OrphanedMediaBytes = orphanedBytes
|
||||
});
|
||||
return Result.Success(new CleanDryRunResult(
|
||||
orphanedMessageCount,
|
||||
orphanedMediaCount,
|
||||
0,
|
||||
expiredStoriesCount,
|
||||
expiredStoriesSize
|
||||
));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<CleanupDryRunResultDto>(new Error("Cleanup.Error", $"Ошибка при анализе данных: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFileIdIfValid(string? url, HashSet<string> validIds)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) return;
|
||||
|
||||
// Предполагаем формат /api/files/{id}
|
||||
if (url.Contains("/api/files/"))
|
||||
{
|
||||
var parts = url.Split('/');
|
||||
var fileId = parts.LastOrDefault();
|
||||
if (!string.IsNullOrEmpty(fileId))
|
||||
{
|
||||
validIds.Add(fileId);
|
||||
}
|
||||
return Result.Failure<CleanDryRunResult>(new Error("Admin.CleanDryRun.Error", $"Error during dry run: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Settings.Abstractions;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Services;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
|
||||
@@ -1,91 +1,64 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using Knot.Modules.Messaging.Domain;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDetailsDto>;
|
||||
public record GetUserDetailsQuery(Guid UserId) : IQuery<AdminUserDto>;
|
||||
|
||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDetailsDto>
|
||||
internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQuery, AdminUserDto>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
||||
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
|
||||
|
||||
public GetUserDetailsQueryHandler(IUserRepository userRepository, IMongoDatabase mongoDatabase)
|
||||
public GetUserDetailsQueryHandler(
|
||||
IUserRepository userRepository,
|
||||
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
||||
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_messages = mongoDatabase.GetCollection<Message>("Messages");
|
||||
_statsService = statsService;
|
||||
_statusService = statusService;
|
||||
}
|
||||
|
||||
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
|
||||
public async Task<Result<AdminUserDto>> Handle(GetUserDetailsQuery request, CancellationToken ct)
|
||||
{
|
||||
var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (targetUser == null)
|
||||
try
|
||||
{
|
||||
return Result.Failure<AdminUserDetailsDto>(AuthErrors.UserNotFound);
|
||||
}
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, ct);
|
||||
if (user == null)
|
||||
return Result.Failure<AdminUserDto>(new Error("Admin.GetUserDetails.NotFound", "User not found"));
|
||||
|
||||
List<Message> userMessages;
|
||||
try
|
||||
var statsMap = await _statsService.GetStatsForUsersAsync(new List<Guid> { request.UserId }, ct);
|
||||
var stats = statsMap.GetValueOrDefault(request.UserId, new Knot.Contracts.Messaging.Application.Abstractions.UserStats(0, 0L));
|
||||
var isOnline = _statusService.IsUserOnline(request.UserId.ToString());
|
||||
|
||||
return Result.Success(new AdminUserDto(
|
||||
user.Id,
|
||||
user.Username ?? "unknown",
|
||||
user.DisplayName ?? "User",
|
||||
user.Email,
|
||||
user.Avatar,
|
||||
user.CreatedAt,
|
||||
isOnline,
|
||||
isOnline ? DateTime.UtcNow : (user.CreatedAt),
|
||||
user.IsBanned,
|
||||
stats.MessageCount,
|
||||
stats.StorageSize
|
||||
));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var filter = Builders<Message>.Filter.Eq(m => m.SenderId, request.UserId);
|
||||
userMessages = await _messages.Find(filter).ToListAsync(cancellationToken);
|
||||
return Result.Failure<AdminUserDto>(new Error("Admin.GetUserDetails.Error", $"Error getting user details: {ex.Message}"));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
userMessages = new List<Message>();
|
||||
}
|
||||
|
||||
var messagesCount = userMessages.Count;
|
||||
|
||||
var mediaMessages = userMessages.OfType<MediaMessage>().ToList();
|
||||
var allUserMedia = mediaMessages
|
||||
.Where(m => m.Media != null)
|
||||
.SelectMany(m => m.Media)
|
||||
.ToList();
|
||||
|
||||
var mediaCount = allUserMedia.Count(m => m.Type == "image" || m.Type == "video");
|
||||
var filesCount = allUserMedia.Count(m => m.Type == "file" || m.Type == "audio");
|
||||
var storageUsed = allUserMedia.Sum(m => m.Size ?? 0);
|
||||
|
||||
var userContents = userMessages.OfType<TextMessage>()
|
||||
.Select(m => m.Content)
|
||||
.Concat(mediaMessages.Where(m => m.Caption != null).Select(m => m.Caption))
|
||||
.Where(c => c != null)
|
||||
.ToList();
|
||||
|
||||
var linksCount = userContents.Count(c => c != null && c.Contains("http", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var isOnline = Knot.Modules.Conversations.Infrastructure.SignalR.ChatHub.IsUserOnline(targetUser.Id.ToString());
|
||||
|
||||
var result = new AdminUserDetailsDto(
|
||||
targetUser.Id,
|
||||
targetUser.Username ?? "unknown",
|
||||
targetUser.DisplayName ?? "User",
|
||||
targetUser.Bio,
|
||||
targetUser.Avatar,
|
||||
targetUser.CreatedAt,
|
||||
isOnline,
|
||||
isOnline ? DateTime.UtcNow : (targetUser.CreatedAt),
|
||||
targetUser.IsBanned,
|
||||
new AdminUserStatsDto(
|
||||
messagesCount,
|
||||
mediaCount,
|
||||
filesCount,
|
||||
linksCount,
|
||||
storageUsed
|
||||
)
|
||||
);
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Admin.Application.Admin.DTOs;
|
||||
using MediatR;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Admin.Application.Admin.Queries;
|
||||
|
||||
@@ -17,13 +17,13 @@ public record SearchUsersQuery(string Query) : IQuery<List<AdminUserDto>>;
|
||||
internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery, List<AdminUserDto>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserStatsService _statsService;
|
||||
private readonly IUserStatusService _statusService;
|
||||
private readonly Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService _statsService;
|
||||
private readonly Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService _statusService;
|
||||
|
||||
public SearchUsersQueryHandler(
|
||||
IUserRepository userRepository,
|
||||
IUserStatsService statsService,
|
||||
IUserStatusService statusService)
|
||||
IUserRepository userRepository,
|
||||
Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService statsService,
|
||||
Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService statusService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_statsService = statsService;
|
||||
@@ -34,20 +34,19 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Ïîèñê ïîëüçîâàòåëåé â ðåëÿöèîííîé ÁÄ
|
||||
var users = await _userRepository.SearchUsersAsync(request.Query ?? "", ct);
|
||||
if (!users.Any()) return Result.Success(new List<AdminUserDto>());
|
||||
|
||||
var userIds = users.Select(u => u.Id).ToList();
|
||||
|
||||
// 2. Ïîëó÷åíèå àãðåãèðîâàííîé ñòàòèñòèêè èç NoSQL
|
||||
var statsMap = await _statsService.GetStatsForUsersAsync(userIds, ct);
|
||||
|
||||
// 3. Ñáîðêà DTO ñ èñïîëüçîâàíèåì ñåðâèñîâ ñòàòóñà è ñòàòèñòèêè
|
||||
var result = users.Select(u => {
|
||||
var stats = statsMap.GetValueOrDefault(u.Id, new UserStats(0, 0L));
|
||||
var result = users.Select(u =>
|
||||
{
|
||||
var stats = statsMap.GetValueOrDefault(u.Id, new Knot.Contracts.Messaging.Application.Abstractions.UserStats(0, 0L));
|
||||
var isOnline = _statusService.IsUserOnline(u.Id.ToString());
|
||||
|
||||
|
||||
|
||||
return new AdminUserDto(
|
||||
u.Id,
|
||||
u.Username ?? "unknown",
|
||||
@@ -67,7 +66,7 @@ internal sealed class SearchUsersQueryHandler : IQueryHandler<SearchUsersQuery,
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<List<AdminUserDto>>(new Error("Admin.SearchUsers.Error", $"Îøèáêà ïðè ïîèñêå ïîëüçîâàòåëåé: {ex.Message}"));
|
||||
return Result.Failure<List<AdminUserDto>>(new Error("Admin.SearchUsers.Error", $"Error searching users: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Admin.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
|
||||
@@ -10,3 +10,4 @@ namespace Knot.Modules.Admin.Domain.Events;
|
||||
/// </summary>
|
||||
public sealed record SystemSettingsUpdatedDomainEvent(SystemSettingsDto Settings) : IDomainEvent;
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</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\Admin\Knot.Contracts.Admin.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Auth\Knot.Contracts.Auth.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||
@@ -14,14 +14,12 @@
|
||||
<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 - PrivateAssets prevents module exposure -->
|
||||
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\Klipy\Knot.Modules.Klipy.csproj" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" PrivateAssets="all" />
|
||||
<ProjectReference Include="..\..\Contracts\Storage\Knot.Contracts.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Carter" Version="10.0.0" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,22 +1,19 @@
|
||||
using Carter;
|
||||
using Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Settings.Application.Abstractions;
|
||||
using Knot.Contracts.Settings.Application.DTOs;
|
||||
using Knot.Modules.Auth.Application.Users.Register;
|
||||
using MediatR;
|
||||
using Knot.Modules.Admin.Application.Admin.Queries;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
using Knot.Modules.Admin.Application.Admin.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Knot.Modules.Admin.Application.Admin.Commands.TestKlipy;
|
||||
using Knot.Modules.Conversations.Application.Users.Commands.DeleteUser;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
|
||||
public record KlipyTestDto(string ApiKey, string AppName);
|
||||
public record ResetPasswordRequest(string NewPassword);
|
||||
|
||||
public sealed class AdminEndpoints : ICarterModule
|
||||
{
|
||||
@@ -47,20 +44,11 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
var result = await sender.Send(new GetDashboardStatsQuery(), ct);
|
||||
return Results.Ok(result.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users", async ([FromBody] RegisterUserCommand command, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(command, ct);
|
||||
if (result.IsFailure) return Results.BadRequest(new { error = result.Error.Description });
|
||||
|
||||
var userDetails = await sender.Send(new GetUserDetailsQuery(result.Value.UserId), ct);
|
||||
return Results.Ok(userDetails.Value);
|
||||
});
|
||||
|
||||
group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordDto dto, ISender sender, CancellationToken ct) =>
|
||||
group.MapPost("users/{userId:guid}/reset-password", async (Guid userId, [FromBody] ResetPasswordRequest dto, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new ResetUserPasswordCommand(userId, dto.NewPassword), ct);
|
||||
if (result.IsFailure)
|
||||
if (result.IsFailure)
|
||||
{
|
||||
if (result.Error.Code == "User.NotFound") return Results.NotFound(new { error = "User not found" });
|
||||
return Results.BadRequest(new { error = result.Error.Description });
|
||||
@@ -80,12 +68,6 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapDelete("users/{userId:guid}", async (Guid userId, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new DeleteUserCommand(userId), ct);
|
||||
return result.IsSuccess ? Results.Ok() : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapGet("users", async (ISender sender, [FromQuery] string query = "", CancellationToken ct = default) =>
|
||||
{
|
||||
var result = await sender.Send(new SearchUsersQuery(query), ct);
|
||||
@@ -117,7 +99,8 @@ public sealed class AdminEndpoints : ICarterModule
|
||||
{
|
||||
// Получаем все системные часовые пояса и формируем удобный для фронтенда формат
|
||||
var zones = TimeZoneInfo.GetSystemTimeZones()
|
||||
.Select(tz => new {
|
||||
.Select(tz => new
|
||||
{
|
||||
id = tz.Id,
|
||||
displayName = tz.DisplayName,
|
||||
standardName = tz.StandardName,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Auth.Infrastructure.Persistence;
|
||||
using Knot.Modules.Auth.Infrastructure.Authentication;
|
||||
using Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
using Knot.Shared.Kernel;
|
||||
@@ -11,27 +12,22 @@ namespace Knot.Modules.Auth;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Регистрация сервисов модуля Identity.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddAuthModule(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// Настройка базы данных
|
||||
string connectionString = configuration.GetConnectionString("DefaultConnection")!;
|
||||
|
||||
services.AddDbContext<AuthDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// Регистрация Unit of Work и Репозиториев
|
||||
services.AddScoped<IAuthUnitOfWork>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<Knot.Contracts.Auth.Application.Abstractions.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext>(sp => sp.GetRequiredService<AuthDbContext>());
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IJwtTokenProvider, JwtTokenProvider>();
|
||||
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Auth.Infrastructure.Services.UserDisplayNameProvider>();
|
||||
|
||||
// Регистрация MediatR для этого модуля
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Contracts.Auth.Application.Abstractions;
|
||||
using Knot.Contracts.Auth.Domain;
|
||||
using Knot.Contracts.Auth.Infrastructure.Persistence;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
using MediatR;
|
||||
@@ -9,7 +10,7 @@ using DomainUser = Knot.Modules.Auth.Domain.User;
|
||||
|
||||
namespace Knot.Modules.Auth.Infrastructure.Persistence;
|
||||
|
||||
public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, IAuthDbContext
|
||||
public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, Knot.Contracts.Auth.Application.Abstractions.IAuthDbContext, Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
@@ -31,6 +32,21 @@ public sealed class AuthDbContext : DbContext, IAuthUnitOfWork, IAuthDbContext
|
||||
Set<DomainUser>().Add(domainUser);
|
||||
}
|
||||
|
||||
IQueryable<UserContract> Knot.Contracts.Auth.Infrastructure.Persistence.IAuthDbContext.Users =>
|
||||
Set<DomainUser>().Select(u => new UserContract
|
||||
{
|
||||
Id = u.Id,
|
||||
Username = u.Username,
|
||||
DisplayName = u.DisplayName,
|
||||
Email = u.Email,
|
||||
Avatar = u.Avatar,
|
||||
PhoneNumber = u.PhoneNumber,
|
||||
CreatedAt = u.CreatedAt,
|
||||
IsBanned = u.IsBanned,
|
||||
Bio = u.Bio,
|
||||
PasswordHash = u.PasswordHash
|
||||
});
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
@@ -29,6 +30,7 @@ public static class DependencyInjection
|
||||
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
|
||||
|
||||
services.AddScoped<ConversationsAbstractions.IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
|
||||
services.AddScoped<Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext>(sp => sp.GetRequiredService<ChatsDbContext>());
|
||||
services.AddScoped<IChatRepository, ChatRepository>();
|
||||
services.AddScoped<IFolderRepository, FolderRepository>();
|
||||
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||
using Knot.Modules.Conversations.Application.Abstractions;
|
||||
using Knot.Modules.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
@@ -11,13 +12,11 @@ using Knot.Shared.Kernel.Security;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using DomainChat = Knot.Modules.Conversations.Domain.Chat;
|
||||
|
||||
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Контекст базы данных для модуля чатов.
|
||||
/// </summary>
|
||||
public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork
|
||||
public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
@@ -29,10 +28,13 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli
|
||||
_encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
public DbSet<Chat> Chats => Set<Chat>();
|
||||
public DbSet<DomainChat> Chats => Set<DomainChat>();
|
||||
public DbSet<Folder> Folders => Set<Folder>();
|
||||
public DbSet<UserChatSettings> UserChatSettings => Set<UserChatSettings>();
|
||||
|
||||
IQueryable<Knot.Contracts.Conversations.Infrastructure.Persistence.Chat> Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext.Chats =>
|
||||
Set<DomainChat>().Select(c => new Knot.Contracts.Conversations.Infrastructure.Persistence.Chat { Avatar = c.Avatar });
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
@@ -43,20 +45,20 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("chats");
|
||||
|
||||
modelBuilder.Entity<Chat>(builder =>
|
||||
{
|
||||
builder.ToTable("Chats");
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Type).HasConversion<string>();
|
||||
modelBuilder.Entity<DomainChat>(builder =>
|
||||
{
|
||||
builder.ToTable("Chats");
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Type).HasConversion<string>();
|
||||
|
||||
builder.OwnsMany(c => c.Members, mb =>
|
||||
{
|
||||
mb.ToTable("ChatMembers");
|
||||
mb.HasKey(m => m.Id);
|
||||
mb.WithOwner().HasForeignKey(m => m.ChatId);
|
||||
mb.HasIndex(m => new { m.ChatId, m.UserId }).IsUnique();
|
||||
}).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
});
|
||||
builder.OwnsMany(c => c.Members, mb =>
|
||||
{
|
||||
mb.ToTable("ChatMembers");
|
||||
mb.HasKey(m => m.Id);
|
||||
mb.WithOwner().HasForeignKey(m => m.ChatId);
|
||||
mb.HasIndex(m => new { m.ChatId, m.UserId }).IsUnique();
|
||||
}).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Folder>(builder =>
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using Knot.Modules.Klipy.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Klipy.Infrastructure.External;
|
||||
|
||||
public sealed class KlipyClient : IKlipyClient
|
||||
public sealed class KlipyClient : Knot.Modules.Klipy.Application.Abstractions.IKlipyClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
@@ -20,10 +20,9 @@ public sealed class KlipyClient : IKlipyClient
|
||||
{
|
||||
try
|
||||
{
|
||||
// According to docs: https://api.klipy.com/api/v1/{app_key}/gifs/trending
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.klipy.com/api/v1/{appName}/gifs/trending?page=1&per_page=1&customer_id=knot_admin_test");
|
||||
request.Headers.Add("X-KLIPY-API-KEY", apiKey);
|
||||
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
@@ -35,7 +34,6 @@ public sealed class KlipyClient : IKlipyClient
|
||||
|
||||
public async Task<List<string>> SearchVideosAsync(string apiKey, string appName, string query, int limit, CancellationToken ct = default)
|
||||
{
|
||||
// To be implemented as needed
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using Knot.Shared.Kernel;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Driver;
|
||||
using ContractUserStatsService = Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService;
|
||||
using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService;
|
||||
|
||||
namespace Knot.Modules.Messaging;
|
||||
|
||||
@@ -27,7 +29,8 @@ public static class DependencyInjection
|
||||
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
services.AddScoped<IMessageReactionRepository, MessageReactionRepository>();
|
||||
services.AddScoped<IUserStatsService, UserStatsService>();
|
||||
services.AddScoped<ModuleUserStatsService, UserStatsService>();
|
||||
services.AddScoped<ContractUserStatsService>(sp => (ContractUserStatsService)sp.GetRequiredService<ModuleUserStatsService>());
|
||||
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
@@ -4,15 +4,17 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Messaging.Abstractions;
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Messaging.Application.Abstractions;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using ContractUserStatsService = Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService;
|
||||
using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService;
|
||||
|
||||
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
|
||||
|
||||
public sealed class UserStatsService : ModuleUserStatsService
|
||||
public sealed class UserStatsService : ModuleUserStatsService, ContractUserStatsService
|
||||
{
|
||||
private readonly IMongoCollection<Message> _messages;
|
||||
|
||||
@@ -23,11 +25,10 @@ public sealed class UserStatsService : ModuleUserStatsService
|
||||
|
||||
public async Task<Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default)
|
||||
{
|
||||
var userIdList = userIds.ToList();
|
||||
if (!userIdList.Any()) return new Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>();
|
||||
if (!userIds.Any()) return new Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>();
|
||||
|
||||
var stats = await _messages.Aggregate()
|
||||
.Match(Builders<Message>.Filter.In(m => m.SenderId, userIdList))
|
||||
.Match(Builders<Message>.Filter.In(m => m.SenderId, userIds))
|
||||
.Group(new BsonDocument {
|
||||
{ "_id", "$SenderId" },
|
||||
{ "Count", new BsonDocument("$sum", 1) },
|
||||
@@ -44,6 +45,28 @@ public sealed class UserStatsService : ModuleUserStatsService
|
||||
);
|
||||
}
|
||||
|
||||
async Task<Dictionary<Guid, Knot.Contracts.Messaging.Application.Abstractions.UserStats>> Knot.Contracts.Messaging.Application.Abstractions.IUserStatsService.GetStatsForUsersAsync(List<Guid> userIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!userIds.Any()) return new Dictionary<Guid, Knot.Contracts.Messaging.Application.Abstractions.UserStats>();
|
||||
|
||||
var stats = await _messages.Aggregate()
|
||||
.Match(Builders<Message>.Filter.In(m => m.SenderId, userIds))
|
||||
.Group(new BsonDocument {
|
||||
{ "_id", "$SenderId" },
|
||||
{ "Count", new BsonDocument("$sum", 1) },
|
||||
{ "MediaSize", new BsonDocument("$sum", new BsonDocument("$sum", "$Media.Size")) }
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return stats.ToDictionary(
|
||||
doc => doc["_id"].AsGuid,
|
||||
doc => new Knot.Contracts.Messaging.Application.Abstractions.UserStats(
|
||||
doc["Count"].AsInt32,
|
||||
doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<long> GetTotalStorageSizeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var result = await _messages.Aggregate()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Net.Http;
|
||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
||||
using MongoDB.Driver;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Knot.Modules.Stories;
|
||||
|
||||
@@ -13,12 +14,12 @@ public static class DependencyInjection
|
||||
public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IStoryRepository, StoryRepository>();
|
||||
|
||||
services.AddScoped<Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection, StoryCollection>();
|
||||
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
services.AddDbContext<Infrastructure.Database.StoriesDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
// MongoDB configuration happens in StoriesMongoMapConfigurator (called in Program.cs when EncryptionService is ready, or we can just call it here)
|
||||
StoriesMongoMapConfigurator.Configure();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
using MongoDB.Driver;
|
||||
using DomainStory = Knot.Modules.Stories.Domain.Story;
|
||||
|
||||
namespace Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
||||
|
||||
public class StoryCollection : Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection
|
||||
{
|
||||
private readonly IMongoCollection<DomainStory> _stories;
|
||||
|
||||
public StoryCollection(IMongoDatabase database)
|
||||
{
|
||||
_stories = database.GetCollection<DomainStory>("stories");
|
||||
}
|
||||
|
||||
public async Task<List<Knot.Contracts.Stories.Infrastructure.Persistence.StoryContract>> GetAllAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var stories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
|
||||
return stories.Select(s => new Knot.Contracts.Stories.Infrastructure.Persistence.StoryContract
|
||||
{
|
||||
Id = s.Id,
|
||||
UserId = s.UserId,
|
||||
MediaUrl = s.MediaUrl,
|
||||
MediaType = s.Type.ToString(),
|
||||
CreatedAt = s.CreatedAt,
|
||||
ExpiresAt = null
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user