Форматирование

This commit is contained in:
Халимов Рустам
2026-03-19 01:31:43 +03:00
parent 282b43d4c1
commit 8f2de0e9bf
90 changed files with 901 additions and 299 deletions
+6
View File
@@ -0,0 +1,6 @@
root = true
[*.cs]
csharp_prefer_braces = true:warning
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.IDE0011.severity = warning
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+80
View File
@@ -0,0 +1,80 @@
using System;
using System.IO;
namespace FormatFixer
{
class Program
{
static void Main(string[] args)
{
var srcPath = @"e:\GIT\forkmessager\apps\server-net\src";
var files = Directory.GetFiles(srcPath, "*.cs", SearchOption.AllDirectories);
foreach (var file in files)
{
var lines = File.ReadAllLines(file);
bool changed = false;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var stripped = line.TrimStart();
if ((stripped.StartsWith("if (") || stripped.StartsWith("if(") ||
stripped.StartsWith("foreach (") || stripped.StartsWith("foreach("))
&& stripped.EndsWith(";"))
{
// Check if it's already a block
if (stripped.Contains("{")) continue;
int parenCount = 0;
bool inString = false;
int firstParenIdx = line.IndexOf('(');
int condEndIdx = -1;
for (int j = firstParenIdx; j < line.Length; j++)
{
char c = line[j];
if (c == '"' && line[j - 1] != '\\')
{
inString = !inString;
}
else if (!inString)
{
if (c == '(') parenCount++;
else if (c == ')')
{
parenCount--;
if (parenCount == 0)
{
condEndIdx = j;
break;
}
}
}
}
if (condEndIdx != -1)
{
var statement = line.Substring(condEndIdx + 1).Trim();
if (!string.IsNullOrEmpty(statement) && !statement.StartsWith("{") && statement.EndsWith(";"))
{
var indent = line.Substring(0, line.Length - line.TrimStart().Length);
var newLine1 = line.Substring(0, condEndIdx + 1);
var newLine2 = indent + "{";
var newLine3 = indent + " " + statement;
var newLine4 = indent + "}";
lines[i] = newLine1 + Environment.NewLine + newLine2 + Environment.NewLine + newLine3 + Environment.NewLine + newLine4;
changed = true;
}
}
}
}
if (changed)
{
File.WriteAllText(file, string.Join(Environment.NewLine, lines));
Console.WriteLine("Fixed: " + file);
}
}
Console.WriteLine("Formatting complete.");
}
}
}
@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@@ -57,9 +57,18 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
.Where(u => !string.IsNullOrEmpty(u.Avatar)) .Where(u => !string.IsNullOrEmpty(u.Avatar))
.Select(u => u.Avatar!); .Select(u => u.Avatar!);
foreach (var u in activeMessageUrls) validUrls.Add(u!); foreach (var u in activeMessageUrls)
foreach (var u in activeChatUrls) validUrls.Add(u); {
foreach (var u in activeUserUrls) validUrls.Add(u); validUrls.Add(u!);
}
foreach (var u in activeChatUrls)
{
validUrls.Add(u);
}
foreach (var u in activeUserUrls)
{
validUrls.Add(u);
}
var validFileIds = validUrls var validFileIds = validUrls
.Where(u => u.Contains("/api/files/")) .Where(u => u.Contains("/api/files/"))
@@ -82,4 +91,4 @@ internal sealed class CleanRunCommandHandler : ICommandHandler<CleanRunCommand,
return Result.Success(new MessageResponse("Cleanup completed successfully")); return Result.Success(new MessageResponse("Cleanup completed successfully"));
} }
} }
@@ -26,7 +26,10 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
public async Task<Result<SuccessResponse>> Handle(ResetUserPasswordCommand request, CancellationToken cancellationToken) public async Task<Result<SuccessResponse>> Handle(ResetUserPasswordCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<SuccessResponse>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<SuccessResponse>(new Error("User.NotFound", "User not found"));
}
if (string.IsNullOrWhiteSpace(request.NewPassword)) if (string.IsNullOrWhiteSpace(request.NewPassword))
return Result.Failure<SuccessResponse>(new Error("InvalidPassword", "Password cannot be empty")); return Result.Failure<SuccessResponse>(new Error("InvalidPassword", "Password cannot be empty"));
@@ -37,4 +40,4 @@ internal sealed class ResetUserPasswordCommandHandler : ICommandHandler<ResetUse
await _identityUnitOfWork.SaveChangesAsync(cancellationToken); await _identityUnitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(new SuccessResponse(true)); return Result.Success(new SuccessResponse(true));
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@@ -60,9 +60,18 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
.Where(u => !string.IsNullOrEmpty(u.Avatar)) .Where(u => !string.IsNullOrEmpty(u.Avatar))
.Select(u => u.Avatar!); .Select(u => u.Avatar!);
foreach (var u in activeMessageUrls) validUrls.Add(u!); foreach (var u in activeMessageUrls)
foreach (var u in activeChatUrls) validUrls.Add(u); {
foreach (var u in activeUserUrls) validUrls.Add(u); validUrls.Add(u!);
}
foreach (var u in activeChatUrls)
{
validUrls.Add(u);
}
foreach (var u in activeUserUrls)
{
validUrls.Add(u);
}
var validFileIds = validUrls var validFileIds = validUrls
.Where(u => u.Contains("/api/files/")) .Where(u => u.Contains("/api/files/"))
@@ -84,4 +93,4 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler<CleanDryRunQuery,
OrphanedMediaBytes = safeBytes OrphanedMediaBytes = safeBytes
}); });
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -27,7 +27,10 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken) public async Task<Result<AdminUserDetailsDto>> Handle(GetUserDetailsQuery request, CancellationToken cancellationToken)
{ {
var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var targetUser = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (targetUser == null) return Result.Failure<AdminUserDetailsDto>(new Error("User.NotFound", "User not found")); if (targetUser == null)
{
return Result.Failure<AdminUserDetailsDto>(new Error("User.NotFound", "User not found"));
}
var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken); var messagesCount = await _chatsDbContext.Messages.CountAsync(m => m.SenderId == request.UserId, cancellationToken);
@@ -69,4 +72,4 @@ internal sealed class GetUserDetailsQueryHandler : IQueryHandler<GetUserDetailsQ
return Result.Success(result); return Result.Success(result);
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -47,12 +47,18 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler<AddStoryR
public async Task<Result<MessageResponse>> Handle(AddStoryReactionCommand request, CancellationToken cancellationToken) public async Task<Result<MessageResponse>> Handle(AddStoryReactionCommand request, CancellationToken cancellationToken)
{ {
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); if (story == null)
{
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found"));
}
var existing = await _context.StoryReactions var existing = await _context.StoryReactions
.FirstOrDefaultAsync(r => r.StoryId == request.StoryId && r.UserId == request.UserId && r.Emoji == request.Emoji, cancellationToken); .FirstOrDefaultAsync(r => r.StoryId == request.StoryId && r.UserId == request.UserId && r.Emoji == request.Emoji, cancellationToken);
if (existing != null) return Result.Success(new MessageResponse("Reaction already exists")); if (existing != null)
{
return Result.Success(new MessageResponse("Reaction already exists"));
}
var reaction = new StoryReaction(request.StoryId, request.UserId, request.Emoji); var reaction = new StoryReaction(request.StoryId, request.UserId, request.Emoji);
_context.StoryReactions.Add(reaction); _context.StoryReactions.Add(reaction);
@@ -103,7 +109,10 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler<AddStoryR
private string GetStoryQuote(Story story) private string GetStoryQuote(Story story)
{ {
if (!string.IsNullOrEmpty(story.Content)) return story.Content; if (!string.IsNullOrEmpty(story.Content))
{
return story.Content;
}
return story.Type.ToLower() switch return story.Type.ToLower() switch
{ {
"image" => "🖼 Фото", "image" => "🖼 Фото",
@@ -119,10 +128,13 @@ internal sealed class AddStoryReactionCommandHandler : ICommandHandler<AddStoryR
c.Type == ChatType.Personal && c.Type == ChatType.Personal &&
c.Members.Any(m => m.UserId == userId2)); c.Members.Any(m => m.UserId == userId2));
if (personalChat != null) return personalChat.Id; if (personalChat != null)
{
return personalChat.Id;
}
var command = new Knot.Modules.Chats.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { userId1, userId2 }); var command = new Knot.Modules.Chats.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { userId1, userId2 });
var result = await _sender.Send(command, cancellationToken); var result = await _sender.Send(command, cancellationToken);
return result.Value; return result.Value;
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -47,7 +47,10 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler<AddStoryRepl
public async Task<Result<MessageResponse>> Handle(AddStoryReplyCommand request, CancellationToken cancellationToken) public async Task<Result<MessageResponse>> Handle(AddStoryReplyCommand request, CancellationToken cancellationToken)
{ {
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); if (story == null)
{
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found"));
}
var reply = new StoryReply(request.StoryId, request.UserId, request.Content); var reply = new StoryReply(request.StoryId, request.UserId, request.Content);
_context.StoryReplies.Add(reply); _context.StoryReplies.Add(reply);
@@ -89,7 +92,10 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler<AddStoryRepl
private string GetStoryQuote(Story story) private string GetStoryQuote(Story story)
{ {
if (!string.IsNullOrEmpty(story.Content)) return story.Content; if (!string.IsNullOrEmpty(story.Content))
{
return story.Content;
}
return story.Type.ToLower() switch return story.Type.ToLower() switch
{ {
"image" => " Фото", "image" => " Фото",
@@ -105,10 +111,13 @@ internal sealed class AddStoryReplyCommandHandler : ICommandHandler<AddStoryRepl
c.Type == ChatType.Personal && c.Type == ChatType.Personal &&
c.Members.Any(m => m.UserId == userId2)); c.Members.Any(m => m.UserId == userId2));
if (personalChat != null) return personalChat.Id; if (personalChat != null)
{
return personalChat.Id;
}
var command = new Knot.Modules.Chats.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { userId1, userId2 }); var command = new Knot.Modules.Chats.Application.Chats.Create.CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { userId1, userId2 });
var result = await _sender.Send(command, cancellationToken); var result = await _sender.Send(command, cancellationToken);
return result.Value; return result.Value;
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediatR; using MediatR;
@@ -22,12 +22,18 @@ internal sealed class DeleteStoryCommandHandler : ICommandHandler<DeleteStoryCom
public async Task<Result<MessageResponse>> Handle(DeleteStoryCommand request, CancellationToken cancellationToken) public async Task<Result<MessageResponse>> Handle(DeleteStoryCommand request, CancellationToken cancellationToken)
{ {
var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken); var story = await _context.Stories.FindAsync(new object[] { request.StoryId }, cancellationToken);
if (story == null) return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); if (story == null)
if (story.UserId != request.UserId) return Result.Failure<MessageResponse>(new Error("Unauthorized", "Unauthorized")); {
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found"));
}
if (story.UserId != request.UserId)
{
return Result.Failure<MessageResponse>(new Error("Unauthorized", "Unauthorized"));
}
_context.Stories.Remove(story); _context.Stories.Remove(story);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
return Result.Success(new MessageResponse("Story deleted")); return Result.Success(new MessageResponse("Story deleted"));
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -26,11 +26,14 @@ internal sealed class RemoveStoryReactionCommandHandler : ICommandHandler<Remove
var reaction = await _context.StoryReactions var reaction = await _context.StoryReactions
.FirstOrDefaultAsync(r => r.StoryId == request.StoryId && r.UserId == request.UserId && r.Emoji == request.Emoji, cancellationToken); .FirstOrDefaultAsync(r => r.StoryId == request.StoryId && r.UserId == request.UserId && r.Emoji == request.Emoji, cancellationToken);
if (reaction == null) return Result.Success(new MessageResponse("Reaction not found")); if (reaction == null)
{
return Result.Success(new MessageResponse("Reaction not found"));
}
_context.StoryReactions.Remove(reaction); _context.StoryReactions.Remove(reaction);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
return Result.Success(new MessageResponse("Reaction removed")); return Result.Success(new MessageResponse("Reaction removed"));
} }
} }
@@ -36,7 +36,10 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
.Include(s => s.Viewers) .Include(s => s.Viewers)
.FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken);
if (story == null) return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found")); if (story == null)
{
return Result.Failure<MessageResponse>(new Error("Story.NotFound", "Story not found"));
}
if (story.UserId == request.UserId) if (story.UserId == request.UserId)
{ {
@@ -70,4 +73,4 @@ internal sealed class ViewStoryCommandHandler : ICommandHandler<ViewStoryCommand
return Result.Success(new MessageResponse("Story already viewed")); return Result.Success(new MessageResponse("Story already viewed"));
} }
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -55,12 +55,18 @@ internal sealed class GetStoriesQueryHandler : IQueryHandler<GetStoriesQuery, Li
foreach (var uid in userIds) foreach (var uid in userIds)
{ {
var u = await _userRepository.GetByIdAsync(uid, cancellationToken); var u = await _userRepository.GetByIdAsync(uid, cancellationToken);
if (u != null) userMap[uid] = u; if (u != null)
{
userMap[uid] = u;
}
} }
foreach (var group in groups) foreach (var group in groups)
{ {
if (!userMap.TryGetValue(group.Key, out var user)) continue; if (!userMap.TryGetValue(group.Key, out var user))
{
continue;
}
result.Add(new StoryGroupDto( result.Add(new StoryGroupDto(
new StoryUserDto(user.Id, user.Username, user.DisplayName, user.Avatar), new StoryUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
@@ -83,8 +89,11 @@ 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) return 0; if (r.User.Id == request.UserId)
{
return 0;
}
return 1; return 1;
}).ToList()); }).ToList());
} }
} }
@@ -31,14 +31,23 @@ internal sealed class GetStoryRepliesQueryHandler : IQueryHandler<GetStoryReplie
.Include(s => s.Replies) .Include(s => s.Replies)
.FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken);
if (story == null) return Result.Failure<List<StoryReplyDto>>(new Error("Story.NotFound", "Story not found")); if (story == null)
if (story.UserId != request.UserId) return Result.Failure<List<StoryReplyDto>>(new Error("Unauthorized", "Unauthorized")); {
return Result.Failure<List<StoryReplyDto>>(new Error("Story.NotFound", "Story not found"));
}
if (story.UserId != request.UserId)
{
return Result.Failure<List<StoryReplyDto>>(new Error("Unauthorized", "Unauthorized"));
}
var replies = new List<StoryReplyDto>(); var replies = new List<StoryReplyDto>();
foreach (var reply in story.Replies.OrderBy(r => r.CreatedAt)) foreach (var reply in story.Replies.OrderBy(r => r.CreatedAt))
{ {
var user = await _userRepository.GetByIdAsync(reply.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(reply.UserId, cancellationToken);
if (user == null) continue; if (user == null)
{
continue;
}
replies.Add(new StoryReplyDto( replies.Add(new StoryReplyDto(
reply.Id, reply.Id,
@@ -53,4 +62,4 @@ internal sealed class GetStoryRepliesQueryHandler : IQueryHandler<GetStoryReplie
return Result.Success(replies); return Result.Success(replies);
} }
} }
@@ -31,8 +31,14 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler<GetStoryViewer
.Include(s => s.Viewers) .Include(s => s.Viewers)
.FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == request.StoryId, cancellationToken);
if (story == null) return Result.Failure<List<StoryViewerDto>>(new Error("Story.NotFound", "Story not found")); if (story == null)
if (story.UserId != request.UserId) return Result.Failure<List<StoryViewerDto>>(new Error("Unauthorized", "Unauthorized")); {
return Result.Failure<List<StoryViewerDto>>(new Error("Story.NotFound", "Story not found"));
}
if (story.UserId != request.UserId)
{
return Result.Failure<List<StoryViewerDto>>(new Error("Unauthorized", "Unauthorized"));
}
var viewerIds = story.Viewers.Select(v => v.UserId).ToList(); var viewerIds = story.Viewers.Select(v => v.UserId).ToList();
var viewers = new List<StoryViewerDto>(); var viewers = new List<StoryViewerDto>();
@@ -40,7 +46,10 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler<GetStoryViewer
foreach (var viewerId in viewerIds) foreach (var viewerId in viewerIds)
{ {
var user = await _userRepository.GetByIdAsync(viewerId, cancellationToken); var user = await _userRepository.GetByIdAsync(viewerId, cancellationToken);
if (user == null) continue; if (user == null)
{
continue;
}
var viewerRecord = story.Viewers.First(v => v.UserId == viewerId); var viewerRecord = story.Viewers.First(v => v.UserId == viewerId);
@@ -55,4 +64,4 @@ internal sealed class GetStoryViewersQueryHandler : IQueryHandler<GetStoryViewer
return Result.Success(viewers); return Result.Success(viewers);
} }
} }
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -37,7 +37,10 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler<GetUserStoriesQ
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var user = await _userRepository.GetByIdAsync(request.TargetUserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.TargetUserId, cancellationToken);
if (user == null) return Result.Failure<StoryGroupDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<StoryGroupDto>(new Error("User.NotFound", "User not found"));
}
var result = new StoryGroupDto( var result = new StoryGroupDto(
new StoryUserDto(user.Id, user.Username, user.DisplayName, user.Avatar), new StoryUserDto(user.Id, user.Username, user.DisplayName, user.Avatar),
@@ -59,4 +62,4 @@ internal sealed class GetUserStoriesQueryHandler : IQueryHandler<GetUserStoriesQ
return Result.Success(result); return Result.Success(result);
} }
} }
@@ -81,7 +81,10 @@ public class AdminController : ControllerBase
public async Task<IActionResult> GetUserDetails(Guid id, CancellationToken ct) public async Task<IActionResult> GetUserDetails(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new GetUserDetailsQuery(id), ct); var result = await _sender.Send(new GetUserDetailsQuery(id), ct);
if (result.IsFailure) return NotFound("User not found"); if (result.IsFailure)
{
return NotFound("User not found");
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -104,4 +107,4 @@ public class AdminController : ControllerBase
var result = await _sender.Send(new CleanRunCommand(fileStorage, identityDb), ct); var result = await _sender.Send(new CleanRunCommand(fileStorage, identityDb), ct);
return Ok(result.Value); return Ok(result.Value);
} }
} }
@@ -33,7 +33,10 @@ public sealed class AuthController : ControllerBase
public async Task<IActionResult> GetMe([FromServices] IUserContext userContext, CancellationToken ct) public async Task<IActionResult> GetMe([FromServices] IUserContext userContext, CancellationToken ct)
{ {
var result = await _sender.Send(new GetMeQuery(userContext.UserId), ct); var result = await _sender.Send(new GetMeQuery(userContext.UserId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(new { User = result.Value.User }); return Ok(new { User = result.Value.User });
} }
@@ -44,4 +47,4 @@ public sealed class AuthController : ControllerBase
if (result.IsFailure) return Unauthorized(new { error = result.Error.Code ?? result.Error.Description }); if (result.IsFailure) return Unauthorized(new { error = result.Error.Code ?? result.Error.Description });
return Ok(result.Value); return Ok(result.Value);
} }
} }
@@ -41,7 +41,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> GetChats(CancellationToken ct) public async Task<IActionResult> GetChats(CancellationToken ct)
{ {
var result = await _sender.Send(new GetChatsQuery(_userContext.UserId), ct); var result = await _sender.Send(new GetChatsQuery(_userContext.UserId), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -50,7 +53,10 @@ public sealed class ChatsController : ControllerBase
{ {
var command = new CreateChatCommand(request.Name, request.Type, request.MemberIds); var command = new CreateChatCommand(request.Name, request.Type, request.MemberIds);
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -61,7 +67,10 @@ public sealed class ChatsController : ControllerBase
{ {
var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { _userContext.UserId, request.UserId }); var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { _userContext.UserId, request.UserId });
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -79,7 +88,10 @@ public sealed class ChatsController : ControllerBase
var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds); var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds);
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -89,7 +101,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> GetOrCreateFavorites(CancellationToken ct) public async Task<IActionResult> GetOrCreateFavorites(CancellationToken ct)
{ {
var result = await _sender.Send(new GetOrCreateFavoritesCommand(_userContext.UserId), ct); var result = await _sender.Send(new GetOrCreateFavoritesCommand(_userContext.UserId), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -99,7 +114,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> UpdateChat(Guid id, [FromBody] UpdateChatRequest request, CancellationToken ct) public async Task<IActionResult> UpdateChat(Guid id, [FromBody] UpdateChatRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new UpdateChatCommand(id, _userContext.UserId, request.Name, request.Description), ct); var result = await _sender.Send(new UpdateChatCommand(id, _userContext.UserId, request.Name, request.Description), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -111,7 +129,10 @@ public sealed class ChatsController : ControllerBase
var result = await _sender.Send(new LeaveOrDeleteChatCommand(id, _userContext.UserId), ct); var result = await _sender.Send(new LeaveOrDeleteChatCommand(id, _userContext.UserId), ct);
if (result.IsFailure) if (result.IsFailure)
{ {
if (result.Error.Code == "Unauthorized") return Forbid(); if (result.Error.Code == "Unauthorized")
{
return Forbid();
}
return NotFound(); return NotFound();
} }
return Ok(result.Value); return Ok(result.Value);
@@ -121,7 +142,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> ClearChat(Guid id, CancellationToken ct) public async Task<IActionResult> ClearChat(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new ClearChatCommand(id, _userContext.UserId), ct); var result = await _sender.Send(new ClearChatCommand(id, _userContext.UserId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -129,7 +153,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> TogglePin(Guid id, CancellationToken ct) public async Task<IActionResult> TogglePin(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new TogglePinCommand(id, _userContext.UserId), ct); var result = await _sender.Send(new TogglePinCommand(id, _userContext.UserId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -137,7 +164,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> AddMembers(Guid id, [FromBody] AddMembersRequest request, CancellationToken ct) public async Task<IActionResult> AddMembers(Guid id, [FromBody] AddMembersRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new AddMembersCommand(id, _userContext.UserId, request.UserIds.ToList()), ct); var result = await _sender.Send(new AddMembersCommand(id, _userContext.UserId, request.UserIds.ToList()), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -147,7 +177,10 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> RemoveMember(Guid id, Guid userId, CancellationToken ct) public async Task<IActionResult> RemoveMember(Guid id, Guid userId, CancellationToken ct)
{ {
var result = await _sender.Send(new RemoveMemberCommand(id, _userContext.UserId, userId), ct); var result = await _sender.Send(new RemoveMemberCommand(id, _userContext.UserId, userId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -156,12 +189,18 @@ public sealed class ChatsController : ControllerBase
[HttpPost("{id:guid}/avatar")] [HttpPost("{id:guid}/avatar")]
public async Task<IActionResult> UploadGroupAvatar(Guid id, Microsoft.AspNetCore.Http.IFormFile avatar, CancellationToken ct) public async Task<IActionResult> UploadGroupAvatar(Guid id, Microsoft.AspNetCore.Http.IFormFile avatar, CancellationToken ct)
{ {
if (avatar == null || avatar.Length == 0) return BadRequest("No file"); if (avatar == null || avatar.Length == 0)
{
return BadRequest("No file");
}
using var stream = avatar.OpenReadStream(); using var stream = avatar.OpenReadStream();
var result = await _sender.Send(new UploadGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream), ct); var result = await _sender.Send(new UploadGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -170,12 +209,18 @@ public sealed class ChatsController : ControllerBase
[HttpPost("{id:guid}/avatar/crop")] [HttpPost("{id:guid}/avatar/crop")]
public async Task<IActionResult> CropGroupAvatar(Guid id, [FromForm] Microsoft.AspNetCore.Http.IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) public async Task<IActionResult> CropGroupAvatar(Guid id, [FromForm] Microsoft.AspNetCore.Http.IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct)
{ {
if (avatar == null || avatar.Length == 0) return BadRequest("No file"); if (avatar == null || avatar.Length == 0)
{
return BadRequest("No file");
}
using var stream = avatar.OpenReadStream(); using var stream = avatar.OpenReadStream();
var result = await _sender.Send(new CropGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream, x, y, width, height), ct); var result = await _sender.Send(new CropGroupAvatarCommand(id, _userContext.UserId, avatar.FileName, avatar.ContentType, stream, x, y, width, height), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
@@ -185,9 +230,12 @@ public sealed class ChatsController : ControllerBase
public async Task<IActionResult> RemoveGroupAvatar(Guid id, CancellationToken ct) public async Task<IActionResult> RemoveGroupAvatar(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new RemoveGroupAvatarCommand(id, _userContext.UserId), ct); var result = await _sender.Send(new RemoveGroupAvatarCommand(id, _userContext.UserId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct); var chatResult = await _sender.Send(new GetChatByIdQuery(_userContext.UserId, result.Value), ct);
return Ok(chatResult.Value); return Ok(chatResult.Value);
} }
} }
@@ -27,7 +27,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> GetFriends(CancellationToken ct) public async Task<IActionResult> GetFriends(CancellationToken ct)
{ {
var result = await _sender.Send(new GetFriendsQuery(_userContext.UserId), ct); var result = await _sender.Send(new GetFriendsQuery(_userContext.UserId), ct);
if (result.IsFailure) return BadRequest(result.Error); if (result.IsFailure)
{
return BadRequest(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -35,7 +38,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> GetRequests(CancellationToken ct) public async Task<IActionResult> GetRequests(CancellationToken ct)
{ {
var result = await _sender.Send(new GetIncomingRequestsQuery(_userContext.UserId), ct); var result = await _sender.Send(new GetIncomingRequestsQuery(_userContext.UserId), ct);
if (result.IsFailure) return BadRequest(result.Error); if (result.IsFailure)
{
return BadRequest(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -43,7 +49,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> GetOutgoingRequests(CancellationToken ct) public async Task<IActionResult> GetOutgoingRequests(CancellationToken ct)
{ {
var result = await _sender.Send(new GetOutgoingRequestsQuery(_userContext.UserId), ct); var result = await _sender.Send(new GetOutgoingRequestsQuery(_userContext.UserId), ct);
if (result.IsFailure) return BadRequest(result.Error); if (result.IsFailure)
{
return BadRequest(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -51,7 +60,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> SendRequest([FromBody] Host.Models.SendFriendRequest request, CancellationToken ct) public async Task<IActionResult> SendRequest([FromBody] Host.Models.SendFriendRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new SendFriendRequestCommand(_userContext.UserId, request.FriendId), ct); var result = await _sender.Send(new SendFriendRequestCommand(_userContext.UserId, request.FriendId), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(new { status = "pending" }); return Ok(new { status = "pending" });
} }
@@ -59,7 +71,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> AcceptRequest(Guid id, CancellationToken ct) public async Task<IActionResult> AcceptRequest(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new AcceptFriendRequestCommand(_userContext.UserId, id), ct); var result = await _sender.Send(new AcceptFriendRequestCommand(_userContext.UserId, id), ct);
if (result.IsFailure) return NotFound(result.Error.Description); if (result.IsFailure)
{
return NotFound(result.Error.Description);
}
return Ok(new { id = result.Value }); return Ok(new { id = result.Value });
} }
@@ -67,7 +82,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> DeclineRequest(Guid id, CancellationToken ct) public async Task<IActionResult> DeclineRequest(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new DeclineFriendRequestCommand(_userContext.UserId, id), ct); var result = await _sender.Send(new DeclineFriendRequestCommand(_userContext.UserId, id), ct);
if (result.IsFailure) return NotFound(result.Error.Description); if (result.IsFailure)
{
return NotFound(result.Error.Description);
}
return Ok(new Knot.Shared.Kernel.SuccessResponse(true)); return Ok(new Knot.Shared.Kernel.SuccessResponse(true));
} }
@@ -75,7 +93,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> GetStatus(Guid userId, CancellationToken ct) public async Task<IActionResult> GetStatus(Guid userId, CancellationToken ct)
{ {
var result = await _sender.Send(new GetFriendshipStatusQuery(_userContext.UserId, userId), ct); var result = await _sender.Send(new GetFriendshipStatusQuery(_userContext.UserId, userId), ct);
if (result.IsFailure) return BadRequest(result.Error); if (result.IsFailure)
{
return BadRequest(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -83,7 +104,10 @@ public sealed class FriendsController : ControllerBase
public async Task<IActionResult> RemoveFriend(Guid id, CancellationToken ct) public async Task<IActionResult> RemoveFriend(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new RemoveFriendCommand(_userContext.UserId, id), ct); var result = await _sender.Send(new RemoveFriendCommand(_userContext.UserId, id), ct);
if (result.IsFailure) return NotFound(result.Error.Description); if (result.IsFailure)
{
return NotFound(result.Error.Description);
}
return Ok(new Knot.Shared.Kernel.SuccessResponse(true)); return Ok(new Knot.Shared.Kernel.SuccessResponse(true));
} }
} }
@@ -34,7 +34,10 @@ public sealed class MessagesController : ControllerBase
public async Task<IActionResult> GetMessages(Guid chatId, [FromQuery] string? cursor, CancellationToken ct = default) public async Task<IActionResult> GetMessages(Guid chatId, [FromQuery] string? cursor, CancellationToken ct = default)
{ {
var result = await _sender.Send(new GetMessagesQuery(_userContext.UserId, chatId, cursor), ct); var result = await _sender.Send(new GetMessagesQuery(_userContext.UserId, chatId, cursor), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -42,21 +45,30 @@ public sealed class MessagesController : ControllerBase
public async Task<IActionResult> GetSearch([FromQuery] string q, [FromQuery] Guid? chatId, CancellationToken ct) public async Task<IActionResult> GetSearch([FromQuery] string q, [FromQuery] Guid? chatId, CancellationToken ct)
{ {
var result = await _sender.Send(new SearchMessagesQuery(_userContext.UserId, q, chatId), ct); var result = await _sender.Send(new SearchMessagesQuery(_userContext.UserId, q, chatId), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(result.Value); return Ok(result.Value);
} }
[HttpPost("upload")] [HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file, CancellationToken ct) public async Task<IActionResult> UploadFile(IFormFile file, CancellationToken ct)
{ {
if (file == null || file.Length == 0) return BadRequest("No file uploaded"); if (file == null || file.Length == 0)
{
return BadRequest("No file uploaded");
}
using var stream = file.OpenReadStream(); using var stream = file.OpenReadStream();
var result = await _sender.Send(new UploadFileCommand(file.FileName, file.ContentType, file.Length, stream), ct); var result = await _sender.Send(new UploadFileCommand(file.FileName, file.ContentType, file.Length, stream), ct);
if (result.IsFailure) if (result.IsFailure)
{ {
if (result.Error.Code == "File.TooLarge") return StatusCode(413, result.Error.Description); if (result.Error.Code == "File.TooLarge")
{
return StatusCode(413, result.Error.Description);
}
return BadRequest(result.Error.Description); return BadRequest(result.Error.Description);
} }
@@ -67,7 +79,10 @@ public sealed class MessagesController : ControllerBase
public async Task<IActionResult> GetSharedMedia(Guid chatId, [FromQuery] string? type, CancellationToken ct) public async Task<IActionResult> GetSharedMedia(Guid chatId, [FromQuery] string? type, CancellationToken ct)
{ {
var result = await _sender.Send(new GetSharedMediaQuery(_userContext.UserId, chatId, type), ct); var result = await _sender.Send(new GetSharedMediaQuery(_userContext.UserId, chatId, type), ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -89,8 +104,11 @@ public sealed class MessagesController : ControllerBase
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error.Description); if (result.IsFailure)
{
return BadRequest(result.Error.Description);
}
return Ok(result.Value); return Ok(result.Value);
} }
} }
@@ -51,7 +51,10 @@ public sealed class StoriesController : ControllerBase
public async Task<IActionResult> GetUserStories(Guid userId, CancellationToken ct) public async Task<IActionResult> GetUserStories(Guid userId, CancellationToken ct)
{ {
var result = await _sender.Send(new GetUserStoriesQuery(_userContext.UserId, userId), ct); var result = await _sender.Send(new GetUserStoriesQuery(_userContext.UserId, userId), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -59,7 +62,10 @@ public sealed class StoriesController : ControllerBase
public async Task<IActionResult> ViewStory(Guid id, CancellationToken ct) public async Task<IActionResult> ViewStory(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new ViewStoryCommand(_userContext.UserId, id), ct); var result = await _sender.Send(new ViewStoryCommand(_userContext.UserId, id), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -69,7 +75,10 @@ public sealed class StoriesController : ControllerBase
var result = await _sender.Send(new GetStoryViewersQuery(_userContext.UserId, id), ct); var result = await _sender.Send(new GetStoryViewersQuery(_userContext.UserId, id), ct);
if (result.IsFailure) if (result.IsFailure)
{ {
if (result.Error.Code == "Unauthorized") return Forbid(); if (result.Error.Code == "Unauthorized")
{
return Forbid();
}
return NotFound(); return NotFound();
} }
return Ok(result.Value); return Ok(result.Value);
@@ -79,7 +88,10 @@ public sealed class StoriesController : ControllerBase
public async Task<IActionResult> AddReaction(Guid id, [FromBody] AddStoryReactionRequest request, CancellationToken ct) public async Task<IActionResult> AddReaction(Guid id, [FromBody] AddStoryReactionRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new AddStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct); var result = await _sender.Send(new AddStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -87,7 +99,10 @@ public sealed class StoriesController : ControllerBase
public async Task<IActionResult> RemoveReaction(Guid id, [FromBody] RemoveStoryReactionRequest request, CancellationToken ct) public async Task<IActionResult> RemoveReaction(Guid id, [FromBody] RemoveStoryReactionRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new RemoveStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct); var result = await _sender.Send(new RemoveStoryReactionCommand(_userContext.UserId, id, request.Emoji), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -95,7 +110,10 @@ public sealed class StoriesController : ControllerBase
public async Task<IActionResult> AddReply(Guid id, [FromBody] AddStoryReplyRequest request, CancellationToken ct) public async Task<IActionResult> AddReply(Guid id, [FromBody] AddStoryReplyRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new AddStoryReplyCommand(_userContext.UserId, id, request.Content), ct); var result = await _sender.Send(new AddStoryReplyCommand(_userContext.UserId, id, request.Content), ct);
if (result.IsFailure) return NotFound(); if (result.IsFailure)
{
return NotFound();
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -105,7 +123,10 @@ public sealed class StoriesController : ControllerBase
var result = await _sender.Send(new GetStoryRepliesQuery(_userContext.UserId, id), ct); var result = await _sender.Send(new GetStoryRepliesQuery(_userContext.UserId, id), ct);
if (result.IsFailure) if (result.IsFailure)
{ {
if (result.Error.Code == "Unauthorized") return Forbid(); if (result.Error.Code == "Unauthorized")
{
return Forbid();
}
return NotFound(); return NotFound();
} }
return Ok(result.Value); return Ok(result.Value);
@@ -117,9 +138,12 @@ public sealed class StoriesController : ControllerBase
var result = await _sender.Send(new DeleteStoryCommand(_userContext.UserId, id), ct); var result = await _sender.Send(new DeleteStoryCommand(_userContext.UserId, id), ct);
if (result.IsFailure) if (result.IsFailure)
{ {
if (result.Error.Code == "Unauthorized") return Forbid(); if (result.Error.Code == "Unauthorized")
{
return Forbid();
}
return NotFound(); return NotFound();
} }
return Ok(result.Value); return Ok(result.Value);
} }
} }
@@ -31,7 +31,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> Search([FromQuery] string q, CancellationToken ct) public async Task<IActionResult> Search([FromQuery] string q, CancellationToken ct)
{ {
var result = await _sender.Send(new SearchUsersQuery(q), ct); var result = await _sender.Send(new SearchUsersQuery(q), ct);
if (result.IsFailure) return BadRequest(result.Error); if (result.IsFailure)
{
return BadRequest(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -39,7 +42,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> UpdateSettings([FromBody] UpdateSettingsRequest request, CancellationToken ct) public async Task<IActionResult> UpdateSettings([FromBody] UpdateSettingsRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new UpdateSettingsCommand(_userContext.UserId, request.HideStoryViews), ct); var result = await _sender.Send(new UpdateSettingsCommand(_userContext.UserId, request.HideStoryViews), ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -47,7 +53,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> UploadAvatar(IFormFile avatar, CancellationToken ct) public async Task<IActionResult> UploadAvatar(IFormFile avatar, CancellationToken ct)
{ {
var fileToUpload = avatar ?? Request.Form.Files.FirstOrDefault(); var fileToUpload = avatar ?? Request.Form.Files.FirstOrDefault();
if (fileToUpload == null || fileToUpload.Length == 0) return BadRequest("No file uploaded"); if (fileToUpload == null || fileToUpload.Length == 0)
{
return BadRequest("No file uploaded");
}
using var stream = fileToUpload.OpenReadStream(); using var stream = fileToUpload.OpenReadStream();
var command = new UploadAvatarCommand( var command = new UploadAvatarCommand(
@@ -58,14 +67,20 @@ public sealed class UsersController : ControllerBase
); );
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
[HttpPost("avatar/crop")] [HttpPost("avatar/crop")]
public async Task<IActionResult> CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) public async Task<IActionResult> CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct)
{ {
if (avatar == null || avatar.Length == 0) return BadRequest("No file uploaded"); if (avatar == null || avatar.Length == 0)
{
return BadRequest("No file uploaded");
}
using var stream = avatar.OpenReadStream(); using var stream = avatar.OpenReadStream();
var command = new CropAvatarCommand( var command = new CropAvatarCommand(
@@ -77,7 +92,10 @@ public sealed class UsersController : ControllerBase
); );
var result = await _sender.Send(command, ct); var result = await _sender.Send(command, ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -85,7 +103,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> DeleteAvatar(CancellationToken ct) public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
{ {
var result = await _sender.Send(new DeleteAvatarCommand(_userContext.UserId), ct); var result = await _sender.Send(new DeleteAvatarCommand(_userContext.UserId), ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -93,7 +114,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request, CancellationToken ct) public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request, CancellationToken ct)
{ {
var result = await _sender.Send(new UpdateProfileCommand(_userContext.UserId, request.DisplayName, request.Bio, request.Birthday), ct); var result = await _sender.Send(new UpdateProfileCommand(_userContext.UserId, request.DisplayName, request.Bio, request.Birthday), ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
@@ -101,7 +125,10 @@ public sealed class UsersController : ControllerBase
public async Task<IActionResult> GetUser(Guid id, CancellationToken ct) public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
{ {
var result = await _sender.Send(new GetUserQuery(id), ct); var result = await _sender.Send(new GetUserQuery(id), ct);
if (result.IsFailure) return NotFound(result.Error); if (result.IsFailure)
{
return NotFound(result.Error);
}
return Ok(result.Value); return Ok(result.Value);
} }
} }
@@ -21,12 +21,21 @@ public sealed class FederationEndpoints : ICarterModule
group.MapPost("/handshake", async ([FromBody] Host.Application.Federation.Commands.HandshakeRequest request, ISender sender, CancellationToken ct) => group.MapPost("/handshake", async ([FromBody] Host.Application.Federation.Commands.HandshakeRequest request, ISender sender, CancellationToken ct) =>
{ {
var result = await sender.Send(new Host.Application.Federation.Commands.HandshakeFederationCommand(request), ct); var result = await sender.Send(new Host.Application.Federation.Commands.HandshakeFederationCommand(request), ct);
if (result.IsSuccess) return Results.Ok(result.Value); if (result.IsSuccess)
{
return Results.Ok(result.Value);
}
if (result.Error.Code == "Unauthorized") return Results.Forbid(); if (result.Error.Code == "Unauthorized")
if (result.Error.Code == Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin) return Results.StatusCode(503); {
return Results.Forbid();
}
if (result.Error.Code == Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin)
{
return Results.StatusCode(503);
}
return Results.BadRequest(new { error = result.Error.Description }); return Results.BadRequest(new { error = result.Error.Description });
}); });
} }
} }
@@ -32,7 +32,9 @@ internal sealed class UploadGroupAvatarCommandHandler : ICommandHandler<UploadGr
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
var url = $"/api/files/{fileId}"; var url = $"/api/files/{fileId}";
@@ -64,7 +66,9 @@ internal sealed class CropGroupAvatarCommandHandler : ICommandHandler<CropGroupA
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
string url; string url;
@@ -112,7 +116,9 @@ internal sealed class RemoveGroupAvatarCommandHandler : ICommandHandler<RemoveGr
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
chat.UpdateAvatar(null); chat.UpdateAvatar(null);
_chatRepository.Update(chat); _chatRepository.Update(chat);
@@ -24,7 +24,9 @@ internal sealed class ClearChatCommandHandler : ICommandHandler<ClearChatCommand
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<MessageResponse>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<MessageResponse>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
// Currently a placeholder // Currently a placeholder
return Result.Success(new MessageResponse("Cleared")); return Result.Success(new MessageResponse("Cleared"));
@@ -29,7 +29,10 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
public async Task<Result<ChatDto?>> Handle(GetChatByIdQuery request, CancellationToken cancellationToken) public async Task<Result<ChatDto?>> Handle(GetChatByIdQuery request, CancellationToken cancellationToken)
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null) return Result.Success<ChatDto?>(null); if (chat == null)
{
return Result.Success<ChatDto?>(null);
}
if (!chat.Members.Any(m => m.UserId == request.UserId)) if (!chat.Members.Any(m => m.UserId == request.UserId))
{ {
@@ -37,14 +40,20 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
} }
var userIdsToFetch = new HashSet<Guid>(); var userIdsToFetch = new HashSet<Guid>();
foreach (var m in chat.Members) userIdsToFetch.Add(m.UserId); foreach (var m in chat.Members)
{
userIdsToFetch.Add(m.UserId);
}
var chatMessages = await _messageRepository.GetChatMessagesAsync(chat.Id, 1, 0, cancellationToken); var chatMessages = await _messageRepository.GetChatMessagesAsync(chat.Id, 1, 0, cancellationToken);
var mFirst = chatMessages.FirstOrDefault(); var mFirst = chatMessages.FirstOrDefault();
if (mFirst != null) if (mFirst != null)
{ {
userIdsToFetch.Add(mFirst.SenderId); userIdsToFetch.Add(mFirst.SenderId);
foreach (var r in mFirst.Reactions) userIdsToFetch.Add(r.UserId); foreach (var r in mFirst.Reactions)
{
userIdsToFetch.Add(r.UserId);
}
} }
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
@@ -36,14 +36,20 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
foreach (var c in userChats) foreach (var c in userChats)
{ {
var userIdsToFetch = new HashSet<Guid>(); var userIdsToFetch = new HashSet<Guid>();
foreach (var m in c.Members) userIdsToFetch.Add(m.UserId); foreach (var m in c.Members)
{
userIdsToFetch.Add(m.UserId);
}
var chatMessages = await _messageRepository.GetChatMessagesAsync(c.Id, 1, 0, cancellationToken); var chatMessages = await _messageRepository.GetChatMessagesAsync(c.Id, 1, 0, cancellationToken);
var mFirst = chatMessages.FirstOrDefault(); var mFirst = chatMessages.FirstOrDefault();
if (mFirst != null) if (mFirst != null)
{ {
userIdsToFetch.Add(mFirst.SenderId); userIdsToFetch.Add(mFirst.SenderId);
foreach (var r in mFirst.Reactions) userIdsToFetch.Add(r.UserId); foreach (var r in mFirst.Reactions)
{
userIdsToFetch.Add(r.UserId);
}
} }
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
@@ -25,10 +25,15 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
public async Task<Result<SuccessResponse>> Handle(LeaveOrDeleteChatCommand request, CancellationToken cancellationToken) public async Task<Result<SuccessResponse>> Handle(LeaveOrDeleteChatCommand request, CancellationToken cancellationToken)
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null) return Result.Success(new SuccessResponse(true)); if (chat == null)
{
return Result.Success(new SuccessResponse(true));
}
if (!chat.Members.Any(m => m.UserId == request.UserId)) if (!chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<SuccessResponse>(new Error("Unauthorized", "Access denied")); return Result.Failure<SuccessResponse>(new Error("Unauthorized", "Access denied"));
}
if (chat.Type == ChatType.Group) if (chat.Type == ChatType.Group)
{ {
@@ -39,7 +44,7 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler<LeaveOrD
{ {
_chatRepository.Remove(chat); _chatRepository.Remove(chat);
} }
await _uow.SaveChangesAsync(cancellationToken); await _uow.SaveChangesAsync(cancellationToken);
return Result.Success(new SuccessResponse(true)); return Result.Success(new SuccessResponse(true));
@@ -27,7 +27,9 @@ internal sealed class AddMembersCommandHandler : ICommandHandler<AddMembersComma
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
foreach (var userId in request.UserIdsToAdd) foreach (var userId in request.UserIdsToAdd)
{ {
@@ -57,7 +59,9 @@ internal sealed class RemoveMemberCommandHandler : ICommandHandler<RemoveMemberC
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
chat.RemoveMember(request.UserIdToRemove); chat.RemoveMember(request.UserIdToRemove);
_chatRepository.Update(chat); _chatRepository.Update(chat);
@@ -27,11 +27,15 @@ internal sealed class TogglePinCommandHandler : ICommandHandler<TogglePinCommand
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null) if (chat == null)
{
return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Chat not found")); return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Chat not found"));
}
var member = chat.Members.FirstOrDefault(m => m.UserId == request.UserId); var member = chat.Members.FirstOrDefault(m => m.UserId == request.UserId);
if (member == null) if (member == null)
{
return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Member not found")); return Result.Failure<TogglePinResponse>(new Error("Chat.NotFound", "Member not found"));
}
member.TogglePin(); member.TogglePin();
_chatRepository.Update(chat); _chatRepository.Update(chat);
@@ -25,12 +25,21 @@ internal sealed class UpdateChatCommandHandler : ICommandHandler<UpdateChatComma
public async Task<Result<Guid>> Handle(UpdateChatCommand request, CancellationToken cancellationToken) public async Task<Result<Guid>> Handle(UpdateChatCommand request, CancellationToken cancellationToken)
{ {
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId))
{
return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied")); return Result.Failure<Guid>(new Error("Chat.NotFound", "Chat not found or access denied"));
}
if (request.Name != null)
{
chat.UpdateName(request.Name);
}
if (request.Description != null)
{
chat.UpdateDescription(request.Description);
}
if (request.Name != null) chat.UpdateName(request.Name);
if (request.Description != null) chat.UpdateDescription(request.Description);
_chatRepository.Update(chat); _chatRepository.Update(chat);
await _uow.SaveChangesAsync(cancellationToken); await _uow.SaveChangesAsync(cancellationToken);
@@ -4,8 +4,8 @@ using System.Collections.Generic;
namespace Knot.Modules.Chats.Application.DTOs; namespace Knot.Modules.Chats.Application.DTOs;
public sealed record SendMessageRequest( public sealed record SendMessageRequest(
string? Content, string? Content,
string Type, string Type,
List<AttachmentDto>? Attachments = null, List<AttachmentDto>? Attachments = null,
Guid? ReplyToId = null, Guid? ReplyToId = null,
string? Quote = null, string? Quote = null,
@@ -20,7 +20,7 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
private readonly IHubContext<ChatHub> _hubContext; private readonly IHubContext<ChatHub> _hubContext;
public DeleteMessagesCommandHandler( public DeleteMessagesCommandHandler(
IMessageRepository messageRepository, IMessageRepository messageRepository,
IChatsUnitOfWork unitOfWork, IChatsUnitOfWork unitOfWork,
IHubContext<ChatHub> hubContext) IHubContext<ChatHub> hubContext)
{ {
@@ -34,7 +34,10 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
foreach (var id in request.MessageIds) foreach (var id in request.MessageIds)
{ {
var message = await _messageRepository.GetByIdAsync(id, cancellationToken); var message = await _messageRepository.GetByIdAsync(id, cancellationToken);
if (message is null || message.ChatId != request.ChatId) continue; if (message is null || message.ChatId != request.ChatId)
{
continue;
}
if (request.DeleteForAll) if (request.DeleteForAll)
{ {
@@ -49,13 +49,21 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
foreach (var m in messages) foreach (var m in messages)
{ {
if (m.DeletedByUsers.Contains(request.UserId)) continue; if (m.DeletedByUsers.Contains(request.UserId))
{
continue;
}
userIdsToFetch.Add(m.SenderId); userIdsToFetch.Add(m.SenderId);
if (m.ForwardedFromId.HasValue) userIdsToFetch.Add(m.ForwardedFromId.Value); if (m.ForwardedFromId.HasValue)
{
userIdsToFetch.Add(m.ForwardedFromId.Value);
}
foreach (var r in m.Reactions) foreach (var r in m.Reactions)
{
userIdsToFetch.Add(r.UserId); userIdsToFetch.Add(r.UserId);
}
if (m.ReplyToId.HasValue) if (m.ReplyToId.HasValue)
{ {
@@ -72,15 +80,18 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
foreach (var m in messages) foreach (var m in messages)
{ {
if (m.DeletedByUsers.Contains(request.UserId)) continue; if (m.DeletedByUsers.Contains(request.UserId))
{
continue;
}
ReplyToMessageDto? replyToObj = null; ReplyToMessageDto? replyToObj = null;
if (m.ReplyToId.HasValue && replyMessages.TryGetValue(m.ReplyToId.Value, out var replyMsg)) if (m.ReplyToId.HasValue && replyMessages.TryGetValue(m.ReplyToId.Value, out var replyMsg))
{ {
var senderObj = senders.TryGetValue(replyMsg.SenderId, out var rs) var senderObj = senders.TryGetValue(replyMsg.SenderId, out var rs)
? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar) ? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar)
: null; : null;
replyToObj = new ReplyToMessageDto( replyToObj = new ReplyToMessageDto(
replyMsg.Id, replyMsg.Id,
replyMsg.Content, replyMsg.Content,
@@ -96,7 +107,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
var userObj = senders.TryGetValue(r.UserId, out var ru) var userObj = senders.TryGetValue(r.UserId, out var ru)
? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar) ? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar)
: new MessageSenderDto(r.UserId, "unknown", "Unknown", null); : new MessageSenderDto(r.UserId, "unknown", "Unknown", null);
reactionsWithUser.Add(new MessageReactionDto( reactionsWithUser.Add(new MessageReactionDto(
r.Id, r.Id,
r.Emoji, r.Emoji,
@@ -128,7 +139,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
reactionsWithUser reactionsWithUser
)); ));
} }
return Result.Success(result); return Result.Success(result);
} }
} }
@@ -69,15 +69,31 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
continue; continue;
} }
if (m.Media == null || !m.Media.Any()) continue; if (m.Media == null || !m.Media.Any())
{
continue;
}
var filteredMedia = m.Media.Where(media => { var filteredMedia = m.Media.Where(media =>
{
var mediaType = media.Type?.ToLower() ?? "file"; var mediaType = media.Type?.ToLower() ?? "file";
var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)); var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
if (filterType == "media") return (mediaType == "image" || mediaType == "video") && !isGif; if (filterType == "media")
if (filterType == "gifs") return isGif; {
if (filterType == "files") return mediaType != "image" && mediaType != "video" && mediaType != "link"; return (mediaType == "image" || mediaType == "video") && !isGif;
}
if (filterType == "gifs")
{
return isGif;
}
if (filterType == "files")
{
return mediaType != "image" && mediaType != "video" && mediaType != "link";
}
return true; return true;
}).ToList(); }).ToList();
@@ -26,7 +26,7 @@ public sealed class ReadMessagesCommandHandler : ICommandHandler<ReadMessagesCom
} }
await _messageRepository.AddReadReceiptsAsync(request.UserId, request.MessageIds, cancellationToken); await _messageRepository.AddReadReceiptsAsync(request.UserId, request.MessageIds, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(); return Result.Success();
@@ -29,8 +29,8 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
private readonly IChatsUnitOfWork _unitOfWork; private readonly IChatsUnitOfWork _unitOfWork;
public SendMessageCommandHandler( public SendMessageCommandHandler(
IChatRepository chatRepository, IChatRepository chatRepository,
IMessageRepository messageRepository, IMessageRepository messageRepository,
IChatsUnitOfWork unitOfWork) IChatsUnitOfWork unitOfWork)
{ {
_chatRepository = chatRepository; _chatRepository = chatRepository;
@@ -25,14 +25,19 @@ internal sealed class UploadFileCommandHandler : ICommandHandler<UploadFileComma
public async Task<Result<UploadFileResponseDto>> Handle(UploadFileCommand request, CancellationToken cancellationToken) public async Task<Result<UploadFileResponseDto>> Handle(UploadFileCommand request, CancellationToken cancellationToken)
{ {
if (request.Length == 0) return Result.Failure<UploadFileResponseDto>(new Error("File.Empty", "No file uploaded")); if (request.Length == 0)
{
return Result.Failure<UploadFileResponseDto>(new Error("File.Empty", "No file uploaded"));
}
var maxMb = _settingsService.Current.MaxFileSizeMb; var maxMb = _settingsService.Current.MaxFileSizeMb;
if (request.Length > maxMb * 1024 * 1024) if (request.Length > maxMb * 1024 * 1024)
{
return Result.Failure<UploadFileResponseDto>(new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB.")); return Result.Failure<UploadFileResponseDto>(new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB."));
}
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
return Result.Success(new UploadFileResponseDto("/api/files/" + fileId, request.FileName, request.Length)); return Result.Success(new UploadFileResponseDto("/api/files/" + fileId, request.FileName, request.Length));
} }
} }
@@ -27,10 +27,14 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
public async Task<Result<AnalyzeImportResponseDto>> Handle(AnalyzeImportCommand request, CancellationToken cancellationToken) public async Task<Result<AnalyzeImportResponseDto>> Handle(AnalyzeImportCommand request, CancellationToken cancellationToken)
{ {
if (request.FileStream == null || request.FileStream.Length == 0) if (request.FileStream == null || request.FileStream.Length == 0)
{
return Result.Failure<AnalyzeImportResponseDto>(new Error("File.Empty", "No file uploaded")); return Result.Failure<AnalyzeImportResponseDto>(new Error("File.Empty", "No file uploaded"));
}
if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (!request.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
return Result.Failure<AnalyzeImportResponseDto>(new Error("File.InvalidExtension", "Must be a ZIP archive")); return Result.Failure<AnalyzeImportResponseDto>(new Error("File.InvalidExtension", "Must be a ZIP archive"));
}
var token = Guid.NewGuid(); var token = Guid.NewGuid();
var tempPath = Path.Combine(Path.GetTempPath(), $"{token}.zip"); var tempPath = Path.Combine(Path.GetTempPath(), $"{token}.zip");
@@ -54,7 +58,10 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
var doc = parser.ParseDocument(stream); var doc = parser.ParseDocument(stream);
var messageNodes = doc.QuerySelectorAll(".message"); var messageNodes = doc.QuerySelectorAll(".message");
if (messageNodes == null) continue; if (messageNodes == null)
{
continue;
}
foreach (var node in messageNodes) foreach (var node in messageNodes)
{ {
@@ -63,8 +70,11 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler<AnalyzeImpor
{ {
var nameNodeText = (IElement)fromNameNode.Clone(); var nameNodeText = (IElement)fromNameNode.Clone();
var innerSpans = nameNodeText.QuerySelectorAll("span"); var innerSpans = nameNodeText.QuerySelectorAll("span");
foreach (var span in innerSpans) span.Remove(); foreach (var span in innerSpans)
{
span.Remove();
}
var name = nameNodeText.TextContent.Trim(); var name = nameNodeText.TextContent.Trim();
if (!string.IsNullOrWhiteSpace(name)) if (!string.IsNullOrWhiteSpace(name))
{ {
@@ -55,14 +55,21 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
public async Task<Result<ExecuteImportResponseDto>> Handle(ExecuteImportCommand request, CancellationToken cancellationToken) public async Task<Result<ExecuteImportResponseDto>> Handle(ExecuteImportCommand request, CancellationToken cancellationToken)
{ {
if (!TelegramImportState.TempZips.TryGetValue(request.Token, out var tempPath)) if (!TelegramImportState.TempZips.TryGetValue(request.Token, out var tempPath))
{
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Expired", "Session not found or expired")); return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Expired", "Session not found or expired"));
}
if (!System.IO.File.Exists(tempPath)) if (!System.IO.File.Exists(tempPath))
{
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Missing", "ZIP file lost")); return Result.Failure<ExecuteImportResponseDto>(new Error("Import.Missing", "ZIP file lost"));
}
var myId = request.CurrentUserId; var myId = request.CurrentUserId;
var targetUserIds = request.Mapping.Values.Distinct().Where(id => id != Guid.Empty).ToList(); var targetUserIds = request.Mapping.Values.Distinct().Where(id => id != Guid.Empty).ToList();
if (!targetUserIds.Contains(myId)) targetUserIds.Add(myId); if (!targetUserIds.Contains(myId))
{
targetUserIds.Add(myId);
}
Guid chatId = Guid.Empty; Guid chatId = Guid.Empty;
var chatMembers = targetUserIds; var chatMembers = targetUserIds;
@@ -71,7 +78,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
var existingChats = await _chatRepository.GetUserChatsAsync(myId, cancellationToken); var existingChats = await _chatRepository.GetUserChatsAsync(myId, cancellationToken);
var personalChat = existingChats.FirstOrDefault(c => c.Type == ChatType.Personal && c.Members.All(m => chatMembers.Contains(m.UserId)) && c.Members.Count == chatMembers.Count); var personalChat = existingChats.FirstOrDefault(c => c.Type == ChatType.Personal && c.Members.All(m => chatMembers.Contains(m.UserId)) && c.Members.Count == chatMembers.Count);
if (personalChat != null) if (personalChat != null)
{ {
chatId = personalChat.Id; chatId = personalChat.Id;
@@ -79,10 +86,18 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
else else
{ {
var friendId = chatMembers.FirstOrDefault(id => id != myId); var friendId = chatMembers.FirstOrDefault(id => id != myId);
if (friendId == Guid.Empty) friendId = myId; if (friendId == Guid.Empty)
{
friendId = myId;
}
var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { myId, friendId }); var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { myId, friendId });
var res = await _sender.Send(command, cancellationToken); var res = await _sender.Send(command, cancellationToken);
if (res.IsFailure) return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code)); if (res.IsFailure)
{
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code));
}
chatId = res.Value; chatId = res.Value;
} }
} }
@@ -90,7 +105,11 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
var command = new CreateChatCommand(request.GroupName ?? "Импортированный чат", ChatType.Group, chatMembers); var command = new CreateChatCommand(request.GroupName ?? "Импортированный чат", ChatType.Group, chatMembers);
var res = await _sender.Send(command, cancellationToken); var res = await _sender.Send(command, cancellationToken);
if (res.IsFailure) return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code)); if (res.IsFailure)
{
return Result.Failure<ExecuteImportResponseDto>(new Error("Import.CreateChatFailed", res.Error.Description ?? res.Error.Code));
}
chatId = res.Value; chatId = res.Value;
} }
@@ -100,7 +119,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
var htmlEntries = archive.Entries var htmlEntries = archive.Entries
.Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase)) .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase))
.OrderBy(e => .OrderBy(e =>
{ {
var name = e.Name.ToLower().Replace("messages", "").Replace(".html", ""); var name = e.Name.ToLower().Replace("messages", "").Replace(".html", "");
return string.IsNullOrEmpty(name) ? 0 : int.TryParse(name, out var num) ? num : 999999; return string.IsNullOrEmpty(name) ? 0 : int.TryParse(name, out var num) ? num : 999999;
@@ -119,10 +138,16 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var doc = parser.ParseDocument(stream); var doc = parser.ParseDocument(stream);
var messageNodes = doc.QuerySelectorAll(".message"); var messageNodes = doc.QuerySelectorAll(".message");
if (messageNodes == null) continue; if (messageNodes == null)
{
continue;
}
var baseDir = Path.GetDirectoryName(entry.FullName)?.Replace("\\", "/") ?? ""; var baseDir = Path.GetDirectoryName(entry.FullName)?.Replace("\\", "/") ?? "";
if (!string.IsNullOrEmpty(baseDir) && !baseDir.EndsWith("/")) baseDir += "/"; if (!string.IsNullOrEmpty(baseDir) && !baseDir.EndsWith("/"))
{
baseDir += "/";
}
foreach (var node in messageNodes) foreach (var node in messageNodes)
{ {
@@ -130,8 +155,8 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
var fromNameNode = node.QuerySelector(".from_name"); var fromNameNode = node.QuerySelector(".from_name");
var textNode = node.QuerySelector(".text"); var textNode = node.QuerySelector(".text");
var dateNode = node.QuerySelector(".date[title]") var dateNode = node.QuerySelector(".date[title]")
?? node.QuerySelector(".pull_right[title]") ?? node.QuerySelector(".pull_right[title]")
?? node.QuerySelector("[title]"); ?? node.QuerySelector("[title]");
@@ -139,13 +164,20 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
var nameNodeText = (AngleSharp.Dom.IElement)fromNameNode.Clone(); var nameNodeText = (AngleSharp.Dom.IElement)fromNameNode.Clone();
var innerSpans = nameNodeText.QuerySelectorAll("span"); var innerSpans = nameNodeText.QuerySelectorAll("span");
foreach (var span in innerSpans) span.Remove(); foreach (var span in innerSpans)
{
span.Remove();
}
var name = nameNodeText.TextContent.Trim(); var name = nameNodeText.TextContent.Trim();
if (request.Mapping.TryGetValue(name, out var mappedId) && mappedId != Guid.Empty) if (request.Mapping.TryGetValue(name, out var mappedId) && mappedId != Guid.Empty)
{
lastSenderGuid = mappedId; lastSenderGuid = mappedId;
}
else else
{
lastSenderGuid = myId; lastSenderGuid = myId;
}
} }
Guid senderGuid = lastSenderGuid; Guid senderGuid = lastSenderGuid;
@@ -153,8 +185,8 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
string content = ""; string content = "";
var mainBodyNode = node.QuerySelector(".body"); var mainBodyNode = node.QuerySelector(".body");
var isForwarded = node.QuerySelector(".forwarded") != null; var isForwarded = node.QuerySelector(".forwarded") != null;
var contentTextNode = isForwarded var contentTextNode = isForwarded
? (node.QuerySelector(".body > .text") ?? node.QuerySelector(".text:not(.forwarded .text)")) ? (node.QuerySelector(".body > .text") ?? node.QuerySelector(".text:not(.forwarded .text)"))
: node.QuerySelector(".text"); : node.QuerySelector(".text");
@@ -168,7 +200,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var tempDoc = tempParser.ParseDocument("<div>" + html + "</div>"); var tempDoc = tempParser.ParseDocument("<div>" + html + "</div>");
content = tempDoc.Body?.TextContent.Trim() ?? ""; content = tempDoc.Body?.TextContent.Trim() ?? "";
} }
DateTime createdAt = lastCreatedAt; DateTime createdAt = lastCreatedAt;
var titleNodes = node.QuerySelectorAll("[title]"); var titleNodes = node.QuerySelectorAll("[title]");
bool parsed = false; bool parsed = false;
@@ -178,11 +210,11 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
foreach (var tnode in titleNodes) foreach (var tnode in titleNodes)
{ {
var dateStr = tnode.GetAttribute("title")?.Trim() ?? ""; var dateStr = tnode.GetAttribute("title")?.Trim() ?? "";
if (dateStr.Length >= 10 && char.IsDigit(dateStr[0]) && char.IsDigit(dateStr[1])) if (dateStr.Length >= 10 && char.IsDigit(dateStr[0]) && char.IsDigit(dateStr[1]))
{ {
var cleanStr = dateStr.Replace("UTC", "", StringComparison.OrdinalIgnoreCase).Trim(); var cleanStr = dateStr.Replace("UTC", "", StringComparison.OrdinalIgnoreCase).Trim();
if (DateTimeOffset.TryParseExact(cleanStr, "dd.MM.yyyy HH:mm:ss zzz", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto)) if (DateTimeOffset.TryParseExact(cleanStr, "dd.MM.yyyy HH:mm:ss zzz", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto))
{ {
createdAt = dto.UtcDateTime; createdAt = dto.UtcDateTime;
@@ -223,10 +255,13 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
if (fwdNameText != null) if (fwdNameText != null)
{ {
var innerSpans = fwdNameText.QuerySelectorAll("span"); var innerSpans = fwdNameText.QuerySelectorAll("span");
foreach (var s in innerSpans) s.Remove(); foreach (var s in innerSpans)
{
s.Remove();
}
} }
var fwdName = fwdNameText != null ? fwdNameText.TextContent.Trim() : "Неизвестного"; var fwdName = fwdNameText != null ? fwdNameText.TextContent.Trim() : "Неизвестного";
if (request.Mapping.TryGetValue(fwdName, out var mappedFwdId) && mappedFwdId != Guid.Empty) if (request.Mapping.TryGetValue(fwdName, out var mappedFwdId) && mappedFwdId != Guid.Empty)
{ {
forwardedFromId = mappedFwdId; forwardedFromId = mappedFwdId;
@@ -235,7 +270,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
forwardedFromId = myId; forwardedFromId = myId;
} }
var fwdTextNode = forwardedNode.QuerySelector(".text"); var fwdTextNode = forwardedNode.QuerySelector(".text");
string fwdContent = ""; string fwdContent = "";
if (fwdTextNode != null) if (fwdTextNode != null)
@@ -248,11 +283,11 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var tempDoc = tempParser.ParseDocument("<div>" + fHtml + "</div>"); var tempDoc = tempParser.ParseDocument("<div>" + fHtml + "</div>");
fwdContent = tempDoc.Body?.TextContent.Trim() ?? ""; fwdContent = tempDoc.Body?.TextContent.Trim() ?? "";
} }
if (forwardedFromId == null) if (forwardedFromId == null)
{ {
content = string.IsNullOrEmpty(content) content = string.IsNullOrEmpty(content)
? $"[Переслано от {fwdName}]:\n{fwdContent}" ? $"[Переслано от {fwdName}]:\n{fwdContent}"
: $"{content}\n\n[Переслано от {fwdName}]:\n{fwdContent}"; : $"{content}\n\n[Переслано от {fwdName}]:\n{fwdContent}";
} }
else if (string.IsNullOrEmpty(content)) else if (string.IsNullOrEmpty(content))
@@ -282,7 +317,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var fallback = node.QuerySelectorAll(".media_wrap a[href]"); var fallback = node.QuerySelectorAll(".media_wrap a[href]");
mediaNodes.AddRange(fallback); mediaNodes.AddRange(fallback);
} }
var messageType = "text"; var messageType = "text";
(string mType, string cType) GetMediaTypes(string fileUrl) (string mType, string cType) GetMediaTypes(string fileUrl)
@@ -306,7 +341,9 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
if (mediaNodes[0].ClassName?.Contains("animated") == true || firstHref.EndsWith(".mp4")) if (mediaNodes[0].ClassName?.Contains("animated") == true || firstHref.EndsWith(".mp4"))
{ {
if (mediaNodes[0].ClassName?.Contains("animated") == true) if (mediaNodes[0].ClassName?.Contains("animated") == true)
messageType = "image"; {
messageType = "image";
}
} }
} }
} }
@@ -323,7 +360,7 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
{ {
string finalContent = content; string finalContent = content;
targetMessage = Message.Import(chatId, senderGuid, finalContent, messageType, createdAt, replyToId, forwardedFromId); targetMessage = Message.Import(chatId, senderGuid, finalContent, messageType, createdAt, replyToId, forwardedFromId);
var idAttr = node.GetAttribute("id"); var idAttr = node.GetAttribute("id");
if (!string.IsNullOrEmpty(idAttr)) if (!string.IsNullOrEmpty(idAttr))
{ {
@@ -349,7 +386,11 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
var types = GetMediaTypes(href); var types = GetMediaTypes(href);
var finalMType = types.mType; var finalMType = types.mType;
if (mediaNode.ClassName?.Contains("animated") == true) finalMType = "image"; if (mediaNode.ClassName?.Contains("animated") == true)
{
finalMType = "image";
}
var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType); var fileId = await _fileStorage.UploadFileAsync(ms, Path.GetFileName(href), types.cType);
targetMessage.AddMedia(finalMType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length); targetMessage.AddMedia(finalMType, $"/api/files/{fileId}", Path.GetFileName(href), zipEntry.Length);
} }
@@ -361,7 +402,11 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler<ExecuteImpor
foreach (var reactionNode in reactionNodes) foreach (var reactionNode in reactionNodes)
{ {
var emojiNode = reactionNode.QuerySelector(".emoji"); var emojiNode = reactionNode.QuerySelector(".emoji");
if (emojiNode == null) continue; if (emojiNode == null)
{
continue;
}
var emoji = emojiNode.TextContent.Trim(); var emoji = emojiNode.TextContent.Trim();
var userpicNodes = reactionNode.QuerySelectorAll(".userpics .userpic .initials[title]"); var userpicNodes = reactionNode.QuerySelectorAll(".userpics .userpic .initials[title]");
@@ -20,7 +20,7 @@ public static class DependencyInjection
{ {
// Настройка базы данных // Настройка базы данных
string connectionString = configuration.GetConnectionString("DefaultConnection")!; string connectionString = configuration.GetConnectionString("DefaultConnection")!;
services.AddDbContext<ChatsDbContext>(options => services.AddDbContext<ChatsDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
@@ -30,7 +30,7 @@ public static class DependencyInjection
services.AddScoped<IMessageRepository, MessageRepository>(); services.AddScoped<IMessageRepository, MessageRepository>();
// Регистрация MediatR для этого модуля // Регистрация MediatR для этого модуля
services.AddMediatR(config => services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
return services; return services;
@@ -80,7 +80,11 @@ public sealed class Chat : AggregateRoot<Guid>
public void AddMember(Guid userId, string role = "member") public void AddMember(Guid userId, string role = "member")
{ {
if (_members.Any(m => m.UserId == userId)) return; if (_members.Any(m => m.UserId == userId))
{
return;
}
_members.Add(new ChatMember(Id, userId, role)); _members.Add(new ChatMember(Id, userId, role));
RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId)); RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId));
} }
@@ -88,7 +92,10 @@ public sealed class Chat : AggregateRoot<Guid>
public void RemoveMember(Guid userId) public void RemoveMember(Guid userId)
{ {
var member = _members.FirstOrDefault(m => m.UserId == userId); var member = _members.FirstOrDefault(m => m.UserId == userId);
if (member != null) _members.Remove(member); if (member != null)
{
_members.Remove(member);
}
} }
public void UpdateName(string name) => Name = name; public void UpdateName(string name) => Name = name;
@@ -7,7 +7,7 @@ public sealed class ReadReceipt : Entity<Guid>
public Guid MessageId { get; private set; } public Guid MessageId { get; private set; }
public Guid UserId { get; private set; } public Guid UserId { get; private set; }
public DateTime ReadAt { get; private set; } public DateTime ReadAt { get; private set; }
// Для EF Core // Для EF Core
private ReadReceipt() : base(Guid.Empty) { } private ReadReceipt() : base(Guid.Empty) { }
@@ -16,7 +16,7 @@ public sealed class ChatCreatedDomainEventHandler : INotificationHandler<ChatCre
private readonly IUserDisplayNameProvider _displayNameProvider; private readonly IUserDisplayNameProvider _displayNameProvider;
public ChatCreatedDomainEventHandler( public ChatCreatedDomainEventHandler(
IHubContext<ChatHub> hubContext, IHubContext<ChatHub> hubContext,
IChatRepository chatRepository, IChatRepository chatRepository,
IUserDisplayNameProvider displayNameProvider) IUserDisplayNameProvider displayNameProvider)
{ {
@@ -52,7 +52,7 @@ public sealed class ChatCreatedDomainEventHandler : INotificationHandler<ChatCre
member.UserId, member.UserId,
member.Role, member.Role,
member.JoinedAt, member.JoinedAt,
User = userInfo != null User = userInfo != null
? new { userInfo.Id, userInfo.Username, userInfo.DisplayName, userInfo.Avatar, isOnline = false } ? new { userInfo.Id, userInfo.Username, userInfo.DisplayName, userInfo.Avatar, isOnline = false }
: new { Id = member.UserId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null, isOnline = false } : new { Id = member.UserId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null, isOnline = false }
}); });
@@ -17,7 +17,7 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
private readonly IUserDisplayNameProvider _displayNameProvider; private readonly IUserDisplayNameProvider _displayNameProvider;
public MessageSentDomainEventHandler( public MessageSentDomainEventHandler(
IHubContext<ChatHub> hubContext, IHubContext<ChatHub> hubContext,
IMessageRepository messageRepository, IMessageRepository messageRepository,
IUserDisplayNameProvider displayNameProvider) IUserDisplayNameProvider displayNameProvider)
{ {
@@ -29,10 +29,13 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
public async Task Handle(MessageSentDomainEvent notification, CancellationToken cancellationToken) public async Task Handle(MessageSentDomainEvent notification, CancellationToken cancellationToken)
{ {
var message = await _messageRepository.GetByIdAsync(notification.MessageId, cancellationToken); var message = await _messageRepository.GetByIdAsync(notification.MessageId, cancellationToken);
if (message is null) return; if (message is null)
{
return;
}
var userInfo = await _displayNameProvider.GetUserInfoAsync(message.SenderId, cancellationToken); var userInfo = await _displayNameProvider.GetUserInfoAsync(message.SenderId, cancellationToken);
var senderObj = userInfo != null var senderObj = userInfo != null
? new { Id = userInfo.Id, Username = userInfo.Username, DisplayName = userInfo.DisplayName, Avatar = userInfo.Avatar } ? new { Id = userInfo.Id, Username = userInfo.Username, DisplayName = userInfo.DisplayName, Avatar = userInfo.Avatar }
: new { Id = message.SenderId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null }; : new { Id = message.SenderId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null };
@@ -82,12 +85,13 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
message.ReplyToId, message.ReplyToId,
ReplyTo = replyToObj, ReplyTo = replyToObj,
message.Quote, message.Quote,
Media = message.Media.Select(m => new { Media = message.Media.Select(m => new
m.Id, {
m.Type, m.Id,
m.Url, m.Type,
m.Url,
Filename = m.Filename, Filename = m.Filename,
Size = m.Size Size = m.Size
}).ToList(), }).ToList(),
Sender = senderObj, Sender = senderObj,
ReadBy = new List<object>(), ReadBy = new List<object>(),
@@ -38,9 +38,9 @@ public sealed class ChatRepository : IChatRepository
{ {
return await _dbContext.Chats return await _dbContext.Chats
.Include(c => c.Members) .Include(c => c.Members)
.FirstOrDefaultAsync(c => .FirstOrDefaultAsync(c =>
c.Type == ChatType.Favorites && c.Type == ChatType.Favorites &&
c.Members.Any(m => m.UserId == userId), c.Members.Any(m => m.UserId == userId),
cancellationToken); cancellationToken);
} }
@@ -104,14 +104,20 @@ public sealed class MessageRepository : IMessageRepository
.AnyAsync(m => m.Id == messageId, cancellationToken); .AnyAsync(m => m.Id == messageId, cancellationToken);
if (!messageExists) return false; if (!messageExists)
{
return false;
}
// Проверяем, есть ли уже такая реакция // Проверяем, есть ли уже такая реакция
var existingReaction = await _dbContext.Reactions var existingReaction = await _dbContext.Reactions
.FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken); .FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken);
if (existingReaction != null) return true; // Уже существует if (existingReaction != null)
{
return true; // Уже существует
}
// Добавляем новую реакцию напрямую // Добавляем новую реакцию напрямую
var reaction = new Reaction(messageId, userId, emoji); var reaction = new Reaction(messageId, userId, emoji);
@@ -128,7 +134,10 @@ public sealed class MessageRepository : IMessageRepository
.FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken); .FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId && r.Emoji == emoji, cancellationToken);
if (reaction == null) return false; if (reaction == null)
{
return false;
}
_dbContext.Reactions.Remove(reaction); _dbContext.Reactions.Remove(reaction);
@@ -518,19 +518,20 @@ public sealed class ChatHub : Hub
if (participants.TryGetValue(userId, out var info)) if (participants.TryGetValue(userId, out var info))
{ {
// Update local state if needed (e.g. muted status) // Update local state if needed (e.g. muted status)
participants[userId] = info with participants[userId] = info with
{ {
IsMuted = request.IsMuted, IsMuted = request.IsMuted,
IsVideoOff = request.IsVideoOff IsVideoOff = request.IsVideoOff
}; };
} }
} }
await Clients.Group(request.ChatId).SendAsync("group_call_status_updated", new { await Clients.Group(request.ChatId).SendAsync("group_call_status_updated", new
chatId = request.ChatId, {
userId = Context.UserIdentifier, chatId = request.ChatId,
isMuted = request.IsMuted, userId = Context.UserIdentifier,
isVideoOff = request.IsVideoOff isMuted = request.IsMuted,
isVideoOff = request.IsVideoOff
}); });
} }
@@ -544,19 +545,20 @@ public sealed class ChatHub : Hub
if (participants.TryGetValue(userId, out var info)) if (participants.TryGetValue(userId, out var info))
{ {
// Update local state if needed (e.g. muted status) // Update local state if needed (e.g. muted status)
participants[userId] = info with participants[userId] = info with
{ {
IsMuted = isMuted, IsMuted = isMuted,
IsVideoOff = isVideoOff IsVideoOff = isVideoOff
}; };
} }
} }
await Clients.Group(chatId).SendAsync("group_call_status_updated", new { await Clients.Group(chatId).SendAsync("group_call_status_updated", new
chatId = chatId, {
userId = Context.UserIdentifier, chatId = chatId,
isMuted = isMuted, userId = Context.UserIdentifier,
isVideoOff = isVideoOff isMuted = isMuted,
isVideoOff = isVideoOff
}); });
} }
@@ -22,8 +22,10 @@ internal sealed class AcceptFriendRequestCommandHandler : ICommandHandler<Accept
public async Task<Result<Guid>> Handle(AcceptFriendRequestCommand request, CancellationToken cancellationToken) public async Task<Result<Guid>> Handle(AcceptFriendRequestCommand request, CancellationToken cancellationToken)
{ {
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId) if (friendship == null || friendship.FriendId != request.UserId)
{
return Result.Failure<Guid>(new Error("Friends.NotFound", "Request not found")); return Result.Failure<Guid>(new Error("Friends.NotFound", "Request not found"));
}
friendship.Accept(); friendship.Accept();
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -22,8 +22,10 @@ internal sealed class DeclineFriendRequestCommandHandler : ICommandHandler<Decli
public async Task<Result> Handle(DeclineFriendRequestCommand request, CancellationToken cancellationToken) public async Task<Result> Handle(DeclineFriendRequestCommand request, CancellationToken cancellationToken)
{ {
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || friendship.FriendId != request.UserId) if (friendship == null || friendship.FriendId != request.UserId)
{
return Result.Failure(new Error("Friends.NotFound", "Request not found")); return Result.Failure(new Error("Friends.NotFound", "Request not found"));
}
friendship.Decline(); friendship.Decline();
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -3,7 +3,7 @@ using System;
namespace Knot.Modules.Identity.Application.Friends; namespace Knot.Modules.Identity.Application.Friends;
public record FriendshipStatusResponse( public record FriendshipStatusResponse(
string Status, string Status,
Guid? FriendshipId = null, Guid? FriendshipId = null,
string? Direction = null string? Direction = null
); );
@@ -36,7 +36,10 @@ internal sealed class GetFriendsQueryHandler : IQueryHandler<GetFriendsQuery, Li
foreach (var id in friendIds) foreach (var id in friendIds)
{ {
var user = await _userRepository.GetByIdAsync(id, cancellationToken); var user = await _userRepository.GetByIdAsync(id, cancellationToken);
if (user == null) continue; if (user == null)
{
continue;
}
var fs = friendships.First(f => f.UserId == id || f.FriendId == id); var fs = friendships.First(f => f.UserId == id || f.FriendId == id);
@@ -20,15 +20,19 @@ internal sealed class GetFriendshipStatusQueryHandler : IQueryHandler<GetFriends
public async Task<Result<FriendshipStatusResponse>> Handle(GetFriendshipStatusQuery request, CancellationToken cancellationToken) public async Task<Result<FriendshipStatusResponse>> Handle(GetFriendshipStatusQuery request, CancellationToken cancellationToken)
{ {
if (request.CurrentUserId == request.TargetUserId) if (request.CurrentUserId == request.TargetUserId)
{
return Result.Success(new FriendshipStatusResponse("self")); return Result.Success(new FriendshipStatusResponse("self"));
}
var fs = await _context.Friendships var fs = await _context.Friendships
.FirstOrDefaultAsync(f => (f.UserId == request.CurrentUserId && f.FriendId == request.TargetUserId) || .FirstOrDefaultAsync(f => (f.UserId == request.CurrentUserId && f.FriendId == request.TargetUserId) ||
(f.UserId == request.TargetUserId && f.FriendId == request.CurrentUserId), cancellationToken); (f.UserId == request.TargetUserId && f.FriendId == request.CurrentUserId), cancellationToken);
if (fs == null) if (fs == null)
{
return Result.Success(new FriendshipStatusResponse("none")); return Result.Success(new FriendshipStatusResponse("none"));
}
return Result.Success(new FriendshipStatusResponse( return Result.Success(new FriendshipStatusResponse(
fs.Status.ToString().ToLowerInvariant(), fs.Status.ToString().ToLowerInvariant(),
@@ -33,7 +33,10 @@ internal sealed class GetIncomingRequestsQueryHandler : IQueryHandler<GetIncomin
foreach (var fs in friendships) foreach (var fs in friendships)
{ {
var user = await _userRepository.GetByIdAsync(fs.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(fs.UserId, cancellationToken);
if (user == null) continue; if (user == null)
{
continue;
}
requestsList.Add(new FriendRequestDto( requestsList.Add(new FriendRequestDto(
fs.Id, fs.Id,
@@ -33,7 +33,10 @@ internal sealed class GetOutgoingRequestsQueryHandler : IQueryHandler<GetOutgoin
foreach (var fs in friendships) foreach (var fs in friendships)
{ {
var user = await _userRepository.GetByIdAsync(fs.FriendId, cancellationToken); var user = await _userRepository.GetByIdAsync(fs.FriendId, cancellationToken);
if (user == null) continue; if (user == null)
{
continue;
}
requestsList.Add(new FriendRequestDto( requestsList.Add(new FriendRequestDto(
fs.Id, fs.Id,
@@ -23,7 +23,9 @@ internal sealed class RemoveFriendCommandHandler : ICommandHandler<RemoveFriendC
{ {
var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken); var friendship = await _context.Friendships.FindAsync(new object[] { request.FriendshipId }, cancellationToken);
if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId)) if (friendship == null || (friendship.UserId != request.UserId && friendship.FriendId != request.UserId))
{
return Result.Failure(new Error("Friends.NotFound", "Friendship not found")); return Result.Failure(new Error("Friends.NotFound", "Friendship not found"));
}
_context.Friendships.Remove(friendship); _context.Friendships.Remove(friendship);
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -23,15 +23,19 @@ internal sealed class SendFriendRequestCommandHandler : ICommandHandler<SendFrie
public async Task<Result> Handle(SendFriendRequestCommand request, CancellationToken cancellationToken) public async Task<Result> Handle(SendFriendRequestCommand request, CancellationToken cancellationToken)
{ {
if (request.UserId == request.FriendId) if (request.UserId == request.FriendId)
{
return Result.Failure(new Error("Friends.Self", "Cannot add yourself")); return Result.Failure(new Error("Friends.Self", "Cannot add yourself"));
}
var existing = await _context.Friendships var existing = await _context.Friendships
.FirstOrDefaultAsync(f => (f.UserId == request.UserId && f.FriendId == request.FriendId) || .FirstOrDefaultAsync(f => (f.UserId == request.UserId && f.FriendId == request.FriendId) ||
(f.UserId == request.FriendId && f.FriendId == request.UserId), cancellationToken); (f.UserId == request.FriendId && f.FriendId == request.UserId), cancellationToken);
if (existing != null) if (existing != null)
{
return Result.Failure(new Error("Friends.Exists", "Friendship already exists")); return Result.Failure(new Error("Friends.Exists", "Friendship already exists"));
}
var friendship = Friendship.Create(request.UserId, request.FriendId); var friendship = Friendship.Create(request.UserId, request.FriendId);
_context.Friendships.Add(friendship); _context.Friendships.Add(friendship);
@@ -25,7 +25,10 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarComma
public async Task<Result<UserProfileDto>> Handle(CropAvatarCommand request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(CropAvatarCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
string avatarUrl; string avatarUrl;
@@ -20,7 +20,10 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
user.UpdateAvatar(null); user.UpdateAvatar(null);
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -23,7 +23,10 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType); var fileId = await _fileStorage.UploadFileAsync(request.FileStream, request.FileName, request.ContentType);
var avatarUrl = $"/api/files/{fileId}"; var avatarUrl = $"/api/files/{fileId}";
@@ -19,7 +19,10 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
public async Task<Result<AuthResponseDto>> Handle(GetMeQuery request, CancellationToken cancellationToken) public async Task<Result<AuthResponseDto>> Handle(GetMeQuery request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<AuthResponseDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<AuthResponseDto>(new Error("User.NotFound", "User not found"));
}
var response = new AuthResponseDto( var response = new AuthResponseDto(
string.Empty, string.Empty,
@@ -18,7 +18,10 @@ internal sealed class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserProf
public async Task<Result<UserProfileDto>> Handle(GetUserQuery request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(GetUserQuery request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken); var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
var dto = new UserProfileDto( var dto = new UserProfileDto(
user.Id, user.Id,
@@ -28,8 +28,8 @@ public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCom
private readonly IJwtTokenProvider _tokenProvider; private readonly IJwtTokenProvider _tokenProvider;
public RegisterUserCommandHandler( public RegisterUserCommandHandler(
IUserRepository userRepository, IUserRepository userRepository,
IIdentityUnitOfWork unitOfWork, IIdentityUnitOfWork unitOfWork,
ISettingsService settings, ISettingsService settings,
IJwtTokenProvider tokenProvider) IJwtTokenProvider tokenProvider)
{ {
@@ -20,7 +20,10 @@ internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfil
public async Task<Result<UserProfileDto>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday); user.UpdateProfile(request.DisplayName ?? user.DisplayName, request.Bio, request.Birthday);
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -20,7 +20,10 @@ internal sealed class UpdateSettingsCommandHandler : ICommandHandler<UpdateSetti
public async Task<Result<UserProfileDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken) public async Task<Result<UserProfileDto>> Handle(UpdateSettingsCommand request, CancellationToken cancellationToken)
{ {
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null) return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found")); if (user == null)
{
return Result.Failure<UserProfileDto>(new Error("User.NotFound", "User not found"));
}
user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews); user.UpdateSettings(request.HideStoryViews ?? user.HideStoryViews);
await _unitOfWork.SaveChangesAsync(cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken);
@@ -20,7 +20,7 @@ public static class DependencyInjection
{ {
// Настройка базы данных // Настройка базы данных
string connectionString = configuration.GetConnectionString("DefaultConnection")!; string connectionString = configuration.GetConnectionString("DefaultConnection")!;
services.AddDbContext<IdentityDbContext>(options => services.AddDbContext<IdentityDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
@@ -32,7 +32,7 @@ public static class DependencyInjection
services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Identity.Infrastructure.Services.UserDisplayNameProvider>(); services.AddScoped<IUserDisplayNameProvider, Knot.Modules.Identity.Infrastructure.Services.UserDisplayNameProvider>();
// Регистрация MediatR для этого модуля // Регистрация MediatR для этого модуля
services.AddMediatR(config => services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
return services; return services;
@@ -40,20 +40,31 @@ public class Story : Entity<Guid>
public void AddViewer(Guid userId) public void AddViewer(Guid userId)
{ {
if (_viewers.Any(v => v.UserId == userId)) return; if (_viewers.Any(v => v.UserId == userId))
{
return;
}
_viewers.Add(new StoryViewer(Id, userId)); _viewers.Add(new StoryViewer(Id, userId));
} }
public void AddReaction(Guid userId, string emoji) public void AddReaction(Guid userId, string emoji)
{ {
if (_reactions.Any(r => r.UserId == userId && r.Emoji == emoji)) return; if (_reactions.Any(r => r.UserId == userId && r.Emoji == emoji))
{
return;
}
_reactions.Add(new StoryReaction(Id, userId, emoji)); _reactions.Add(new StoryReaction(Id, userId, emoji));
} }
public void RemoveReaction(Guid userId, string emoji) public void RemoveReaction(Guid userId, string emoji)
{ {
var reaction = _reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji); var reaction = _reactions.FirstOrDefault(r => r.UserId == userId && r.Emoji == emoji);
if (reaction != null) _reactions.Remove(reaction); if (reaction != null)
{
_reactions.Remove(reaction);
}
} }
public void AddReply(Guid userId, string content) public void AddReply(Guid userId, string content)
@@ -17,7 +17,7 @@ public sealed class User : AggregateRoot<Guid>
public DateTime CreatedAt { get; private set; } public DateTime CreatedAt { get; private set; }
public bool HideStoryViews { get; private set; } public bool HideStoryViews { get; private set; }
private User(Guid id, string username, string passwordHash, string displayName, string? email, string? bio = null) private User(Guid id, string username, string passwordHash, string displayName, string? email, string? bio = null)
: base(id) : base(id)
{ {
Username = username; Username = username;
@@ -101,7 +101,7 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork, Knot.Mod
{ {
builder.ToTable("StoryReplies"); builder.ToTable("StoryReplies");
builder.HasKey(r => r.Id); builder.HasKey(r => r.Id);
builder.Property(r => r.Content) builder.Property(r => r.Content)
.IsRequired() .IsRequired()
.HasMaxLength(500) .HasMaxLength(500)
@@ -38,7 +38,7 @@ public sealed class UserRepository : IUserRepository
public async Task<List<User>> SearchUsersAsync(string query, CancellationToken cancellationToken = default) public async Task<List<User>> SearchUsersAsync(string query, CancellationToken cancellationToken = default)
{ {
return await _context.Users return await _context.Users
.Where(u => u.Username.ToLower().Contains(query.ToLower()) || .Where(u => u.Username.ToLower().Contains(query.ToLower()) ||
(u.DisplayName != null && u.DisplayName.ToLower().Contains(query.ToLower()))) (u.DisplayName != null && u.DisplayName.ToLower().Contains(query.ToLower())))
.Take(20) .Take(20)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -21,7 +21,11 @@ public sealed class UserDisplayNameProvider : IUserDisplayNameProvider
public async Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken ct = default) public async Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken ct = default)
{ {
var user = await _userRepository.GetByIdAsync(userId, ct); var user = await _userRepository.GetByIdAsync(userId, ct);
if (user == null) return null; if (user == null)
{
return null;
}
return new UserInfo(user.Id, user.Username, user.DisplayName, user.Avatar); return new UserInfo(user.Id, user.Username, user.DisplayName, user.Avatar);
} }
@@ -21,9 +21,9 @@ public static class DependencyInjection
services.AddScoped<IUserContext, UserContext>(); services.AddScoped<IUserContext, UserContext>();
// Настройка шифрования // Настройка шифрования
var masterKey = configuration["KNOT_MASTER_ENCRYPTION_KEY"] var masterKey = configuration["KNOT_MASTER_ENCRYPTION_KEY"]
?? throw new ArgumentNullException("KNOT_MASTER_ENCRYPTION_KEY is missing in env."); ?? throw new ArgumentNullException("KNOT_MASTER_ENCRYPTION_KEY is missing in env.");
services.AddSingleton<IEncryptionService>(new AesEncryptionService(masterKey)); services.AddSingleton<IEncryptionService>(new AesEncryptionService(masterKey));
// Настройка MinIO (S3) // Настройка MinIO (S3)
@@ -38,7 +38,7 @@ public static class DependencyInjection
.WithSSL(false) .WithSSL(false)
.Build()); .Build());
services.AddScoped<IFileStorageService>(provider => services.AddScoped<IFileStorageService>(provider =>
{ {
var minioClient = provider.GetRequiredService<IMinioClient>(); var minioClient = provider.GetRequiredService<IMinioClient>();
var encService = provider.GetRequiredService<IEncryptionService>(); var encService = provider.GetRequiredService<IEncryptionService>();
@@ -46,10 +46,10 @@ public static class DependencyInjection
}); });
// Настройка System Database // Настройка System Database
var connectionString = configuration.GetConnectionString("DefaultConnection") var connectionString = configuration.GetConnectionString("DefaultConnection")
?? configuration["DATABASE_URL"] ?? configuration["DATABASE_URL"]
?? throw new ArgumentNullException("Database connection string not found."); ?? throw new ArgumentNullException("Database connection string not found.");
services.AddDbContext<SystemDbContext>(options => services.AddDbContext<SystemDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
@@ -40,7 +40,7 @@ public class AdminAuthMiddleware
var authHeader = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"]); var authHeader = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter ?? string.Empty); var credentialBytes = Convert.FromBase64String(authHeader.Parameter ?? string.Empty);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);
var username = credentials[0]; var username = credentials[0];
var password = credentials[1]; var password = credentials[1];
@@ -32,7 +32,7 @@ public sealed class ExceptionHandlingMiddleware
private static async Task HandleExceptionAsync(HttpContext context, Exception exception) private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{ {
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
var (statusCode, message) = exception switch var (statusCode, message) = exception switch
{ {
UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "Unauthorized access."), UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "Unauthorized access."),
@@ -22,8 +22,8 @@ public class SystemDbContextFactory : IDesignTimeDbContextFactory<SystemDbContex
.Build(); .Build();
var builder = new DbContextOptionsBuilder<SystemDbContext>(); var builder = new DbContextOptionsBuilder<SystemDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection") var connectionString = configuration.GetConnectionString("DefaultConnection")
?? configuration["DATABASE_URL"] ?? configuration["DATABASE_URL"]
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass"; ?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
builder.UseNpgsql(connectionString); builder.UseNpgsql(connectionString);
@@ -29,17 +29,22 @@ public class AesEncryptionService : IEncryptionService
} }
if (_key.Length != 32) if (_key.Length != 32)
{
throw new ArgumentException("Мастер-ключ должен быть ровно 32 байта для AES-256."); throw new ArgumentException("Мастер-ключ должен быть ровно 32 байта для AES-256.");
}
} }
// Сообщения: AES-256-GCM // Сообщения: AES-256-GCM
public string EncryptMessage(string plainText) public string EncryptMessage(string plainText)
{ {
if (string.IsNullOrEmpty(plainText)) return plainText; if (string.IsNullOrEmpty(plainText))
{
return plainText;
}
var nonce = new byte[12]; var nonce = new byte[12];
RandomNumberGenerator.Fill(nonce); RandomNumberGenerator.Fill(nonce);
var plainBytes = Encoding.UTF8.GetBytes(plainText); var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = new byte[plainBytes.Length]; var cipherBytes = new byte[plainBytes.Length];
var tag = new byte[16]; var tag = new byte[16];
@@ -53,10 +58,16 @@ public class AesEncryptionService : IEncryptionService
public string DecryptMessage(string cipherText) public string DecryptMessage(string cipherText)
{ {
if (string.IsNullOrEmpty(cipherText)) return cipherText; if (string.IsNullOrEmpty(cipherText))
{
return cipherText;
}
var parts = cipherText.Split(':'); var parts = cipherText.Split(':');
if (parts.Length != 3) return cipherText; if (parts.Length != 3)
{
return cipherText;
}
try try
{ {
@@ -49,28 +49,31 @@ public class StatisticsService : IStatisticsService
long totalDiskSpace = 5L * 1024 * 1024 * 1024 * 1024; // Fallback long totalDiskSpace = 5L * 1024 * 1024 * 1024 * 1024; // Fallback
long freeSpace = 0; long freeSpace = 0;
try try
{ {
var drive = new System.IO.DriveInfo("/"); var drive = new System.IO.DriveInfo("/");
if(drive.IsReady) if (drive.IsReady)
{ {
totalDiskSpace = drive.TotalSize; totalDiskSpace = drive.TotalSize;
freeSpace = drive.AvailableFreeSpace; freeSpace = drive.AvailableFreeSpace;
} }
else else
{ {
drive = new System.IO.DriveInfo(System.IO.Directory.GetCurrentDirectory()); drive = new System.IO.DriveInfo(System.IO.Directory.GetCurrentDirectory());
totalDiskSpace = drive.TotalSize; totalDiskSpace = drive.TotalSize;
freeSpace = drive.AvailableFreeSpace; freeSpace = drive.AvailableFreeSpace;
} }
} }
catch { } catch { }
var usedSpace = latestStat?.TotalFilesSize ?? 0; var usedSpace = latestStat?.TotalFilesSize ?? 0;
long onlineUsersCount = _cache.TryGetValue("Global_OnlineUsersCount", out int count) ? count : 0; long onlineUsersCount = _cache.TryGetValue("Global_OnlineUsersCount", out int count) ? count : 0;
long offlineUsersCount = totalUsers - onlineUsersCount; long offlineUsersCount = totalUsers - onlineUsersCount;
if(offlineUsersCount < 0) offlineUsersCount = 0; if (offlineUsersCount < 0)
{
offlineUsersCount = 0;
}
var stats = new DashboardStatsDto var stats = new DashboardStatsDto
{ {
@@ -78,7 +81,7 @@ public class StatisticsService : IStatisticsService
StorageLimitBytes = freeSpace > 0 ? usedSpace + freeSpace : totalDiskSpace, StorageLimitBytes = freeSpace > 0 ? usedSpace + freeSpace : totalDiskSpace,
OnlineUsers = onlineUsersCount, OnlineUsers = onlineUsersCount,
OfflineUsers = offlineUsersCount, OfflineUsers = offlineUsersCount,
TotalUsers = totalUsers, TotalUsers = totalUsers,
ActivityTimeline = history ActivityTimeline = history
}; };
@@ -97,7 +100,10 @@ internal static class SqlExtensions
{ {
cmd.CommandText = "SELECT COUNT(*) FROM identity.\"Users\""; cmd.CommandText = "SELECT COUNT(*) FROM identity.\"Users\"";
if (cmd.Connection?.State != System.Data.ConnectionState.Open) if (cmd.Connection?.State != System.Data.ConnectionState.Open)
{
await ((System.Data.Common.DbConnection)cmd.Connection!).OpenAsync(); await ((System.Data.Common.DbConnection)cmd.Connection!).OpenAsync();
}
var result = await ((System.Data.Common.DbCommand)cmd).ExecuteScalarAsync(); var result = await ((System.Data.Common.DbCommand)cmd).ExecuteScalarAsync();
return Convert.ToInt64(result); return Convert.ToInt64(result);
} }
@@ -30,7 +30,7 @@ public class StatisticsWorker : BackgroundService
// Simplified gathering for daily stats // Simplified gathering for daily stats
var today = DateTime.UtcNow.Date; var today = DateTime.UtcNow.Date;
var stat = await db.DailyStats.FirstOrDefaultAsync(s => s.Date == today, stoppingToken); var stat = await db.DailyStats.FirstOrDefaultAsync(s => s.Date == today, stoppingToken);
if (stat == null) if (stat == null)
{ {
stat = new DailyStat { Date = today }; stat = new DailyStat { Date = today };
@@ -41,10 +41,10 @@ public class StatisticsWorker : BackgroundService
var cmd = conn.CreateCommand(); var cmd = conn.CreateCommand();
stat.ActiveUsers = await cmd.QueryTotalUsersAsync(); stat.ActiveUsers = await cmd.QueryTotalUsersAsync();
long dbSize = 0; long dbSize = 0;
long filesSize = 0; long filesSize = 0;
try try
{ {
// 1. Database size itself // 1. Database size itself
cmd.CommandText = "SELECT pg_database_size(current_database());"; cmd.CommandText = "SELECT pg_database_size(current_database());";
@@ -59,7 +59,7 @@ public class StatisticsWorker : BackgroundService
catch { } catch { }
stat.TotalFilesSize = dbSize + filesSize; // Database size + MinIO files size stat.TotalFilesSize = dbSize + filesSize; // Database size + MinIO files size
await db.SaveChangesAsync(stoppingToken); await db.SaveChangesAsync(stoppingToken);
} }
catch catch
@@ -118,9 +118,20 @@ public class S3FileStorageService : IFileStorageService
var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId); var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId);
var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false); var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false);
if (stat.MetaData.ContainsKey("Contenttype")) contentType = stat.MetaData["Contenttype"]; if (stat.MetaData.ContainsKey("Contenttype"))
if (stat.MetaData.ContainsKey("Originalfilename")) fileName = stat.MetaData["Originalfilename"]; {
if (stat.MetaData.ContainsKey("Iv")) ivBase64 = stat.MetaData["Iv"]; contentType = stat.MetaData["Contenttype"];
}
if (stat.MetaData.ContainsKey("Originalfilename"))
{
fileName = stat.MetaData["Originalfilename"];
}
if (stat.MetaData.ContainsKey("Iv"))
{
ivBase64 = stat.MetaData["Iv"];
}
// Загружаем во временный файл (так как CryptoStream требует правильного чтения/записи) // Загружаем во временный файл (так как CryptoStream требует правильного чтения/записи)
var getObjArgs = new GetObjectArgs() var getObjArgs = new GetObjectArgs()
@@ -171,7 +182,7 @@ public class S3FileStorageService : IFileStorageService
try try
{ {
var listArgs = new ListObjectsArgs().WithBucket(_bucketName).WithRecursive(true); var listArgs = new ListObjectsArgs().WithBucket(_bucketName).WithRecursive(true);
await foreach (var item in _minioClient.ListObjectsEnumAsync(listArgs).ConfigureAwait(false)) await foreach (var item in _minioClient.ListObjectsEnumAsync(listArgs).ConfigureAwait(false))
{ {
result.Add((item.Key, (long)item.Size)); result.Add((item.Key, (long)item.Size));
@@ -22,7 +22,7 @@ public class SystemSettingsDto
// Confederation // Confederation
public bool EnableConfederation { get; set; } = false; public bool EnableConfederation { get; set; } = false;
public List<string> AllowedDomains { get; set; } = new(); public List<string> AllowedDomains { get; set; } = new();
// Auth // Auth
public bool EnableRegistration { get; set; } = true; public bool EnableRegistration { get; set; } = true;
} }
@@ -4,12 +4,12 @@ public static class Klipy
{ {
public const string ApiUrlCo = "https://api.klipy.co/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}"; public const string ApiUrlCo = "https://api.klipy.co/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}";
public const string ApiUrlCom = "https://api.klipy.com/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}"; public const string ApiUrlCom = "https://api.klipy.com/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}";
public const string ResourceTrending = "gifs/trending"; public const string ResourceTrending = "gifs/trending";
public const string ResourceSearch = "gifs/search"; public const string ResourceSearch = "gifs/search";
public const int TrendingCacheMinutes = 60; public const int TrendingCacheMinutes = 60;
public const int SearchCacheMinutes = 15; public const int SearchCacheMinutes = 15;
public const string DefaultCustomerId = "anonymous"; public const string DefaultCustomerId = "anonymous";
} }
@@ -21,8 +21,16 @@ public abstract class Entity<TId> : IEquatable<Entity<TId>>
public bool Equals(Entity<TId>? other) public bool Equals(Entity<TId>? other)
{ {
if (other is null) return false; if (other is null)
if (ReferenceEquals(this, other)) return true; {
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Id.Equals(other.Id); return Id.Equals(other.Id);
} }
@@ -7,7 +7,9 @@ public interface ICommand : IRequest<Result> { }
public interface ICommand<TResponse> : IRequest<Result<TResponse>> { } public interface ICommand<TResponse> : IRequest<Result<TResponse>> { }
public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, Result> public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, Result>
where TCommand : ICommand { } where TCommand : ICommand
{ }
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>> public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
where TCommand : ICommand<TResponse> { } where TCommand : ICommand<TResponse>
{ }
@@ -20,9 +20,14 @@ public class Result
protected Result(bool isSuccess, Error error) protected Result(bool isSuccess, Error error)
{ {
if (isSuccess && error != Error.None) if (isSuccess && error != Error.None)
{
throw new InvalidOperationException(); throw new InvalidOperationException();
}
if (!isSuccess && error == Error.None) if (!isSuccess && error == Error.None)
{
throw new InvalidOperationException(); throw new InvalidOperationException();
}
IsSuccess = isSuccess; IsSuccess = isSuccess;
Error = error; Error = error;
@@ -10,7 +10,7 @@ public interface IEncryptionService
// Создает поток шифрования для потоковой передачи файлов "на лету" // Создает поток шифрования для потоковой передачи файлов "на лету"
Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv); Stream CreateEncryptionStream(Stream unencryptedOutputStream, out byte[] iv);
// Создает поток дешифрования для чтения файлов "на лету" // Создает поток дешифрования для чтения файлов "на лету"
Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv); Stream CreateDecryptionStream(Stream encryptedInputStream, byte[] iv);
} }
@@ -15,7 +15,11 @@ public abstract class ValueObject : IEquatable<ValueObject>
public bool Equals(ValueObject? other) public bool Equals(ValueObject? other)
{ {
if (other is null) return false; if (other is null)
{
return false;
}
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
} }
@@ -27,7 +27,7 @@ public class GetOrCreateFavoritesCommandHandlerTests
// Arrange // Arrange
var userId = Guid.NewGuid(); var userId = Guid.NewGuid();
var existingChat = Chat.Create("Избранное", ChatType.Favorites); var existingChat = Chat.Create("Избранное", ChatType.Favorites);
_chatRepository.GetFavoritesAsync(userId, Arg.Any<CancellationToken>()) _chatRepository.GetFavoritesAsync(userId, Arg.Any<CancellationToken>())
.Returns(existingChat); .Returns(existingChat);
@@ -39,7 +39,7 @@ public class GetOrCreateFavoritesCommandHandlerTests
// Assert // Assert
result.IsSuccess.Should().BeTrue(); result.IsSuccess.Should().BeTrue();
result.Value.Should().Be(existingChat.Id); result.Value.Should().Be(existingChat.Id);
_chatRepository.DidNotReceive().Add(Arg.Any<Chat>()); _chatRepository.DidNotReceive().Add(Arg.Any<Chat>());
await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>()); await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
} }
@@ -60,12 +60,12 @@ public class GetOrCreateFavoritesCommandHandlerTests
// Assert // Assert
result.IsSuccess.Should().BeTrue(); result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeEmpty(); result.Value.Should().NotBeEmpty();
_chatRepository.Received(1).Add(Arg.Is<Chat>(c => _chatRepository.Received(1).Add(Arg.Is<Chat>(c =>
c.Type == ChatType.Favorites && c.Type == ChatType.Favorites &&
c.Name == "Избранное" && c.Name == "Избранное" &&
c.Members.Any(m => m.UserId == userId))); c.Members.Any(m => m.UserId == userId)));
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>()); await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
} }
} }
@@ -29,12 +29,12 @@ public class LoginUserCommandHandlerTests
var password = "password123"; var password = "password123";
var passwordHash = BCrypt.Net.BCrypt.HashPassword(password); var passwordHash = BCrypt.Net.BCrypt.HashPassword(password);
var user = User.Create("testuser", passwordHash, "Test User"); var user = User.Create("testuser", passwordHash, "Test User");
var command = new LoginUserCommand("testuser", password); var command = new LoginUserCommand("testuser", password);
_userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>()) _userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>())
.Returns(user); .Returns(user);
_tokenProvider.Generate(user).Returns("valid-jwt-token"); _tokenProvider.Generate(user).Returns("valid-jwt-token");
// Act // Act
@@ -68,9 +68,9 @@ public class LoginUserCommandHandlerTests
var correctPassword = "correctPassword"; var correctPassword = "correctPassword";
var passwordHash = BCrypt.Net.BCrypt.HashPassword(correctPassword); var passwordHash = BCrypt.Net.BCrypt.HashPassword(correctPassword);
var user = User.Create("testuser", passwordHash, "Test User"); var user = User.Create("testuser", passwordHash, "Test User");
var command = new LoginUserCommand("testuser", "wrongPassword"); var command = new LoginUserCommand("testuser", "wrongPassword");
_userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>()) _userRepository.GetByUsernameAsync(command.Username, Arg.Any<CancellationToken>())
.Returns(user); .Returns(user);
@@ -35,12 +35,12 @@ public class RegisterUserCommandHandlerTests
// Assert // Assert
result.IsSuccess.Should().BeTrue(); result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeEmpty(); result.Value.Should().NotBeEmpty();
_userRepository.Received(1).Add(Arg.Is<User>(u => _userRepository.Received(1).Add(Arg.Is<User>(u =>
u.Username == command.Username && u.Username == command.Username &&
u.DisplayName == command.DisplayName && u.DisplayName == command.DisplayName &&
u.Email == command.Email)); u.Email == command.Email));
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>()); await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
} }
@@ -58,7 +58,7 @@ public class RegisterUserCommandHandlerTests
// Assert // Assert
result.IsFailure.Should().BeTrue(); result.IsFailure.Should().BeTrue();
result.Error.Code.Should().Be("Identity.UsernameNotUnique"); result.Error.Code.Should().Be("Identity.UsernameNotUnique");
_userRepository.DidNotReceive().Add(Arg.Any<User>()); _userRepository.DidNotReceive().Add(Arg.Any<User>());
await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>()); await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
} }