diff --git a/Govor.API.Tests/IntegrationTests/Controllers/FriendsControllerTests.cs b/Govor.API.Tests/IntegrationTests/Controllers/FriendsControllerTests.cs index 34d0791..98618d5 100644 --- a/Govor.API.Tests/IntegrationTests/Controllers/FriendsControllerTests.cs +++ b/Govor.API.Tests/IntegrationTests/Controllers/FriendsControllerTests.cs @@ -141,7 +141,7 @@ public class FriendsControllerTests _currentUserServiceMock.Setup(c => c.GetCurrentUserId()).Returns(currentUserId); - _friendsServiceMock.Setup(f => f.SendFriendRequestAsync(targetUserId, currentUserId)) + _friendsServiceMock.Setup(f => f.SendFriendRequestAsync(currentUserId, targetUserId)) .ThrowsAsync(new RequestAlreadySentException(currentUserId, targetUserId)); var result = await _controller.SendRequest(targetUserId); @@ -351,7 +351,7 @@ public class FriendsControllerTests var result = await _controller.GetFriends(); // Assert - Assert.That(result, Is.InstanceOf()); + Assert.That(result, Is.InstanceOf()); } [Test] diff --git a/Govor.API/Controllers/FriendsController.cs b/Govor.API/Controllers/FriendsController.cs index c7092fc..d49a732 100644 --- a/Govor.API/Controllers/FriendsController.cs +++ b/Govor.API/Controllers/FriendsController.cs @@ -57,7 +57,7 @@ public class FriendsController : Controller try { - await _friendsService.SendFriendRequestAsync(targetUserId, _currentUserService.GetCurrentUserId()); + await _friendsService.SendFriendRequestAsync(_currentUserService.GetCurrentUserId(), targetUserId); return Ok(new { message = "Friend request sent successfully." }); } catch (InvalidOperationException ex) @@ -98,14 +98,14 @@ public class FriendsController : Controller } [HttpPost("accept")] - public async Task AcceptFriend(Guid requesterId) + public async Task AcceptFriend(Guid friendshipId) { - if (requesterId == Guid.Empty) + if (friendshipId == Guid.Empty) return BadRequest("Requester ID is invalid"); try { - await _friendsService.AcceptFriendRequestAsync(requesterId, _currentUserService.GetCurrentUserId()); + await _friendsService.AcceptFriendRequestAsync(friendshipId, _currentUserService.GetCurrentUserId()); return Ok(new { message = "Friend request accepted." }); } catch (InvalidOperationException ex) @@ -136,7 +136,7 @@ public class FriendsController : Controller catch (InvalidOperationException ex) { _logger.LogError(ex, ex.Message); - return BadRequest(new { error = "User data not found." }); + return Ok(Array.Empty()); } catch (Exception ex) { diff --git a/Govor.API/Controllers/OnlinePingingController.cs b/Govor.API/Controllers/OnlinePingingController.cs index 71d74d7..871e960 100644 --- a/Govor.API/Controllers/OnlinePingingController.cs +++ b/Govor.API/Controllers/OnlinePingingController.cs @@ -54,4 +54,10 @@ public class OnlinePingingController : Controller return StatusCode(500, new { error = "Failed to send friend request." }); } } + + [HttpGet("is-online")] + public async Task IsOnline(Guid userId) + { + return BadRequest(); + } } \ No newline at end of file diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 4942226..5c37ba9 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -1,11 +1,9 @@ -using System.Security.Claims; -using Govor.Application.Interfaces; +using Govor.API.Services; +using Govor.Application.Interfaces.Messages; +using Govor.Application.Interfaces.Messages.Parameters; using Govor.Contracts.Requests.SignalR; using Govor.Core.Models; -using Govor.Core.Repositories.Users; -using Govor.Data.Repositories.Exceptions; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; namespace Govor.API.Hubs; @@ -13,14 +11,16 @@ namespace Govor.API.Hubs; [Authorize] public class ChatsHub : Hub { - private readonly IUsersRepository _usersRepository; - private readonly IVerifyFriendship _verifyFriendship; private readonly ILogger _logger; - - public ChatsHub(IUsersRepository usersRepository, ILogger logger) + private readonly IChatService _chatService; + private readonly IGroupService _groupService; + + public ChatsHub(ILogger logger, + IChatService chatService + ) { - _usersRepository = usersRepository; _logger = logger; + _chatService = chatService; } public override async Task OnConnectedAsync() @@ -28,7 +28,7 @@ public class ChatsHub : Hub var userId = GetUserId(); if (userId != Guid.Empty) { - // Привязываем ConnectionId к UserId + // Binding ConnectionId to UserId await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId); } @@ -46,6 +46,11 @@ public class ChatsHub : Hub await base.OnDisconnectedAsync(exception); } + public async Task Remove(Guid recipientId, Guid messageId) + { + + } + public async Task Edit(string newMessage, Guid messageId) { @@ -53,7 +58,6 @@ public class ChatsHub : Hub public async Task Send(MessageRequest request) { - // Валидация входных данных if (string.IsNullOrWhiteSpace(request.EncryptedContent)) { _logger.LogWarning("Empty message received from user {UserId}", GetUserId()); @@ -62,38 +66,43 @@ public class ChatsHub : Hub var senderId = GetUserId(); - // Проверка существования получателя и установленной дружбы - try - { - await _usersRepository.FindByIdAsync(request.RecipientId); - await _verifyFriendship.VerifyAsync(senderId, request.RecipientId); - } - catch (NotFoundByKeyException ex) - { - _logger.LogWarning("Recipient user {ToUserId} not found", request.RecipientId); - throw; - } - catch (ArgumentException ex) - { - _logger.LogWarning("Invalid recipient userId received from user {UserId}", GetUserId()); - throw; - } - try { _logger.LogInformation("Message sent from {SenderId} to {RecipientId} at {UtcNow}", senderId, request.RecipientId, DateTime.UtcNow); + + var message = new SendMessage( + EncryptContent: request.EncryptedContent, + ReplyToMessageId: request.ReplyToMessageId, + FromUserId: senderId, + RecipientId: request.RecipientId, + SendAt: DateTime.UtcNow, + Media: request.MediaAttachments?.Select(f => new SendMedia( + f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty()); - // Отправка сообщения отправителю и получателю - //await Clients.Group(request.RecipientId.ToString()).SendAsync("Receive", message, senderId); - // Clients.Group(senderId.ToString()).SendAsync("Receive", message, senderId); + if (request.RecipientType == RecipientType.User) + { + await SendUser(message); + } + // TODO: Send to Group } catch (Exception ex) { - //_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, toUserId); + _logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId); throw; } } + private async Task SendUser(SendMessage sendMessage) + { + Result result = await _chatService.SendMessageAsync(sendMessage); + if(result.IsSuccess == false) + throw result.Exception; + + // Sending a message to the sender and recipient + await Clients.Group(sendMessage.RecipientId.ToString()).SendAsync("Receive", sendMessage); + await Clients.Group(sendMessage.FromUserId.ToString()).SendAsync("Receive", sendMessage); + } + private Guid GetUserId() { var userIdClaim = Context.User?.FindFirst("userID")?.Value; diff --git a/Govor.Application/Interfaces/Messages/IChatService.cs b/Govor.Application/Interfaces/Messages/IChatService.cs new file mode 100644 index 0000000..e44dc9b --- /dev/null +++ b/Govor.Application/Interfaces/Messages/IChatService.cs @@ -0,0 +1,8 @@ +namespace Govor.Application.Interfaces.Messages; + +public interface IChatService : IMessageSendingService, IMessageManagementService +{ + +} + + diff --git a/Govor.Application/Interfaces/Messages/IMessageManagementService.cs b/Govor.Application/Interfaces/Messages/IMessageManagementService.cs new file mode 100644 index 0000000..9b35c43 --- /dev/null +++ b/Govor.Application/Interfaces/Messages/IMessageManagementService.cs @@ -0,0 +1,9 @@ +using Govor.Application.Interfaces.Messages.Parameters; + +namespace Govor.Application.Interfaces.Messages; + +public interface IMessageManagementService +{ + Task EditMessageAsync(Guid editorId, Guid messageId, string newContent); + Task DeleteMessageAsync(Guid editorId, Guid messageId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageReactionService.cs b/Govor.Application/Interfaces/Messages/IMessageReactionService.cs new file mode 100644 index 0000000..c8b624a --- /dev/null +++ b/Govor.Application/Interfaces/Messages/IMessageReactionService.cs @@ -0,0 +1,6 @@ +namespace Govor.Application.Interfaces.Messages; + +public interface IMessageReactionService +{ + +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageSendingService.cs b/Govor.Application/Interfaces/Messages/IMessageSendingService.cs new file mode 100644 index 0000000..1bee95f --- /dev/null +++ b/Govor.Application/Interfaces/Messages/IMessageSendingService.cs @@ -0,0 +1,8 @@ +using Govor.Application.Interfaces.Messages.Parameters; + +namespace Govor.Application.Interfaces.Messages; + +public interface IMessageSendingService +{ + Task SendMessageAsync(SendMessage newMessage); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/Result.cs b/Govor.Application/Interfaces/Messages/Parameters/Result.cs new file mode 100644 index 0000000..c07618e --- /dev/null +++ b/Govor.Application/Interfaces/Messages/Parameters/Result.cs @@ -0,0 +1,3 @@ +namespace Govor.Application.Interfaces.Messages.Parameters; + +public record Result(bool IsSuccess, Exception Exception); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs b/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs new file mode 100644 index 0000000..06a002d --- /dev/null +++ b/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs @@ -0,0 +1,8 @@ +using Govor.Core.Models; + +namespace Govor.Application.Interfaces.Messages.Parameters; + +public record SendMedia(Guid Id, + string EncryptedKey, + MediaType Type, + string MimeType); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs b/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs new file mode 100644 index 0000000..23c7cc8 --- /dev/null +++ b/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs @@ -0,0 +1,9 @@ +namespace Govor.Application.Interfaces.Messages.Parameters; + +public record SendMessage( + string EncryptContent, + Guid? ReplyToMessageId, + Guid RecipientId, + Guid FromUserId, + DateTime SendAt, + IEnumerable Media); \ No newline at end of file diff --git a/Govor.Application/Services/FriendsService.cs b/Govor.Application/Services/FriendsService.cs index b906207..c68f2bb 100644 --- a/Govor.Application/Services/FriendsService.cs +++ b/Govor.Application/Services/FriendsService.cs @@ -26,12 +26,8 @@ public class FriendsService : IFriendsService { all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query); - var friends = await _friendshipsRepository.FindByUserIdAsync(currentId); - - friends = friends.Where(f => f.Status == FriendshipStatus.Accepted).ToList(); - return all - .Where(u => u.Id != currentId && !friends.Select(f => f.RequesterId).Contains(u.Id)) + .Where(u => u.Id != currentId) .ToList(); } catch (NotFoundByKeyException<(string, Guid)> ex) @@ -94,7 +90,6 @@ public class FriendsService : IFriendsService var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); return friendships - .Where(f => f.Status == FriendshipStatus.Accepted) .Select(f => f.RequesterId == userId ? f.Addressee : f.Requester) .ToList(); } diff --git a/Govor.Application/Services/PrivateChatService.cs b/Govor.Application/Services/PrivateChatService.cs new file mode 100644 index 0000000..f18dad5 --- /dev/null +++ b/Govor.Application/Services/PrivateChatService.cs @@ -0,0 +1,68 @@ +using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Messages; +using Govor.Application.Interfaces.Messages.Parameters; +using Govor.Core.Models; +using Govor.Core.Repositories.Messages; +using Govor.Data; +using Microsoft.EntityFrameworkCore; + +namespace Govor.Application.Services; + +public class PrivateChatService : IChatService +{ + private readonly IVerifyFriendship _verifyFriendship; + private readonly IMessagesRepository _messages; + + public PrivateChatService(IMessagesRepository messages, IVerifyFriendship verifyFriendship) + { + _messages = messages; + _verifyFriendship = verifyFriendship; + } + + public async Task SendMessageAsync(SendMessage newMessage) + { + try + { + await _verifyFriendship.VerifyAsync(newMessage.FromUserId, newMessage.RecipientId); + + var messageId = Guid.NewGuid(); + + var message = new Message() + { + Id = messageId, + EncryptedContent = newMessage.EncryptContent, + IsEdited = false, + SenderId = newMessage.FromUserId, + RecipientId = newMessage.RecipientId, + RecipientType = RecipientType.User, + ReplyToMessageId = newMessage.ReplyToMessageId, + + MediaAttachments = newMessage.Media.Select(m => new MediaAttachments() + { + Id = m.Id, + MessageId = messageId, + Type = m.Type, + MimeType = m.MimeType, + EncryptedKey = m.EncryptedKey, + }).ToList() + }; + + await _messages.AddAsync(message); + return new Result(true, null); + } + catch (Exception ex) + { + return new Result(false, ex); + } + } + + public Task EditMessageAsync(Guid editorId, Guid messageId, string newContent) + { + throw new NotImplementedException(); + } + + public Task DeleteMessageAsync(Guid editorId, Guid messageId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.Contracts/Requests/SignalR/MessageRequest.cs b/Govor.Contracts/Requests/SignalR/MessageRequest.cs index 477b68c..ec1417f 100644 --- a/Govor.Contracts/Requests/SignalR/MessageRequest.cs +++ b/Govor.Contracts/Requests/SignalR/MessageRequest.cs @@ -1,8 +1,11 @@ +using Govor.Core.Models; + namespace Govor.Contracts.Requests.SignalR; public record MessageRequest { public Guid RecipientId { get; init; } + public RecipientType RecipientType { get; set; } public string EncryptedContent { get; init; } = string.Empty; public Guid? ReplyToMessageId { get; set; } public List MediaAttachments { get; set; } = new(); diff --git a/Govor.Data/Repositories/FriendshipsRepository.cs b/Govor.Data/Repositories/FriendshipsRepository.cs index f2a0d7a..7ccf49b 100644 --- a/Govor.Data/Repositories/FriendshipsRepository.cs +++ b/Govor.Data/Repositories/FriendshipsRepository.cs @@ -46,7 +46,8 @@ public class FriendshipsRepository : IFriendshipsRepository .Include(x => x.Requester) .Include(x => x.Addressee) .AsSplitQuery() - .Where(x => x.RequesterId == userId) + .Where(f => + (f.RequesterId == userId || f.AddresseeId == userId)) .ToListOrThrowIfEmpty(new NotFoundByKeyException(userId, "Friendship with given user id was not found")); } @@ -113,6 +114,7 @@ public class FriendshipsRepository : IFriendshipsRepository public bool Exist(Guid requesterId, Guid addresseeId) { - return _context.Friendships.AsNoTracking().Any(x => (x.RequesterId == requesterId && x.AddresseeId == addresseeId)); + return _context.Friendships.AsNoTracking().Any(x => (x.RequesterId == requesterId && x.AddresseeId == addresseeId) || + x.RequesterId == addresseeId && x.AddresseeId == requesterId); } } \ No newline at end of file diff --git a/Govor.Data/Repositories/UsersRepository.cs b/Govor.Data/Repositories/UsersRepository.cs index 3cd8dfb..b08bbb9 100644 --- a/Govor.Data/Repositories/UsersRepository.cs +++ b/Govor.Data/Repositories/UsersRepository.cs @@ -22,6 +22,10 @@ public class UsersRepository : IUsersRepository { return await _context.Users .AsNoTracking() + .Include(u => u.Invite) + .Include(u => u.ReceivedFriendRequests) + .Include(u => u.SentFriendRequests) + .AsSplitQuery() .ToListOrThrowIfEmpty(new NotFoundException("Users in Database not exists")); } @@ -48,6 +52,10 @@ public class UsersRepository : IUsersRepository return await _context.Users .AsNoTracking() .Where(x => ids.Contains(x.Id)) + .Include(u => u.Invite) + .Include(u => u.ReceivedFriendRequests) + .Include(u => u.SentFriendRequests) + .AsSplitQuery() .ToListOrThrowIfEmpty(new NotFoundByKeyException>(ids,"Users with given ids not found")); } @@ -70,6 +78,9 @@ public class UsersRepository : IUsersRepository { return await _context.Users .AsNoTracking() + .Include(u => u.Invite) + .Include(u => u.ReceivedFriendRequests) + .Include(u => u.SentFriendRequests) .AsSplitQuery() .Where(u => u.Id != currentUserId && u.Username.ToLower().Contains(query.ToLower()) && @@ -211,5 +222,4 @@ public class UsersRepository : IUsersRepository { return _context.Users.AnyAsync(u => u.Username == username); } - } \ No newline at end of file