Разделение

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

View File

@@ -14,12 +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 -->
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
<ProjectReference Include="..\Klipy\Knot.Modules.Klipy.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
<!-- 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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" />

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -1,3 +1,4 @@
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.Abstractions;
@@ -8,6 +9,7 @@ using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ConversationsAbstractions = Knot.Modules.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations;
@@ -22,28 +24,22 @@ public static class DependencyInjection
services.AddDbContext<ChatsDbContext>(options =>
options.UseNpgsql(connectionString));
// MongoDB Setup for Messages
ConversationsMongoDbMapConfigurator.Configure();
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
// Registration
services.AddScoped<IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<ConversationsAbstractions.IChatsUnitOfWork>(sp => sp.GetRequiredService<ChatsDbContext>());
services.AddScoped<IChatRepository, ChatRepository>();
services.AddScoped<IFolderRepository, FolderRepository>();
services.AddScoped<IUserChatSettingsRepository, UserChatSettingsRepository>();
services.AddScoped<IUserFolderSettingsRepository, UserFolderSettingsRepository>();
// Messaging Repository - registered in Messaging module
// services.AddScoped<IMessageRepository, MessageRepository>();
// MediatR
services.AddMediatR(config =>
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.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;
}
}

View File

@@ -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();
}
}

View File

@@ -1,22 +1,23 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using System;
using System.Collections.Generic;
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.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
/// <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 IEncryptionService _encryptionService;
@@ -69,7 +70,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
builder.ToTable("UserChatSettings");
builder.HasKey(s => s.Id);
builder.HasIndex(s => new { s.UserId, s.ChatId }).IsUnique();
builder.Property(s => s.FolderIds)
.HasConversion(
v => string.Join(',', v),

View File

@@ -12,7 +12,6 @@
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
<ProjectReference Include="..\..\Contracts\Stories\Knot.Contracts.Stories.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" />

View File

@@ -8,7 +8,6 @@
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" />

View File

@@ -1,5 +1,6 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Messaging.Application.Abstractions;
using Knot.Modules.Messaging.Infrastructure.Persistence;
using Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
using Knot.Shared.Kernel;
@@ -24,12 +25,10 @@ public static class DependencyInjection
services.AddScoped<IMongoDatabase>(sp =>
sp.GetRequiredService<IMongoClient>().GetDatabase("forkmessager_chats"));
// Registration
services.AddScoped<IMessageRepository, MessageRepository>();
services.AddScoped<IMessageReactionRepository, MessageReactionRepository>();
services.AddScoped<IUserStatsService, UserStatsService>();
// MediatR
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));

View File

@@ -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);
}
}

View File

@@ -153,6 +153,12 @@ public sealed class MessageRepository : IMessageRepository
var filter = Builders<Message>.Filter.Eq(m => m.Id, id);
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);
}
}

View File

@@ -3,14 +3,16 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Messaging.Application.Abstractions;
using MongoDB.Bson;
using MongoDB.Driver;
using ModuleUserStatsService = Knot.Modules.Messaging.Application.Abstractions.IUserStatsService;
namespace Knot.Modules.Messaging.Infrastructure.Persistence.Mongo;
public sealed class UserStatsService : IUserStatsService
public sealed class UserStatsService : ModuleUserStatsService
{
private readonly IMongoCollection<Message> _messages;
@@ -19,13 +21,13 @@ public sealed class UserStatsService : IUserStatsService
_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();
if (!userGuidList.Any()) return new Dictionary<Guid, UserStats>();
var userIdList = userIds.ToList();
if (!userIdList.Any()) return new Dictionary<Guid, Knot.Modules.Messaging.Application.Abstractions.UserStats>();
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 {
{ "_id", "$SenderId" },
{ "Count", new BsonDocument("$sum", 1) },
@@ -35,7 +37,7 @@ public sealed class UserStatsService : IUserStatsService
return stats.ToDictionary(
doc => doc["_id"].AsGuid,
doc => new UserStats(
doc => new Knot.Modules.Messaging.Application.Abstractions.UserStats(
doc["Count"].AsInt32,
doc.Contains("MediaSize") && !doc["MediaSize"].IsBsonNull ? (long)(doc["MediaSize"].IsInt64 ? doc["MediaSize"].AsInt64 : doc["MediaSize"].AsInt32) : 0L
)

View File

@@ -3,13 +3,13 @@
<ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</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="MongoDB.Driver" Version="3.2.0" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup>
<PropertyGroup>

View File

@@ -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();
}
}

View File

@@ -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.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Knot.Modules.Relations.Infrastructure.Persistence;
namespace Knot.Modules.Relations;
@@ -13,6 +15,8 @@ public static class DependencyInjection
services.AddDbContext<RelationsDbContext>(options =>
options.UseNpgsql(connectionString));
services.AddScoped<IFriendshipRepository, FriendshipRepository>();
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));

View File

@@ -1,14 +1,8 @@
using Knot.Contracts.Relations.Domain;
using Knot.Shared.Kernel;
namespace Knot.Modules.Relations.Domain;
public enum FriendshipStatus
{
Pending,
Accepted,
Declined
}
public sealed class Friendship : Entity<Guid>
{
public Guid UserId { get; private set; }

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
</ItemGroup>
@@ -21,7 +22,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Carter" Version="10.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>
</Project>

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,12 +1,11 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Stories.Domain;
namespace Knot.Modules.Stories.Application.Abstractions;
public interface IUserRepository
{
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);
}

View File

@@ -1,21 +1,15 @@
using Knot.Modules.Stories.Application.Stories;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
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.Abstractions;
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.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Knot.Modules.Stories.Application.Stories.Commands.ViewStory;
@@ -26,18 +20,18 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
private readonly StoriesDbContext _context;
private readonly Knot.Modules.Stories.Application.Abstractions.IStoryRepository _storyRepository;
private readonly IUserRepository _userRepository;
private readonly IHubContext<ChatHub> _hubContext;
private readonly IStoryNotificationService _notificationService;
public ViewStoryCommandHandler(
StoriesDbContext context,
StoriesDbContext context,
Knot.Modules.Stories.Application.Abstractions.IStoryRepository storyRepository,
IUserRepository userRepository,
IHubContext<ChatHub> hubContext)
IUserRepository userRepository,
IStoryNotificationService notificationService)
{
_context = context;
_storyRepository = storyRepository;
_userRepository = userRepository;
_hubContext = hubContext;
_notificationService = notificationService;
}
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));
await _context.SaveChangesAsync(cancellationToken);
await _storyRepository.IncrementViewsCountAsync(story.Id, cancellationToken);
var viewer = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
await _hubContext.Clients.All.SendAsync("story_viewed", new
{
storyId = story.Id,
userId = request.UserId,
username = viewer?.Username,
displayName = viewer?.DisplayName,
avatar = viewer?.Avatar,
viewedAt = DateTime.UtcNow,
viewCount = story.ViewsCount + 1,
ownerId = story.UserId
}, cancellationToken);
await _notificationService.NotifyStoryViewedAsync(
story.Id,
request.UserId,
viewer?.Username,
viewer?.DisplayName,
viewer?.Avatar,
story.ViewsCount + 1,
story.UserId,
cancellationToken);
}
return Result.Success(new MessageResponse("Story viewed"));
}

View File

@@ -3,18 +3,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Knot.Shared.Kernel;
using Knot.Modules.Stories.Domain;
using Knot.Contracts.Relations.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.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 IStoryRepository _storyRepository;
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;
_storyRepository = storyRepository;
_userRepository = userRepository;
_friendshipRepository = friendshipRepository;
}
public async Task<Result<List<StoryGroupDto>>> Handle(GetStoriesQuery request, CancellationToken cancellationToken)
{
var friendships = await _context.Set<Friendship>()
.Where(f => f.Status == FriendshipStatus.Accepted && (f.UserId == request.UserId || f.FriendId == request.UserId))
.ToListAsync(cancellationToken);
var friendships = await _friendshipRepository.GetAcceptedFriendshipsAsync(request.UserId, cancellationToken);
var friendIds = friendships.Select(f => f.UserId == request.UserId ? f.FriendId : f.UserId).ToList();
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 1;
}).ToList());
return 0;
}
return 1;
}).ToList());
}
}

View File

@@ -2,15 +2,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using MediatR;
using Knot.Shared.Kernel;
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.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);
}
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 к удаленному домену
// return await _federationGateway.FetchStoriesAsync(user.Domain, user.Id);

View File

@@ -1,3 +1,2 @@
global using Knot.Modules.Stories.Application.Abstractions;
global using Knot.Modules.Stories.Domain;
global using Knot.Modules.Relations.Domain;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -7,10 +7,11 @@
</PropertyGroup>
<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="..\Conversations\Knot.Modules.Conversations.csproj" />
<ProjectReference Include="..\Relations\Knot.Modules.Relations.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
</ItemGroup>
<ItemGroup>
@@ -24,7 +25,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,3 @@
using Knot.Modules.Conversations.Domain;
using Knot.Modules.Messaging.Domain;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -8,11 +6,11 @@ using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using AngleSharp.Dom;
using Knot.Shared.Kernel;
using AngleSharp.Html.Parser;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Contracts.Settings.Application.DTOs;
using Knot.Shared.Kernel;
using MediatR;
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 AnalyzeImportResponseDto(
Guid Token,
List<string> Names,
Guid Token,
List<string> Names,
List<ImportConflictDto> Conflicts);
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)
{
return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileEmpty);
return Result.Failure<AnalyzeImportResponseDto>(TelegramImportErrors.FileEmpty);
}
if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
return Result.Failure<AnalyzeImportResponseDto>(ChatErrors.FileInvalidExtension);
return Result.Failure<AnalyzeImportResponseDto>(TelegramImportErrors.FileInvalidExtension);
}
var token = Guid.NewGuid();
@@ -104,12 +102,12 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
var settings = await _settingsService.GetSettingsAsync(cancellationToken);
var conflicts = new List<ImportConflictDto>();
if (!settings.Messages.AllowMedia)
if (!settings.Messages.AllowMedia)
conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true));
if (!settings.Messages.AllowVoiceMessages)
conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true));
if (!settings.Messages.AllowPolls)
conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true));

View File

@@ -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");
}

View File

@@ -4,17 +4,17 @@ using System.IO.Compression;
using System.Linq;
using System.Threading;
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.Hosting;
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;
@@ -34,7 +34,7 @@ public class TelegramImportWorker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Telegram Import Worker started.");
// В реальном проекте здесь будет чтение из Channels или RabbitMQ
// Для примера оставим заглушку цикла
while (!stoppingToken.IsCancellationRequested)
@@ -55,33 +55,33 @@ public class TelegramImportWorker : BackgroundService
var jobInfo = new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Processing };
_jobStore.AddOrUpdate(jobInfo);
try
try
{
using var archive = ZipFile.OpenRead(zipPath);
var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList();
using var archive = ZipFile.OpenRead(zipPath);
var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList();
// 1. Создание чата (уже было в оригинале, но здесь в фоне)
Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга
// 1. Создание чата (уже было в оригинале, но здесь в фоне)
Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга
foreach (var entry in entries)
{
using var stream = entry.Open();
var messages = await parser.ParseMessagesAsync(stream, "", ct);
foreach (var m in messages)
{
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);
msgRepo.Add(textMsg);
jobInfo.ProcessedMessages++;
_jobStore.AddOrUpdate(jobInfo);
}
await uow.SaveChangesAsync(ct);
}
jobInfo.Status = ImportJobStatus.Completed;
foreach (var entry in entries)
{
using var stream = entry.Open();
var messages = await parser.ParseMessagesAsync(stream, "", ct);
foreach (var m in messages)
{
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);
msgRepo.Add(textMsg);
jobInfo.ProcessedMessages++;
_jobStore.AddOrUpdate(jobInfo);
}
await uow.SaveChangesAsync(ct);
}
jobInfo.Status = ImportJobStatus.Completed;
}
catch (Exception ex)
{

View File

@@ -2,9 +2,14 @@
<ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
<ProjectReference Include="..\Messaging\Knot.Modules.Messaging.csproj" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\..\Contracts\Messaging\Knot.Contracts.Messaging.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2" />
<PackageReference Include="Carter" Version="10.0.0" />
</ItemGroup>
<PropertyGroup>

View File

@@ -7,14 +7,11 @@
<ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
<ProjectReference Include="..\Conversations\Knot.Modules.Conversations.csproj" />
<ProjectReference Include="..\Auth\Knot.Modules.Auth.csproj" />
<ProjectReference Include="..\Stories\Knot.Modules.Stories.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.1.25120.3" />
<ProjectReference Include="..\Settings\Knot.Modules.Settings.csproj" />
</ItemGroup>
</Project>