Разделение

This commit is contained in:
Халимов Рустам
2026-03-30 15:35:28 +03:00
parent 465e84e686
commit 09e5cbaa76
57 changed files with 631 additions and 233 deletions
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Auth.Abstractions;
public interface IUserQueryService
{
Task<List<UserInfo>> GetAllUsersAsync(CancellationToken cancellationToken);
Task<List<UserInfo>> SearchUsersAsync(string query, CancellationToken cancellationToken);
}
public record UserInfo(Guid Id, string? Avatar, string? UserName, string? DisplayName, DateTime CreatedAt, string? PhoneNumber, string? Email);
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Knot.Contracts.Auth.Domain;
using Microsoft.EntityFrameworkCore;
namespace Knot.Contracts.Auth.Application.Abstractions; namespace Knot.Contracts.Auth.Application.Abstractions;
@@ -1,6 +0,0 @@
using Knot.Contracts.Auth.Domain;
using MediatR;
namespace Knot.Contracts.Auth.Application.Users.GetMe;
public record GetMeQuery() : IRequest<UserContract>;
@@ -1,10 +0,0 @@
using Knot.Contracts.Auth.Application.Auth.DTOs;
using Knot.Shared.Kernel;
using MediatR;
namespace Knot.Contracts.Auth.Application.Users.Login;
public record LoginUserCommand(
string Username,
string Password
) : IRequest<Result<AuthResponseDto>>;
@@ -1,14 +0,0 @@
using Knot.Contracts.Auth.Application.Auth.DTOs;
using Knot.Contracts.Auth.Domain;
using Knot.Shared.Kernel;
using MediatR;
namespace Knot.Contracts.Auth.Application.Users.Register;
public record RegisterUserCommand(
string Username,
string Password,
string DisplayName,
string? Email,
string? Bio
) : IRequest<Result<AuthResponseDto>>;
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Cleanup.Abstractions;
public interface ICleanupService
{
Task<CleanupData> GetCleanupDataAsync(CancellationToken cancellationToken);
Task CleanOrphanMessagesAsync(List<Guid> messageIds, CancellationToken cancellationToken);
Task CleanOrphanFilesAsync(List<string> fileIds, CancellationToken cancellationToken);
}
public record CleanupData(
List<ChatCleanupInfo> Chats,
List<MessageCleanupInfo> Messages,
List<StoryCleanupInfo> Stories,
List<UserCleanupInfo> Users,
List<FileCleanupInfo> Files);
public record ChatCleanupInfo(Guid Id, string? Avatar);
public record MessageCleanupInfo(Guid Id, Guid ChatId, bool IsDeleted, string? MediaUrl);
public record StoryCleanupInfo(Guid Id, string? MediaUrl);
public record UserCleanupInfo(Guid Id, string? Avatar);
public record FileCleanupInfo(string FileId);
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Conversations.Abstractions;
public interface IChatQueryService
{
Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken);
}
public interface IUserDeleterService
{
Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
}
public record ChatInfo(Guid Id, string? Avatar);
@@ -0,0 +1,6 @@
namespace Knot.Contracts.Conversations.Abstractions;
public interface IUserStatusService
{
bool IsUserOnline(string userId);
}
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.SignalR;
namespace Knot.Contracts.Conversations.Application.Abstractions;
public interface IStoryNotificationsClient
{
Task StoryViewed(Guid storyId, Guid userId);
Task StoryReactionAdded(Guid storyId, Guid userId, string reaction);
Task StoryReactionRemoved(Guid storyId, Guid userId);
}
public interface IChatHubClient
{
Task MessageReceived(object message);
Task MessageDeleted(Guid messageId);
Task MessageEdited(Guid messageId, object newContent);
Task ReactionAdded(Guid messageId, string reaction, Guid userId);
Task ReactionRemoved(Guid messageId, string reaction, Guid userId);
Task UserStatusChanged(Guid userId, bool isOnline);
Task ChatUpdated(object chat);
Task ChatDeleted(Guid chatId);
Task UserJoinedChat(Guid chatId, Guid userId);
Task UserLeftChat(Guid chatId, Guid userId);
}
@@ -0,0 +1,20 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Shared.Kernel;
namespace Knot.Contracts.Conversations.Infrastructure.Persistence;
public interface IChatsDbContext : IChatsUnitOfWork
{
IQueryable<Chat> Chats { get; }
}
public class Chat : AggregateRoot<Guid>
{
public Chat() : base(Guid.NewGuid()) { }
public string? Avatar { get; set; }
}
@@ -11,6 +11,10 @@
<ProjectReference Include="..\Messaging\Knot.Contracts.Messaging.csproj" /> <ProjectReference Include="..\Messaging\Knot.Contracts.Messaging.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" /> <PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
@@ -0,0 +1,6 @@
namespace Knot.Contracts.Klipy.Abstractions;
public interface IKlipyClient
{
Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Abstractions;
public interface IMessageQueryService
{
Task<List<MessageInfo>> GetAllMessagesAsync(CancellationToken cancellationToken);
Task DeleteMessagesAsync(List<Guid> messageIds, CancellationToken cancellationToken);
}
public record MessageInfo(Guid Id, Guid ChatId, bool IsDeleted, string? MediaUrl);
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Abstractions;
public interface IUserStatsService
{
Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(List<Guid> userIds, CancellationToken cancellationToken = default);
}
public record UserStats(int MessageCount, long StorageSize);
@@ -18,4 +18,5 @@ public interface IMessageRepository
Task DeleteChatMessagesAsync(Guid chatId, CancellationToken cancellationToken); Task DeleteChatMessagesAsync(Guid chatId, CancellationToken cancellationToken);
Task DeleteUserMessagesAsync(Guid userId, CancellationToken cancellationToken); Task DeleteUserMessagesAsync(Guid userId, CancellationToken cancellationToken);
Task RemoveAsync(Guid id, CancellationToken cancellationToken); Task RemoveAsync(Guid id, CancellationToken cancellationToken);
Task DeleteAsync(Message message, CancellationToken cancellationToken);
} }
@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Application.Abstractions;
public record UserStats(int MessageCount, long StorageSize);
public interface IUserStatsService
{
Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default);
Task<long> GetTotalStorageSizeAsync(CancellationToken ct = default);
Task<int> GetCountOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken ct = default);
Task<long> GetOrphanedMediaSizeAsync(HashSet<string> validFileIds, CancellationToken ct = default);
}
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Infrastructure.Persistence;
public interface IMongoMessageCollection
{
Task<List<Message>> GetAllAsync(CancellationToken cancellationToken);
Task<List<Message>> GetOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken cancellationToken);
Task DeleteManyAsync(List<Guid> messageIds, CancellationToken cancellationToken);
}
public class Message
{
public Guid Id { get; set; }
public Guid ChatId { get; set; }
public bool IsDeleted { get; set; }
public string? MediaUrl { get; set; }
public List<Media>? Media { get; set; }
}
public class Media
{
public string Url { get; set; } = string.Empty;
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Relations.Domain;
namespace Knot.Contracts.Relations.Application.Abstractions;
public interface IFriendshipRepository
{
Task<List<Friendship>> GetAcceptedFriendshipsAsync(Guid userId, CancellationToken ct = default);
}
@@ -0,0 +1,11 @@
namespace Knot.Contracts.Relations.Domain;
using System;
public record Friendship(
Guid Id,
Guid UserId,
Guid FriendId,
FriendshipStatus Status,
DateTime CreatedAt
);
@@ -0,0 +1,8 @@
namespace Knot.Contracts.Relations.Domain;
public enum FriendshipStatus
{
Pending,
Accepted,
Declined
}
@@ -0,0 +1,12 @@
namespace Knot.Contracts.Relations.Domain;
using System;
public record UserReplica(
Guid Id,
string Username,
string DisplayName,
string Avatar,
bool IsExternal,
string? Domain
);
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Stories.Abstractions;
public interface IStoryQueryService
{
Task<List<StoryInfo>> GetAllStoriesAsync(CancellationToken cancellationToken);
}
public record StoryInfo(Guid Id, string? MediaUrl);
@@ -13,6 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" /> <PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -14,12 +14,12 @@
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" /> <ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" /> <ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
<ProjectReference Include="..\..\Contracts\Klipy\Knot.Contracts.Klipy.csproj" /> <ProjectReference Include="..\..\Contracts\Klipy\Knot.Contracts.Klipy.csproj" />
<!-- Admin needs direct module access for cleanup operations --> <!-- Admin needs direct module access for cleanup operations - PrivateAssets prevents module exposure -->
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" /> <ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" /> <ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" /> <ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Klipy\Knot.Modules.Klipy.csproj" /> <ProjectReference Include="..\Klipy\Knot.Modules.Klipy.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" /> <ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" /> <PackageReference Include="Carter" Version="10.0.0" />
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Auth.Abstractions;
using Knot.Modules.Auth.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Auth.Infrastructure.Persistence;
public sealed class UserQueryService : IUserQueryService
{
private readonly AuthDbContext _context;
public UserQueryService(AuthDbContext context)
{
_context = context;
}
public async Task<List<UserInfo>> GetAllUsersAsync(CancellationToken cancellationToken)
{
var users = await _context.Users.AsNoTracking().ToListAsync(cancellationToken);
return users.Select(u => new UserInfo(u.Id, u.Avatar, u.Username, u.DisplayName, u.CreatedAt, u.PhoneNumber, u.Email)).ToList();
}
public async Task<List<UserInfo>> SearchUsersAsync(string query, CancellationToken cancellationToken)
{
var users = await _context.Users
.AsNoTracking()
.Where(u => u.Username != null && u.Username.Contains(query) ||
u.DisplayName != null && u.DisplayName.Contains(query) ||
u.Email != null && u.Email.Contains(query) ||
u.PhoneNumber != null && u.PhoneNumber.Contains(query))
.Take(50)
.ToListAsync(cancellationToken);
return users.Select(u => new UserInfo(u.Id, u.Avatar, u.Username, u.DisplayName, u.CreatedAt, u.PhoneNumber, u.Email)).ToList();
}
}
@@ -0,0 +1,11 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Application.Abstractions;
public interface IUserDeleterService
{
Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
}
@@ -1,3 +1,4 @@
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.Abstractions;
@@ -8,6 +9,7 @@ using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ConversationsAbstractions = Knot.Modules.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations; namespace Knot.Modules.Conversations;
@@ -22,28 +24,22 @@ public static class DependencyInjection
services.AddDbContext<ChatsDbContext>(options => services.AddDbContext<ChatsDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
// MongoDB Setup for Messages
ConversationsMongoDbMapConfigurator.Configure(); ConversationsMongoDbMapConfigurator.Configure();
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017"; var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
// Registration services.AddScoped<ConversationsAbstractions.IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatRepository, ChatRepository>(); services.AddScoped<IChatRepository, ChatRepository>();
services.AddScoped<IFolderRepository, FolderRepository>(); services.AddScoped<IFolderRepository, FolderRepository>();
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>(); services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>(); services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>();
// Messaging Repository - registered in Messaging module
// services.AddScoped<IMessageRepository, MessageRepository>();
// MediatR
services.AddMediatR(config => services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
services.AddScoped<Knot.Contracts.Messaging.Application.Abstractions.IChatAccessProvider, Knot.Modules.Conversations.Infrastructure.Services.ChatAccessProvider>(); 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<Knot.Contracts.Messaging.Application.Abstractions.IMessageNotifier, Knot.Modules.Conversations.Infrastructure.SignalR.MessageNotifier>();
services.AddScoped<IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>(); services.AddScoped<ConversationsAbstractions.IUserStatusService, Knot.Modules.Conversations.Infrastructure.Services.UserStatusService>();
return services; return services;
} }
} }
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Abstractions;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
public sealed class ChatQueryService : IChatQueryService
{
private readonly ChatsDbContext _context;
public ChatQueryService(ChatsDbContext context)
{
_context = context;
}
public async Task<List<ChatInfo>> GetAllChatsAsync(CancellationToken cancellationToken)
{
var chats = await _context.Chats.AsNoTracking().ToListAsync(cancellationToken);
return chats.Select(c => new ChatInfo(c.Id, c.Avatar)).ToList();
}
}
@@ -1,22 +1,23 @@
using MediatR; using System;
using Microsoft.EntityFrameworkCore; using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Diagnostics; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Domain; using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel; using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security; using Knot.Shared.Kernel.Security;
using System.Linq; using MediatR;
using System; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Conversations.Infrastructure.Persistence; namespace Knot.Modules.Conversations.Infrastructure.Persistence;
/// <summary> /// <summary>
/// Контекст базы данных для модуля чатов. /// Контекст базы данных для модуля чатов.
/// </summary> /// </summary>
public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork
{ {
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IEncryptionService _encryptionService; private readonly IEncryptionService _encryptionService;
@@ -69,7 +70,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
builder.ToTable("UserChatSettings"); builder.ToTable("UserChatSettings");
builder.HasKey(s => s.Id); builder.HasKey(s => s.Id);
builder.HasIndex(s => new { s.UserId, s.ChatId }).IsUnique(); builder.HasIndex(s => new { s.UserId, s.ChatId }).IsUnique();
builder.Property(s => s.FolderIds) builder.Property(s => s.FolderIds)
.HasConversion( .HasConversion(
v => string.Join(',', v), v => string.Join(',', v),
@@ -12,7 +12,6 @@
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" /> <ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" /> <ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" /> <ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" /> <PackageReference Include="Carter" Version="10.0.0" />
@@ -8,7 +8,6 @@
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" /> <ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" /> <PackageReference Include="Carter" Version="10.0.0" />
@@ -1,5 +1,6 @@
using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Messaging.Application.Abstractions;
using Knot.Modules.Messaging.Infrastructure.Persistence; using Knot.Modules.Messaging.Infrastructure.Persistence;
using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
using Knot.Shared.Kernel; using Knot.Shared.Kernel;
@@ -24,12 +25,10 @@ public static class DependencyInjection
services.AddScoped<IMongoDatabase>(sp => services.AddScoped<IMongoDatabase>(sp =>
sp.GetRequiredService<IMongoClient>().GetDatabase("forkmessager_chats")); sp.GetRequiredService<IMongoClient>().GetDatabase("forkmessager_chats"));
// Registration
services.AddScoped<IMessageRepository, MessageRepository>(); services.AddScoped<IMessageRepository, MessageRepository>();
services.AddScoped<IMessageReactionRepository, MessageReactionRepository>(); services.AddScoped<IMessageReactionRepository, MessageReactionRepository>();
services.AddScoped<IUserStatsService, UserStatsService>(); services.AddScoped<IUserStatsService, UserStatsService>();
// MediatR
services.AddMediatR(config => services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Abstractions;
using Knot.Contracts.Messaging.Domain;
using MongoDB.Driver;
namespace Knot.Modules.Messaging.Infrastructure.Persistence;
public sealed class MessageQueryService : IMessageQueryService
{
private readonly IMongoCollection<Message> _messages;
public MessageQueryService(IMongoDatabase database)
{
_messages = database.GetCollection<Message>("messages");
}
public async Task<List<MessageInfo>> GetAllMessagesAsync(CancellationToken cancellationToken)
{
var messages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
return messages.Select(m => new MessageInfo(
m.Id,
m.ChatId,
m.State.HasFlag(MessageState.IsDeleted),
m is MediaMessage mm && mm.Media.Any() ? mm.Media.First().Url : null
)).ToList();
}
public async Task DeleteMessagesAsync(List<Guid> messageIds, CancellationToken cancellationToken)
{
var filter = Builders<Message>.Filter.In(m => m.Id, messageIds);
await _messages.DeleteManyAsync(filter, cancellationToken);
}
}
@@ -153,6 +153,12 @@ public sealed class MessageRepository : IMessageRepository
var filter = Builders<Message>.Filter.Eq(m => m.Id, id); var filter = Builders<Message>.Filter.Eq(m => m.Id, id);
await _messages.DeleteOneAsync(filter, cancellationToken); await _messages.DeleteOneAsync(filter, cancellationToken);
} }
public async Task DeleteAsync(Message message, CancellationToken cancellationToken)
{
var filter = Builders<Message>.Filter.Eq(m => m.Id, message.Id);
await _messages.DeleteOneAsync(filter, cancellationToken);
}
} }
@@ -3,14 +3,16 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Abstractions;
using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Messaging.Application.Abstractions;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService;
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo; namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
public sealed class UserStatsService : IUserStatsService public sealed class UserStatsService : ModuleUserStatsService
{ {
private readonly IMongoCollection<Message> _messages; private readonly IMongoCollection<Message> _messages;
@@ -19,13 +21,13 @@ public sealed class UserStatsService : IUserStatsService
_messages = database.GetCollection<Message>("messages"); _messages = database.GetCollection<Message>("messages");
} }
public async Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default) public async Task<Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>> GetStatsForUsersAsync(IEnumerable<Guid> userIds, CancellationToken ct = default)
{ {
var userGuidList = userIds.ToList(); var userIdList = userIds.ToList();
if (!userGuidList.Any()) return new Dictionary<Guid, UserStats>(); if (!userIdList.Any()) return new Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>();
var stats = await _messages.Aggregate() var stats = await _messages.Aggregate()
.Match(Builders<Message>.Filter.In(m => m.SenderId, userGuidList)) .Match(Builders<Message>.Filter.In(m => m.SenderId, userIdList))
.Group(new BsonDocument { .Group(new BsonDocument {
{ "_id", "$SenderId" }, { "_id", "$SenderId" },
{ "Count", new BsonDocument("$sum", 1) }, { "Count", new BsonDocument("$sum", 1) },
@@ -35,7 +37,7 @@ public sealed class UserStatsService : IUserStatsService
return stats.ToDictionary( return stats.ToDictionary(
doc => doc["_id"].AsGuid, doc => doc["_id"].AsGuid,
doc => new UserStats( doc => new Knot.Modules.Messaging.Application.Abstractions.UserStats(
doc["Count"].AsInt32, doc["Count"].AsInt32,
doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L
) )
@@ -3,13 +3,13 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" /> <ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
<PackageReference Include="MongoDB.Driver" Version="3.2.0" /> <PackageReference Include="MongoDB.Driver" Version="3.2.0" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Relations.Application.Abstractions;
using Knot.Contracts.Relations.Domain;
using Knot.Modules.Relations.Domain;
using Knot.Modules.Relations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Relations.Application.Abstractions;
internal sealed class FriendshipRepository : IFriendshipRepository
{
private readonly RelationsDbContext _context;
public FriendshipRepository(RelationsDbContext context)
{
_context = context;
}
public async Task<List<Knot.Contracts.Relations.Domain.Friendship>> GetAcceptedFriendshipsAsync(Guid userId, CancellationToken ct = default)
{
var friendships = await _context.Set<Knot.Modules.Relations.Domain.Friendship>()
.Where(f => f.Status == Knot.Contracts.Relations.Domain.FriendshipStatus.Accepted && (f.UserId == userId || f.FriendId == userId))
.ToListAsync(ct);
return friendships.Select(f => new Knot.Contracts.Relations.Domain.Friendship(
f.Id,
f.UserId,
f.FriendId,
f.Status,
f.CreatedAt
)).ToList();
}
}
@@ -1,7 +1,9 @@
using Knot.Contracts.Relations.Application.Abstractions;
using Knot.Modules.Relations.Application.Abstractions;
using Knot.Modules.Relations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Knot.Modules.Relations.Infrastructure.Persistence;
namespace Knot.Modules.Relations; namespace Knot.Modules.Relations;
@@ -13,6 +15,8 @@ public static class DependencyInjection
services.AddDbContext<RelationsDbContext>(options => services.AddDbContext<RelationsDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
services.AddScoped<IFriendshipRepository, FriendshipRepository>();
services.AddMediatR(config => services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
@@ -1,14 +1,8 @@
using Knot.Contracts.Relations.Domain;
using Knot.Shared.Kernel; using Knot.Shared.Kernel;
namespace Knot.Modules.Relations.Domain; namespace Knot.Modules.Relations.Domain;
public enum FriendshipStatus
{
Pending,
Accepted,
Declined
}
public sealed class Friendship : Entity<Guid> public sealed class Friendship : Entity<Guid>
{ {
public Guid UserId { get; private set; } public Guid UserId { get; private set; }
@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
@@ -21,7 +22,6 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -17,7 +17,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" /> <PackageReference Include="Carter" Version="10.0.0" />
<PackageReference Include="Minio" Version="7.0.0" /> <PackageReference Include="Minio" Version="7.0.0" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" /> <ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,21 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Stories.Domain;
using Knot.Modules.Relations.Domain;
namespace Knot.Modules.Stories.Application.Abstractions;
public interface IStoriesDbContext
{
DbSet<Story> Stories { get; }
DbSet<Friendship> Friendships { get; }
DbSet<StoryViewer> StoryViewers { get; }
DbSet<TEntity> Set<TEntity>() where TEntity : class;
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
public interface IStoriesUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,18 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Stories.Application.Abstractions;
public interface IStoryNotificationService
{
Task NotifyStoryViewedAsync(
Guid storyId,
Guid userId,
string? username,
string? displayName,
string? avatar,
int newViewsCount,
Guid storyOwnerId,
CancellationToken ct = default);
}
@@ -1,12 +1,11 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Knot.Modules.Stories.Domain;
namespace Knot.Modules.Stories.Application.Abstractions; namespace Knot.Modules.Stories.Application.Abstractions;
public interface IUserRepository public interface IUserRepository
{ {
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default); Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
Task<UserReplica?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task<Knot.Contracts.Relations.Domain.UserReplica?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
} }
@@ -1,21 +1,15 @@
using Knot.Modules.Stories.Application.Stories;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediatR; using Knot.Modules.Stories.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Stories.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.SignalR;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using Knot.Modules.Stories.Application.DTOs; using Knot.Modules.Stories.Application.DTOs;
using Knot.Modules.Stories.Application.Stories;
using Knot.Modules.Stories.Domain;
using Knot.Modules.Stories.Infrastructure.Database; using Knot.Modules.Stories.Infrastructure.Database;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Stories.Application.Stories.Commands.ViewStory; namespace Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
@@ -26,18 +20,18 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
private readonly StoriesDbContext _context; private readonly StoriesDbContext _context;
private readonly Knot.Modules.Stories.Application.Abstractions.IStoryRepository _storyRepository; private readonly Knot.Modules.Stories.Application.Abstractions.IStoryRepository _storyRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IHubContext<ChatHub> _hubContext; private readonly IStoryNotificationService _notificationService;
public ViewStoryCommandHandler( public ViewStoryCommandHandler(
StoriesDbContext context, StoriesDbContext context,
Knot.Modules.Stories.Application.Abstractions.IStoryRepository storyRepository, Knot.Modules.Stories.Application.Abstractions.IStoryRepository storyRepository,
IUserRepository userRepository, IUserRepository userRepository,
IHubContext<ChatHub> hubContext) IStoryNotificationService notificationService)
{ {
_context = context; _context = context;
_storyRepository = storyRepository; _storyRepository = storyRepository;
_userRepository = userRepository; _userRepository = userRepository;
_hubContext = hubContext; _notificationService = notificationService;
} }
public async Task<Result<MessageResponse>> Handle(ViewStoryCommand request, CancellationToken cancellationToken) public async Task<Result<MessageResponse>> Handle(ViewStoryCommand request, CancellationToken cancellationToken)
@@ -60,22 +54,20 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
{ {
_context.StoryViewers.Add(StoryViewer.Create(story.Id, request.UserId)); _context.StoryViewers.Add(StoryViewer.Create(story.Id, request.UserId));
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
await _storyRepository.IncrementViewsCountAsync(story.Id, cancellationToken); await _storyRepository.IncrementViewsCountAsync(story.Id, cancellationToken);
var viewer = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var viewer = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
await _hubContext.Clients.All.SendAsync("story_viewed", new await _notificationService.NotifyStoryViewedAsync(
{ story.Id,
storyId = story.Id, request.UserId,
userId = request.UserId, viewer?.Username,
username = viewer?.Username, viewer?.DisplayName,
displayName = viewer?.DisplayName, viewer?.Avatar,
avatar = viewer?.Avatar, story.ViewsCount + 1,
viewedAt = DateTime.UtcNow, story.UserId,
viewCount = story.ViewsCount + 1, cancellationToken);
ownerId = story.UserId
}, cancellationToken);
} }
return Result.Success(new MessageResponse("Story viewed")); return Result.Success(new MessageResponse("Story viewed"));
} }
@@ -3,18 +3,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediatR; using Knot.Contracts.Relations.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Stories.Domain;
using Knot.Modules.Stories.Application.Abstractions; using Knot.Modules.Stories.Application.Abstractions;
using Knot.Modules.Stories.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Stories.Application.DTOs; using Knot.Modules.Stories.Application.DTOs;
using Knot.Modules.Stories.Domain;
using Knot.Modules.Stories.Infrastructure.Database;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
@@ -27,20 +23,24 @@ internal sealed class GetStoriesQueryHandler : IQueryHandler<GetStoriesQuery, Li
private readonly StoriesDbContext _context; private readonly StoriesDbContext _context;
private readonly IStoryRepository _storyRepository; private readonly IStoryRepository _storyRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IFriendshipRepository _friendshipRepository;
public GetStoriesQueryHandler(StoriesDbContext context, IStoryRepository storyRepository, IUserRepository userRepository) public GetStoriesQueryHandler(
StoriesDbContext context,
IStoryRepository storyRepository,
IUserRepository userRepository,
IFriendshipRepository friendshipRepository)
{ {
_context = context; _context = context;
_storyRepository = storyRepository; _storyRepository = storyRepository;
_userRepository = userRepository; _userRepository = userRepository;
_friendshipRepository = friendshipRepository;
} }
public async Task<Result<List<StoryGroupDto>>> Handle(GetStoriesQuery request, CancellationToken cancellationToken) public async Task<Result<List<StoryGroupDto>>> Handle(GetStoriesQuery request, CancellationToken cancellationToken)
{ {
var friendships = await _context.Set<Friendship>() var friendships = await _friendshipRepository.GetAcceptedFriendshipsAsync(request.UserId, cancellationToken);
.Where(f => f.Status == FriendshipStatus.Accepted && (f.UserId == request.UserId || f.FriendId == request.UserId))
.ToListAsync(cancellationToken);
var friendIds = friendships.Select(f => f.UserId == request.UserId ? f.FriendId : f.UserId).ToList(); var friendIds = friendships.Select(f => f.UserId == request.UserId ? f.FriendId : f.UserId).ToList();
friendIds.Add(request.UserId); friendIds.Add(request.UserId);
@@ -89,14 +89,14 @@ internal sealed class GetStoriesQueryHandler : IQueryHandler<GetStoriesQuery, Li
)); ));
} }
return Result.Success(result.OrderBy(r => return Result.Success(result.OrderBy(r =>
{
if (r.User.Id == request.UserId)
{ {
if (r.User.Id == request.UserId) return 0;
{ }
return 0; return 1;
} }).ToList());
return 1;
}).ToList());
} }
} }
@@ -2,15 +2,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using MediatR;
using Knot.Shared.Kernel;
using Knot.Modules.Stories.Application.Abstractions; using Knot.Modules.Stories.Application.Abstractions;
using Knot.Modules.Stories.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Knot.Modules.Stories.Application.DTOs; using Knot.Modules.Stories.Application.DTOs;
using Knot.Modules.Stories.Infrastructure.Database;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
@@ -69,7 +66,7 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler<GetUserStoriesQ
return Result.Success(result); return Result.Success(result);
} }
private async Task<Result<StoryGroupDto>> GetFederatedStories(Knot.Modules.Relations.Domain.UserReplica user, Guid currentUserId, CancellationToken ct) private async Task<Result<StoryGroupDto>> GetFederatedStories(Knot.Contracts.Relations.Domain.UserReplica user, Guid currentUserId, CancellationToken ct)
{ {
// В будущем: Реальный вызов через HttpClient к удаленному домену // В будущем: Реальный вызов через HttpClient к удаленному домену
// return await _federationGateway.FetchStoriesAsync(user.Domain, user.Id); // return await _federationGateway.FetchStoriesAsync(user.Domain, user.Id);
@@ -1,3 +1,2 @@
global using Knot.Modules.Stories.Application.Abstractions; global using Knot.Modules.Stories.Application.Abstractions;
global using Knot.Modules.Stories.Domain; global using Knot.Modules.Stories.Domain;
global using Knot.Modules.Relations.Domain;
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Stories.Abstractions;
using Knot.Modules.Stories.Domain;
using MongoDB.Driver;
namespace Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
public sealed class StoryQueryService : IStoryQueryService
{
private readonly IMongoCollection<Story> _stories;
public StoryQueryService(IMongoDatabase database)
{
_stories = database.GetCollection<Story>("stories");
}
public async Task<List<StoryInfo>> GetAllStoriesAsync(CancellationToken cancellationToken)
{
var stories = await _stories.Find(_ => true).ToListAsync(cancellationToken);
return stories.Select(s => new StoryInfo(s.Id, s.MediaUrl)).ToList();
}
}
@@ -0,0 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Knot.Modules.Stories.Application.Abstractions;
internal sealed class StoryNotificationService : IStoryNotificationService
{
private readonly ILogger<StoryNotificationService> _logger;
public StoryNotificationService(ILogger<StoryNotificationService> logger)
{
_logger = logger;
}
public Task NotifyStoryViewedAsync(
Guid storyId,
Guid userId,
string? username,
string? displayName,
string? avatar,
int newViewsCount,
Guid storyOwnerId,
CancellationToken ct = default)
{
// TODO: Реализовать отправку через SignalR
// Пока заглушка - в будущем использовать IHubContext
_logger.LogInformation(
"Story {StoryId} viewed by user {UserId}. New views count: {ViewsCount}",
storyId, userId, newViewsCount);
return Task.CompletedTask;
}
}
@@ -7,10 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
<ProjectReference Include="..\Relations\Knot.Modules.Relations.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -24,7 +25,6 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,5 +1,3 @@
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Messaging.Domain;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -8,11 +6,11 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using AngleSharp.Dom; using AngleSharp.Dom;
using Knot.Shared.Kernel; using AngleSharp.Html.Parser;
using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Contracts.Settings.Application.DTOs; using Knot.Contracts.Settings.Application.DTOs;
using Knot.Shared.Kernel;
using MediatR; using MediatR;
namespace Knot.Modules.TelegramImport.Application.TelegramImport; namespace Knot.Modules.TelegramImport.Application.TelegramImport;
@@ -25,8 +23,8 @@ public static class TelegramImportState
public record ImportConflictDto(string Type, string Message, bool Blocked); public record ImportConflictDto(string Type, string Message, bool Blocked);
public record AnalyzeImportResponseDto( public record AnalyzeImportResponseDto(
Guid Token, Guid Token,
List<string> Names, List<string> Names,
List<ImportConflictDto> Conflicts); List<ImportConflictDto> Conflicts);
public record AnalyzeImportCommand(Stream FileStream, string FileName) : ICommand<AnalyzeImportResponseDto>; public record AnalyzeImportCommand(Stream FileStream, string FileName) : ICommand<AnalyzeImportResponseDto>;
@@ -43,12 +41,12 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
{ {
if (request.FileStream == null || request.FileStream.Length == 0) if (request.FileStream == null || request.FileStream.Length == 0)
{ {
return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileEmpty); return Result.Failure<AnalyzeImportResponseDto>(TelegramImportErrors.FileEmpty);
} }
if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{ {
return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileInvalidExtension); return Result.Failure<AnalyzeImportResponseDto>(TelegramImportErrors.FileInvalidExtension);
} }
var token = Guid.NewGuid(); var token = Guid.NewGuid();
@@ -104,12 +102,12 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
var settings = await _settingsService.GetSettingsAsync(cancellationToken); var settings = await _settingsService.GetSettingsAsync(cancellationToken);
var conflicts = new List<ImportConflictDto>(); var conflicts = new List<ImportConflictDto>();
if (!settings.Messages.AllowMedia) if (!settings.Messages.AllowMedia)
conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true)); conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true));
if (!settings.Messages.AllowVoiceMessages) if (!settings.Messages.AllowVoiceMessages)
conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true)); conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true));
if (!settings.Messages.AllowPolls) if (!settings.Messages.AllowPolls)
conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true)); conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true));
@@ -0,0 +1,7 @@
namespace Knot.Modules.TelegramImport.Application.TelegramImport;
public static class TelegramImportErrors
{
public static readonly Knot.Shared.Kernel.Error FileEmpty = new("File.Empty", "No file uploaded");
public static readonly Knot.Shared.Kernel.Error FileInvalidExtension = new("File.InvalidExtension", "Only .zip files are supported");
}
@@ -4,17 +4,17 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Modules.TelegramImport.Application.TelegramImport;
using Knot.Modules.TelegramImport.Infrastructure.Background;
using Knot.Shared.Kernel.Storage;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Modules.TelegramImport.Application.TelegramImport;
using Knot.Modules.Messaging.Application.Abstractions;
using Knot.Modules.Conversations.Application.Abstractions;
using Knot.Modules.Messaging.Domain;
using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel.Storage;
using Knot.Modules.TelegramImport.Infrastructure.Background;
namespace Knot.Modules.TelegramImport.Infrastructure.Background; namespace Knot.Modules.TelegramImport.Infrastructure.Background;
@@ -34,7 +34,7 @@ public class TelegramImportWorker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_logger.LogInformation("Telegram Import Worker started."); _logger.LogInformation("Telegram Import Worker started.");
// В реальном проекте здесь будет чтение из Channels или RabbitMQ // В реальном проекте здесь будет чтение из Channels или RabbitMQ
// Для примера оставим заглушку цикла // Для примера оставим заглушку цикла
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
@@ -55,33 +55,33 @@ public class TelegramImportWorker : BackgroundService
var jobInfo = new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Processing }; var jobInfo = new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Processing };
_jobStore.AddOrUpdate(jobInfo); _jobStore.AddOrUpdate(jobInfo);
try try
{ {
using var archive = ZipFile.OpenRead(zipPath); using var archive = ZipFile.OpenRead(zipPath);
var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList(); var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList();
// 1. Создание чата (уже было в оригинале, но здесь в фоне) // 1. Создание чата (уже было в оригинале, но здесь в фоне)
Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга
foreach (var entry in entries) foreach (var entry in entries)
{ {
using var stream = entry.Open(); using var stream = entry.Open();
var messages = await parser.ParseMessagesAsync(stream, "", ct); var messages = await parser.ParseMessagesAsync(stream, "", ct);
foreach (var m in messages) foreach (var m in messages)
{ {
Guid senderGuid = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId; Guid senderGuid = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId;
var textMsg = new TextMessage(Guid.NewGuid(), targetChatId, senderGuid, m.Content, null, null, null, m.CreatedAt, true); var textMsg = new TextMessage(Guid.NewGuid(), targetChatId, senderGuid, m.Content, null, null, null, m.CreatedAt, true);
msgRepo.Add(textMsg); msgRepo.Add(textMsg);
jobInfo.ProcessedMessages++; jobInfo.ProcessedMessages++;
_jobStore.AddOrUpdate(jobInfo); _jobStore.AddOrUpdate(jobInfo);
} }
await uow.SaveChangesAsync(ct); await uow.SaveChangesAsync(ct);
} }
jobInfo.Status = ImportJobStatus.Completed; jobInfo.Status = ImportJobStatus.Completed;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -2,9 +2,14 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" /> <ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" /> <ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" /> <ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2" />
<PackageReference Include="Carter" Version="10.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -7,14 +7,11 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" /> <ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" /> <ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" /> <PackageReference Include="Carter" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.1.25120.3" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.1.25120.3" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>