Разделение
This commit is contained in:
14
backend/src/Contracts/Auth/Abstractions/IUserQueryService.cs
Normal file
14
backend/src/Contracts/Auth/Abstractions/IUserQueryService.cs
Normal file
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
10
backend/src/Contracts/Cleanup/Knot.Contracts.Cleanup.csproj
Normal file
10
backend/src/Contracts/Cleanup/Knot.Contracts.Cleanup.csproj
Normal file
@@ -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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
|
||||
6
backend/src/Contracts/Klipy/Abstractions/IKlipyClient.cs
Normal file
6
backend/src/Contracts/Klipy/Abstractions/IKlipyClient.cs
Normal file
@@ -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 DeleteUserMessagesAsync(Guid userId, 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);
|
||||
}
|
||||
11
backend/src/Contracts/Relations/Domain/Friendship.cs
Normal file
11
backend/src/Contracts/Relations/Domain/Friendship.cs
Normal file
@@ -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
|
||||
}
|
||||
12
backend/src/Contracts/Relations/Domain/UserReplica.cs
Normal file
12
backend/src/Contracts/Relations/Domain/UserReplica.cs
Normal file
@@ -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>
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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),
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
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.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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.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));
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
global using Knot.Modules.Stories.Application.Abstractions;
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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.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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user