Рабочий чат

This commit is contained in:
Халимов Рустам
2026-04-01 23:18:55 +03:00
parent 249c344df8
commit 4ae7dd60ce
42 changed files with 1016 additions and 184 deletions

View File

@@ -0,0 +1,31 @@
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Events;
using Knot.Contracts.Profiles.Domain;
using Knot.Modules.Profiles.Domain;
using MediatR;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Application.Profiles.Integration;
internal sealed class UserStatusChangedHandler :
INotificationHandler<UserBannedDomainEvent>,
INotificationHandler<UserDeletedDomainEvent>
{
private readonly IProfileRepository _repository;
public UserStatusChangedHandler(IProfileRepository repository)
{
_repository = repository;
}
public async Task Handle(UserBannedDomainEvent notification, CancellationToken cancellationToken)
{
await _repository.UpdateStatusAsync(notification.UserId, notification.IsBanned, false, cancellationToken);
}
public async Task Handle(UserDeletedDomainEvent notification, CancellationToken cancellationToken)
{
await _repository.DeleteAsync(notification.UserId, cancellationToken);
}
}

View File

@@ -5,23 +5,69 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using System;
using Knot.Contracts.Relations.Application.Contacts;
using Knot.Contracts.Auth.Application.Abstractions;
namespace Knot.Modules.Profiles.Application.Profiles.Search;
public sealed record SearchProfilesQuery(string Query) : IQuery<List<UserProfileDto>>;
public sealed record SearchProfilesQuery(string Query, Guid UserId) : IQuery<List<UserProfileDto>>;
internal sealed class SearchProfilesQueryHandler : IQueryHandler<SearchProfilesQuery, List<UserProfileDto>>
{
private readonly IProfileRepository _repository;
private readonly ISender _sender;
public SearchProfilesQueryHandler(IProfileRepository repository)
public SearchProfilesQueryHandler(IProfileRepository repository, ISender sender)
{
_repository = repository;
_sender = sender;
}
public async Task<Result<List<UserProfileDto>>> Handle(SearchProfilesQuery request, CancellationToken cancellationToken)
{
// 1. Fetch candidates from MongoDB (which might have orphans)
var profiles = await _repository.SearchAsync(request.Query, 20, cancellationToken);
return Result.Success(profiles);
var candidates = profiles.Where(p => p.UserId != request.UserId).ToList();
if (!candidates.Any()) return Result.Success(new List<UserProfileDto>());
var candidateIds = candidates.Select(p => p.UserId).ToList();
// 2. Validate existence in Auth module (to filter out orphans from deleted accounts)
var existingIds = new List<Guid>();
try {
var existenceResult = await _sender.Send(new GetUsersExistenceQuery(candidateIds), cancellationToken);
if (existenceResult.IsSuccess) existingIds = existenceResult.Value;
} catch {
// If Auth module is not available, we assume all exist to avoid empty results
existingIds = candidateIds;
}
// 3. Remove orphans (and physically delete them from Mongo if they don't exist in Auth)
var validCandidates = candidates.Where(p => existingIds.Contains(p.UserId)).ToList();
var orphanIds = candidateIds.Except(existingIds).ToList();
foreach (var orphanId in orphanIds)
{
// Background cleanup (fire and forget or just do it since it's only a few)
_ = _repository.DeleteAsync(orphanId, CancellationToken.None);
}
if (!validCandidates.Any()) return Result.Success(new List<UserProfileDto>());
// 4. Check for blocked users among valid candidates
var blockedIds = new List<Guid>();
try {
var blockedResult = await _sender.Send(new CheckBlockedStatusQuery(request.UserId, validCandidates.Select(v => v.UserId).ToList()), cancellationToken);
if (blockedResult.IsSuccess) blockedIds = blockedResult.Value;
} catch { }
// final filter
var filtered = validCandidates
.Where(p => !blockedIds.Contains(p.UserId))
.ToList();
return Result.Success(filtered);
}
}

View File

@@ -25,6 +25,10 @@ public sealed class ProfileDocument
public bool HideStoryViews { get; private set; }
public bool IsBanned { get; private set; }
public bool IsDeleted { get; private set; }
public DateTime CreatedAt { get; private set; }
#pragma warning disable CS8618
@@ -37,6 +41,8 @@ public sealed class ProfileDocument
Username = username;
DisplayName = displayName;
Bio = bio;
IsBanned = false;
IsDeleted = false;
CreatedAt = DateTime.UtcNow;
}
@@ -58,4 +64,10 @@ public sealed class ProfileDocument
public void UpdateSettings(bool hideStoryViews)
=> HideStoryViews = hideStoryViews;
public void UpdateStatus(bool isBanned, bool isDeleted)
{
IsBanned = isBanned;
IsDeleted = isDeleted;
}
}

View File

@@ -43,18 +43,25 @@ internal class ProfileRepository : IProfileRepository
public async Task<List<UserProfileDto>> SearchAsync(string query, int limit = 20, CancellationToken ct = default)
{
var baseFilter = Builders<ProfileDocument>.Filter.And(
Builders<ProfileDocument>.Filter.Ne(p => p.IsBanned, true),
Builders<ProfileDocument>.Filter.Ne(p => p.IsDeleted, true)
);
List<ProfileDocument> docs;
if (string.IsNullOrWhiteSpace(query))
{
docs = await _profiles.Find(_ => true).Limit(limit).ToListAsync(ct);
docs = await _profiles.Find(baseFilter).Limit(limit).ToListAsync(ct);
}
else
{
var filter = Builders<ProfileDocument>.Filter.Or(
var searchFilter = Builders<ProfileDocument>.Filter.Or(
Builders<ProfileDocument>.Filter.Regex(p => p.Username, new BsonRegularExpression(query, "i")),
Builders<ProfileDocument>.Filter.Regex(p => p.DisplayName, new BsonRegularExpression(query, "i"))
);
docs = await _profiles.Find(filter).Limit(limit).ToListAsync(ct);
var combinedFilter = Builders<ProfileDocument>.Filter.And(baseFilter, searchFilter);
docs = await _profiles.Find(combinedFilter).Limit(limit).ToListAsync(ct);
}
return docs.Select(p => p.ToDto()).ToList();
}
@@ -82,6 +89,16 @@ internal class ProfileRepository : IProfileRepository
return Result.Success(profile.ToDto());
}
public async Task<Result> UpdateStatusAsync(Guid userId, bool isBanned, bool isDeleted, CancellationToken ct = default)
{
var profile = await _profiles.Find(p => p.Id == userId).FirstOrDefaultAsync(ct);
if (profile is null) return Result.Failure(ProfilesErrors.ProfileNotFound);
profile.UpdateStatus(isBanned, isDeleted);
await _profiles.ReplaceOneAsync(p => p.Id == userId, profile, cancellationToken: ct);
return Result.Success();
}
public async Task<Result> DeleteAsync(Guid userId, CancellationToken ct = default)
{
var result = await _profiles.DeleteOneAsync(p => p.Id == userId, ct);

View File

@@ -10,6 +10,8 @@
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
<ProjectReference Include="..\..\Shared\Knot.Shared.Infrastructure\Knot.Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
<ProjectReference Include="..\..\Contracts\Auth\Knot.Contracts.Auth.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -18,9 +18,9 @@ public static class ProfilesEndpoints
{
var group = app.MapGroup("api/profiles").RequireAuthorization();
group.MapGet("search", async ([FromQuery] string q, ISender sender, CancellationToken ct) =>
group.MapGet("search", async ([FromQuery] string q, ISender sender, IUserContext userContext, CancellationToken ct) =>
{
var result = await sender.Send(new SearchProfilesQuery(q), ct);
var result = await sender.Send(new SearchProfilesQuery(q, userContext.UserId), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error);
});