From b1f3aa0266018dc33bd2996a4a7cfad226393f33 Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:28:04 +0700 Subject: [PATCH] rework and starts for new friends system --- .../IntegrationTests/Hubs/ChatsHubTests.cs | 12 +- .../FriendRequestServiceTests.cs} | 216 ++++++++---------- .../Friends/FriendshipServiceTests.cs | 157 +++++++++++++ .../Services/PingHandlerServiceTests.cs | 2 +- .../AdminStuff/FriendshipsController.cs | 1 - Govor.API/Controllers/ChatLoadController.cs | 16 ++ Govor.API/Controllers/FriendsController.cs | 49 +++- .../ConfigurationProgramExtensions.cs | 11 +- Govor.API/Govor.API.csproj | 4 + Govor.API/Hubs/ChatsHub.cs | 3 +- Govor.API/Hubs/FriendsHub.cs | 38 +++ Govor.API/Hubs/UsersHub.cs | 8 - .../Friends/IFriendRequestService.cs | 12 + .../Friends/IFriendsBlockService.cs | 7 + .../Interfaces/Friends/IFriendshipService.cs | 9 + .../Interfaces/IFriendsService.cs | 6 +- .../Interfaces/IMessagesLoader.cs | 9 + Govor.Application/Services/AuthService.cs | 4 +- .../FriendRequestService.cs} | 87 +++---- .../Services/Friends/FriendsBlockService.cs | 16 ++ .../Services/Friends/FriendshipService.cs | 64 ++++++ Govor.Application/Services/MessagesLoader.cs | 43 ++++ Govor.Console/Program.cs | 2 +- Govor.Contracts/DTOs/FriendshipDto.cs | 1 - Govor.Core/Models/GroupMembership.cs | 2 +- .../Groups/IGroupMessagesReader.cs | 2 +- ... 20250707142430_InitialCreate.Designer.cs} | 174 +++++++++++--- ...ate.cs => 20250707142430_InitialCreate.cs} | 210 ++++++++++++----- .../Migrations/GovorDbContextModelSnapshot.cs | 172 +++++++++++--- 29 files changed, 1028 insertions(+), 309 deletions(-) rename Govor.API.Tests/UnitTests/Services/{FriendsServiceTests.cs => Friends/FriendRequestServiceTests.cs} (66%) create mode 100644 Govor.API.Tests/UnitTests/Services/Friends/FriendshipServiceTests.cs create mode 100644 Govor.API/Controllers/ChatLoadController.cs create mode 100644 Govor.API/Hubs/FriendsHub.cs delete mode 100644 Govor.API/Hubs/UsersHub.cs create mode 100644 Govor.Application/Interfaces/Friends/IFriendRequestService.cs create mode 100644 Govor.Application/Interfaces/Friends/IFriendsBlockService.cs create mode 100644 Govor.Application/Interfaces/Friends/IFriendshipService.cs create mode 100644 Govor.Application/Interfaces/IMessagesLoader.cs rename Govor.Application/Services/{FriendsService.cs => Friends/FriendRequestService.cs} (56%) create mode 100644 Govor.Application/Services/Friends/FriendsBlockService.cs create mode 100644 Govor.Application/Services/Friends/FriendshipService.cs create mode 100644 Govor.Application/Services/MessagesLoader.cs rename Govor.Data/Migrations/{20250630111826_InitialCreate.Designer.cs => 20250707142430_InitialCreate.Designer.cs} (74%) rename Govor.Data/Migrations/{20250630111826_InitialCreate.cs => 20250707142430_InitialCreate.cs} (74%) diff --git a/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs index 4f2ebe1..01d0682 100644 --- a/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs +++ b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs @@ -1,5 +1,6 @@ using AutoFixture; using Govor.API.Hubs; +using Govor.Application.Interfaces; using Govor.Application.Interfaces.Messages; using Microsoft.Extensions.Logging; using Moq; @@ -11,6 +12,7 @@ public class ChatsHubTests { private Mock> _loggerMock; private Mock _messageServiceMock; + private Mock _userGroupsServiceMock; private Fixture _fixture; private ChatsHub _chatsHub; @@ -22,14 +24,20 @@ public class ChatsHubTests _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); _messageServiceMock = new Mock(); + _userGroupsServiceMock = new Mock(); _loggerMock = new Mock>(); _chatsHub = new ChatsHub( _loggerMock.Object, - _messageServiceMock.Object + _messageServiceMock.Object, + _userGroupsServiceMock.Object ); } // Test for Send action - + [Test] + public void SendMessage_Success() + { + + } } \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Services/FriendsServiceTests.cs b/Govor.API.Tests/UnitTests/Services/Friends/FriendRequestServiceTests.cs similarity index 66% rename from Govor.API.Tests/UnitTests/Services/FriendsServiceTests.cs rename to Govor.API.Tests/UnitTests/Services/Friends/FriendRequestServiceTests.cs index 79f6a6f..9f3f7a4 100644 --- a/Govor.API.Tests/UnitTests/Services/FriendsServiceTests.cs +++ b/Govor.API.Tests/UnitTests/Services/Friends/FriendRequestServiceTests.cs @@ -1,22 +1,22 @@ using AutoFixture; using Govor.Application.Exceptions.FriendsService; -using Govor.Application.Interfaces; -using Govor.Application.Services; +using Govor.Application.Interfaces.Friends; +using Govor.Application.Services.Friends; using Govor.Core.Models; using Govor.Core.Repositories.Friendships; using Govor.Core.Repositories.Users; using Govor.Data.Repositories.Exceptions; using Moq; -namespace Govor.API.Tests.UnitTests.Services; +namespace Govor.API.Tests.UnitTests.Services.Friends; [TestFixture] -public class FriendsServiceTests +public class FriendRequestServiceTests { private Fixture _fixture; private Mock _usersRepositoryMock; private Mock _friendshipsRepositoryMock; - private IFriendsService _service; + private IFriendRequestService _service; [SetUp] public void SetUp() @@ -31,96 +31,10 @@ public class FriendsServiceTests _usersRepositoryMock = new Mock(); _friendshipsRepositoryMock = new Mock(); - _service = new FriendsService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object); - } - - // SearchUsersAsync - [Test] - public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends() - { - // Arrange - var userId = _fixture.Create(); - var user = _fixture.Create(); - user.Id = Guid.NewGuid(); - - _usersRepositoryMock - .Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny())) - .ReturnsAsync(new List { user }); - - _friendshipsRepositoryMock - .Setup(f => f.FindByUserIdAsync(userId)) - .ReturnsAsync(new List()); // No friends - - // Act - var result = await _service.SearchUsersAsync("test", userId); - - // Assert - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result[0].Id, Is.EqualTo(user.Id)); - } - - [Test] - public void SearchUsersAsync_Throws_SearchUsersException_IfThrowsNotFoundByKeyException_String_Guid() - { - // Arrange - _usersRepositoryMock - .Setup(u => u.SearchPotentialFriendsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test")); - - // Axt & Assert - Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); - } - - [Test] - public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid() - { - // Arrange - var userId = _fixture.Create(); - var user = _fixture.Create(); - user.Id = Guid.NewGuid(); - - _usersRepositoryMock - .Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny())) - .ReturnsAsync(new List { user }); - - _friendshipsRepositoryMock - .Setup(f => f.FindByUserIdAsync(userId)) - .ThrowsAsync(new NotFoundByKeyException(userId, "test")); - - // Act - var result = await _service.SearchUsersAsync("test", userId); - - // Assert - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result[0].Id, Is.EqualTo(user.Id)); - } - - [Test] - public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInUsersRepository() - { - // Arrange - _usersRepositoryMock - .Setup(u => u.SearchPotentialFriendsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("test")); - - // Act & Assert - Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); - } - - [Test] - public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository() - { - // Arrange - _friendshipsRepositoryMock - .Setup(u => u.FindByUserIdAsync(It.IsAny())) - .ThrowsAsync(new Exception("test")); - - // Act & Assert - Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); + _service = new FriendRequestService(_friendshipsRepositoryMock.Object); } // SendFriendRequestAsync - [Test] public void SendFriendRequestAsync_SendingToSelf_ThrowsInvalidOperationException() { @@ -270,45 +184,104 @@ public class FriendsServiceTests Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Accepted)); } - - // GetFriendsAsync + + // RejectFriend [Test] - public async Task GetFriendsAsync_ReturnsUsers_IfUserExists() + public async Task RejectFriendRequestAsync_ValidRequest_CallsRemoveAsync() { - // Arrange - var userId = Guid.NewGuid(); - var friendships = _fixture.CreateMany().ToList(); + var friendship = _fixture.Build() + .With(f => f.Status, FriendshipStatus.Pending) + .Create(); + + _friendshipsRepositoryMock + .Setup(r => r.GetByIdAsync(friendship.Id)) + .ReturnsAsync(friendship); - friendships.ForEach(f => - { - f.RequesterId = userId; - f.Status = FriendshipStatus.Accepted; - }); + await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId); + + // Assert + _friendshipsRepositoryMock.Verify(r => r.UpdateAsync(It.Is(f => + f.Id == friendship.Id && + f.RequesterId == friendship.RequesterId && + f.AddresseeId == friendship.AddresseeId && + f.Status == FriendshipStatus.Rejected + )), Times.Once); + } + + [Test] + public void RejectFriendRequestAsync_Throws_InvalidOperationException_IfStatusNotEqualPendingOrReject() + { + var friendship = _fixture.Build() + .With(f => f.Status, FriendshipStatus.Accepted) + .Create(); - _friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId)) - .ReturnsAsync(friendships); - - // Act - var result = await _service.GetFriendsAsync(userId); - - // Assert - Assert.That(result.Count, Is.EqualTo(friendships.Count)); - Assert.That(result, Is.EquivalentTo(friendships.Select(f => f.Addressee))); + _friendshipsRepositoryMock + .Setup(r => r.GetByIdAsync(friendship.Id)) + .ReturnsAsync(friendship); + + // Act & Assert + Assert.ThrowsAsync(async () => await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId)); } [Test] - public void GetFriendsAsync_Throws_InvalidOperationException_IfUserNotExists() + public void RejectFriendRequestAsync_Throws_UnauthorizedAccessException_IfCurrentUserIdIsNotAddressee() { - // Arrange - _friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(It.IsAny())) - .ThrowsAsync(new NotFoundByKeyException(Guid.NewGuid())); - - // Act & Assert - Assert.ThrowsAsync(async () => await _service.GetFriendsAsync(Guid.NewGuid())); + // Arrange + var friendship = _fixture.Build() + .With(f => f.Status, FriendshipStatus.Pending) + .Create(); + + _friendshipsRepositoryMock + .Setup(r => r.GetByIdAsync(friendship.Id)) + .ReturnsAsync(friendship); + + var wrongUserId = Guid.NewGuid(); // не совпадает с AddresseeId + + // Act & Assert + Assert.ThrowsAsync(async () => + await _service.RejectFriendRequestAsync(friendship.Id, wrongUserId)); + } + + [Test] + public void RejectFriendRequestAsync_Throws_InvalidOperationException_IfFriendshipNotFound() + { + // Arrange + var requestId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + + _friendshipsRepositoryMock + .Setup(r => r.GetByIdAsync(requestId)) + .ThrowsAsync(new NotFoundByKeyException(requestId)); + + // Act & Assert + Assert.ThrowsAsync(async () => + await _service.RejectFriendRequestAsync(requestId, userId)); + } + + [Test] + public async Task RejectFriendRequestAsync_ChangesStatusToAccepted() + { + var friendship = _fixture.Build() + .With(f => f.Status, FriendshipStatus.Pending) + .Create(); + + _friendshipsRepositoryMock + .Setup(r => r.GetByIdAsync(friendship.Id)) + .ReturnsAsync(friendship); + + Friendship updatedFriendship = null; + + _friendshipsRepositoryMock + .Setup(r => r.UpdateAsync(It.IsAny())) + .Callback(f => updatedFriendship = f) + .Returns(Task.CompletedTask); + + await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId); + + Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Rejected)); } // GetIncomingRequestsAsync - [Test] public async Task GetIncomingRequestsAsync_ReturnsFriendships_IfFriendshipsExists() { @@ -354,5 +327,4 @@ public class FriendsServiceTests Assert.ThrowsAsync(async () => await _service.GetIncomingRequestsAsync(userId)); } - -} +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Services/Friends/FriendshipServiceTests.cs b/Govor.API.Tests/UnitTests/Services/Friends/FriendshipServiceTests.cs new file mode 100644 index 0000000..abb9205 --- /dev/null +++ b/Govor.API.Tests/UnitTests/Services/Friends/FriendshipServiceTests.cs @@ -0,0 +1,157 @@ +using AutoFixture; +using Govor.Application.Exceptions.FriendsService; +using Govor.Application.Interfaces.Friends; +using Govor.Application.Services.Friends; +using Govor.Core.Models; +using Govor.Core.Repositories.Friendships; +using Govor.Core.Repositories.Users; +using Govor.Data.Repositories.Exceptions; +using Moq; + +namespace Govor.API.Tests.UnitTests.Services.Friends; + +[TestFixture] +public class FriendshipServiceTests +{ + private Fixture _fixture; + private Mock _usersRepositoryMock; + private Mock _friendshipsRepositoryMock; + IFriendshipService _service; + + [SetUp] + public void SetUp() + { + _fixture = new Fixture(); + _fixture.Behaviors + .OfType() + .ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _usersRepositoryMock = new Mock(); + _friendshipsRepositoryMock = new Mock(); + + _service = new FriendshipService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object); + } + + // SearchUsersAsync + [Test] + public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends() + { + // Arrange + var userId = _fixture.Create(); + var user = _fixture.Create(); + user.Id = Guid.NewGuid(); + + _usersRepositoryMock + .Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny())) + .ReturnsAsync(new List { user }); + + _friendshipsRepositoryMock + .Setup(f => f.FindByUserIdAsync(userId)) + .ReturnsAsync(new List()); // No friends + + // Act + var result = await _service.SearchUsersAsync("test", userId); + + // Assert + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0].Id, Is.EqualTo(user.Id)); + } + + [Test] + public void SearchUsersAsync_Throws_SearchUsersException_IfThrowsNotFoundByKeyException_String_Guid() + { + // Arrange + _usersRepositoryMock + .Setup(u => u.SearchPotentialFriendsAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test")); + + // Axt & Assert + Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); + } + + [Test] + public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid() + { + // Arrange + var userId = _fixture.Create(); + var user = _fixture.Create(); + user.Id = Guid.NewGuid(); + + _usersRepositoryMock + .Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny())) + .ReturnsAsync(new List { user }); + + _friendshipsRepositoryMock + .Setup(f => f.FindByUserIdAsync(userId)) + .ThrowsAsync(new NotFoundByKeyException(userId, "test")); + + // Act + var result = await _service.SearchUsersAsync("test", userId); + + // Assert + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0].Id, Is.EqualTo(user.Id)); + } + + [Test] + public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInUsersRepository() + { + // Arrange + _usersRepositoryMock + .Setup(u => u.SearchPotentialFriendsAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("test")); + + // Act & Assert + Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); + } + + [Test] + public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository() + { + // Arrange + _friendshipsRepositoryMock + .Setup(u => u.FindByUserIdAsync(It.IsAny())) + .ThrowsAsync(new Exception("test")); + + // Act & Assert + Assert.ThrowsAsync(async () => await _service.SearchUsersAsync("test", Guid.NewGuid())); + } + + // GetFriendsAsync + [Test] + public async Task GetFriendsAsync_ReturnsUsers_IfUserExists() + { + // Arrange + var userId = Guid.NewGuid(); + var friendships = _fixture.CreateMany().ToList(); + + friendships.ForEach(f => + { + f.RequesterId = userId; + f.Status = FriendshipStatus.Accepted; + }); + + _friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId)) + .ReturnsAsync(friendships); + + // Act + var result = await _service.GetFriendsAsync(userId); + + // Assert + Assert.That(result.Count, Is.EqualTo(friendships.Count)); + Assert.That(result, Is.EquivalentTo(friendships.Select(f => f.Addressee))); + } + + [Test] + public void GetFriendsAsync_Throws_InvalidOperationException_IfUserNotExists() + { + // Arrange + _friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(It.IsAny())) + .ThrowsAsync(new NotFoundByKeyException(Guid.NewGuid())); + + // Act & Assert + Assert.ThrowsAsync(async () => await _service.GetFriendsAsync(Guid.NewGuid())); + } +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Services/PingHandlerServiceTests.cs b/Govor.API.Tests/UnitTests/Services/PingHandlerServiceTests.cs index 4510910..312342b 100644 --- a/Govor.API.Tests/UnitTests/Services/PingHandlerServiceTests.cs +++ b/Govor.API.Tests/UnitTests/Services/PingHandlerServiceTests.cs @@ -4,7 +4,7 @@ using Govor.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -namespace Govor.Tests.Application.Services; +namespace Govor.API.Tests.UnitTests.Services; [TestFixture] public class PingHandlerServiceTests diff --git a/Govor.API/Controllers/AdminStuff/FriendshipsController.cs b/Govor.API/Controllers/AdminStuff/FriendshipsController.cs index 94160e0..a124dbf 100644 --- a/Govor.API/Controllers/AdminStuff/FriendshipsController.cs +++ b/Govor.API/Controllers/AdminStuff/FriendshipsController.cs @@ -104,6 +104,5 @@ public class FriendshipsController : Controller Status = f.Status, AddresseeId = f.AddresseeId, RequesterId = f.RequesterId, - Requester = BuildUserDtos([f.Requester]).First(), }).ToList(); } \ No newline at end of file diff --git a/Govor.API/Controllers/ChatLoadController.cs b/Govor.API/Controllers/ChatLoadController.cs new file mode 100644 index 0000000..fd9a537 --- /dev/null +++ b/Govor.API/Controllers/ChatLoadController.cs @@ -0,0 +1,16 @@ +using Govor.Application.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Govor.API.Controllers; + +[ApiController] +[Authorize] +[Route("api/chats")] +public class ChatLoadController : Controller +{ + public ChatLoadController(ILogger logger, IMessagesLoader messagesLoader) + { + + } +} \ No newline at end of file diff --git a/Govor.API/Controllers/FriendsController.cs b/Govor.API/Controllers/FriendsController.cs index cfa6295..c333998 100644 --- a/Govor.API/Controllers/FriendsController.cs +++ b/Govor.API/Controllers/FriendsController.cs @@ -97,6 +97,26 @@ public class FriendsController : Controller } } + [HttpGet("responses")] + public async Task GetResponses() + { + try + { + var result = await _friendsService.GetResponsesAsync(_currentUserService.GetCurrentUserId()); + return Ok(BuildFriendshipDtos(result)); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, ex.Message); + return Ok(Array.Empty()); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { error = "Internal server error." }); + } + } + [HttpPost("accept")] public async Task AcceptFriend([FromQuery] Guid friendshipId) { @@ -125,6 +145,34 @@ public class FriendsController : Controller } } + [HttpPost("reject")] + public async Task RejectFriend([FromQuery] Guid friendshipId) + { + if (friendshipId == Guid.Empty) + return BadRequest("Requester ID is invalid"); + + try + { + await _friendsService.RejectFriendRequestAsync(friendshipId, _currentUserService.GetCurrentUserId()); + return Ok(new { message = "Friend request rejected." }); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, ex.Message); + return NotFound(new { error = ex.Message }); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, ex.Message); + return Forbid(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { error = "Failed to accept friend request." }); + } + } + [HttpGet] public async Task GetFriends() { @@ -160,6 +208,5 @@ public class FriendsController : Controller Status = f.Status, AddresseeId = f.AddresseeId, RequesterId = f.RequesterId, - Requester = BuildUserDtos([f.Requester]).First(), }).ToList(); } diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index d9d49f8..fc394e4 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -5,9 +5,11 @@ using Govor.Application.Infrastructure.Validators; using Govor.Application.Interfaces; using Govor.Application.Interfaces.AdminsStuff; using Govor.Application.Interfaces.Authentication; +using Govor.Application.Interfaces.Friends; using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Application.Interfaces.Messages; using Govor.Application.Services; +using Govor.Application.Services.Friends; using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; @@ -36,7 +38,11 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + + // Friends services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(sp => { @@ -53,6 +59,7 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public static void AddRepositories(this IServiceCollection services) @@ -65,6 +72,8 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // other } public static void AddValidators(this IServiceCollection services) diff --git a/Govor.API/Govor.API.csproj b/Govor.API/Govor.API.csproj index b88211f..3614dc4 100644 --- a/Govor.API/Govor.API.csproj +++ b/Govor.API/Govor.API.csproj @@ -23,5 +23,9 @@ + + + + diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 792c7f4..0deabc4 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -17,10 +17,11 @@ public class ChatsHub : Hub private readonly IMessageService _messageService; private readonly IUserGroupsService _userService; - public ChatsHub(ILogger logger, IMessageService messageService) + public ChatsHub(ILogger logger, IMessageService messageService, IUserGroupsService userService) { _logger = logger; _messageService = messageService; + _userService = userService; } public override async Task OnConnectedAsync() diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs new file mode 100644 index 0000000..d1a9b30 --- /dev/null +++ b/Govor.API/Hubs/FriendsHub.cs @@ -0,0 +1,38 @@ +using Govor.Application.Interfaces.Friends; +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Microsoft.AspNetCore.SignalR; + +namespace Govor.API.Hubs; + +public class FriendsHub : Hub +{ + private readonly IFriendRequestService _friendRequestService; + private readonly ICurrentUserService _currentUserService; + + public FriendsHub(IFriendRequestService friendRequestService, ICurrentUserService currentUserService) + { + _friendRequestService = friendRequestService; + _currentUserService = currentUserService; + } + + public async Task SendRequest(Guid targetUserId) + { + var userId = _currentUserService.GetCurrentUserId(); + await _friendRequestService.SendFriendRequestAsync(userId, targetUserId); + await Clients.User(targetUserId.ToString()).SendAsync("FriendRequestReceived", userId); + } + + public async Task AcceptRequest(Guid friendshipId) + { + var userId = _currentUserService.GetCurrentUserId(); + await _friendRequestService.AcceptFriendRequestAsync(friendshipId, userId); + await Clients.User(userId.ToString()).SendAsync("FriendRequestAccepted", friendshipId); + } + + public async Task RejectRequest(Guid friendshipId) + { + var userId = _currentUserService.GetCurrentUserId(); + await _friendRequestService.RejectFriendRequestAsync(friendshipId, userId); + await Clients.User(userId.ToString()).SendAsync("FriendRequestRejected", friendshipId); + } +} \ No newline at end of file diff --git a/Govor.API/Hubs/UsersHub.cs b/Govor.API/Hubs/UsersHub.cs deleted file mode 100644 index 880aba6..0000000 --- a/Govor.API/Hubs/UsersHub.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.AspNetCore.SignalR; - -namespace Govor.API.Hubs; - -public class UsersHub : Hub -{ - -} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Friends/IFriendRequestService.cs b/Govor.Application/Interfaces/Friends/IFriendRequestService.cs new file mode 100644 index 0000000..d6d9e37 --- /dev/null +++ b/Govor.Application/Interfaces/Friends/IFriendRequestService.cs @@ -0,0 +1,12 @@ +using Govor.Core.Models; + +namespace Govor.Application.Interfaces.Friends; + +public interface IFriendRequestService +{ + Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId); + Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId); + Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId); + Task> GetIncomingRequestsAsync(Guid userId); + Task> GetResponsesAsync(Guid userId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Friends/IFriendsBlockService.cs b/Govor.Application/Interfaces/Friends/IFriendsBlockService.cs new file mode 100644 index 0000000..2d69160 --- /dev/null +++ b/Govor.Application/Interfaces/Friends/IFriendsBlockService.cs @@ -0,0 +1,7 @@ +namespace Govor.Application.Interfaces; + +public interface IFriendsBlockService +{ + Task BlockFriendRequestAsync(Guid userId, Guid currentUserId); + Task UnblockFriendRequestAsync(Guid userId, Guid currentUserId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Friends/IFriendshipService.cs b/Govor.Application/Interfaces/Friends/IFriendshipService.cs new file mode 100644 index 0000000..c791143 --- /dev/null +++ b/Govor.Application/Interfaces/Friends/IFriendshipService.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Application.Interfaces.Friends; + +public interface IFriendshipService +{ + Task> GetFriendsAsync(Guid userId); + Task> SearchUsersAsync(string query, Guid currentId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/IFriendsService.cs b/Govor.Application/Interfaces/IFriendsService.cs index d234395..9cd4f71 100644 --- a/Govor.Application/Interfaces/IFriendsService.cs +++ b/Govor.Application/Interfaces/IFriendsService.cs @@ -7,6 +7,10 @@ public interface IFriendsService Task> SearchUsersAsync(string query, Guid currentId); Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId); Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId); + Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId); Task> GetFriendsAsync(Guid userId); + Task> GetResponsesAsync(Guid userId); Task> GetIncomingRequestsAsync(Guid userId); -} \ No newline at end of file +} + + diff --git a/Govor.Application/Interfaces/IMessagesLoader.cs b/Govor.Application/Interfaces/IMessagesLoader.cs new file mode 100644 index 0000000..1cc3334 --- /dev/null +++ b/Govor.Application/Interfaces/IMessagesLoader.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Application.Interfaces; + +public interface IMessagesLoader +{ + Task> LoadLastMessagesInUserChat(Guid userId,Guid currentId, Guid? startMessageId, int pageSize = 20); + Task> LoadLastMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int pageSize = 20); +} \ No newline at end of file diff --git a/Govor.Application/Services/AuthService.cs b/Govor.Application/Services/AuthService.cs index e5682c2..f16bdb8 100644 --- a/Govor.Application/Services/AuthService.cs +++ b/Govor.Application/Services/AuthService.cs @@ -55,7 +55,7 @@ public class AuthService : IAccountService await _usersRepository.AddAsync(user); - SetRole(user, invitation); + await SetRole(user, invitation); return await _jwtService.GenerateJwtTokenAsync(user); } @@ -74,7 +74,7 @@ public class AuthService : IAccountService return await _jwtService.GenerateJwtTokenAsync(user); } - private async void SetRole(User user, Invitation invitation) + private async Task SetRole(User user, Invitation invitation) { if(invitation.IsAdmin) await _adminsRepository.AddAsync(new Admin() { UserId = user.Id }); diff --git a/Govor.Application/Services/FriendsService.cs b/Govor.Application/Services/Friends/FriendRequestService.cs similarity index 56% rename from Govor.Application/Services/FriendsService.cs rename to Govor.Application/Services/Friends/FriendRequestService.cs index 45b0de8..f174074 100644 --- a/Govor.Application/Services/FriendsService.cs +++ b/Govor.Application/Services/Friends/FriendRequestService.cs @@ -1,59 +1,29 @@ using Govor.Application.Exceptions.FriendsService; -using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Friends; using Govor.Core.Models; using Govor.Core.Repositories.Friendships; -using Govor.Core.Repositories.Users; using Govor.Data.Repositories.Exceptions; -namespace Govor.Application.Services; +namespace Govor.Application.Services.Friends; -public class FriendsService : IFriendsService +public class FriendRequestService : IFriendRequestService { - private IUsersRepository _usersRepository; - private IFriendshipsRepository _friendshipsRepository; - - public FriendsService(IUsersRepository usersRepository, IFriendshipsRepository relationshipsRepository) - { - _usersRepository = usersRepository; - _friendshipsRepository = relationshipsRepository; - } - - public async Task> SearchUsersAsync(string query, Guid currentId) - { - List all = new List(); - - try - { - all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query); + private readonly IFriendshipsRepository _friendshipsRepository; - return all - .Where(u => u.Id != currentId) - .ToList(); - } - catch (NotFoundByKeyException<(string, Guid)> ex) - { - throw new SearchUsersException( - $"Users with given query: \"{query}\" for user with id {currentId} was not found", ex); - } - catch (NotFoundByKeyException ex) - { - return all.Where(u => u.Id != currentId).ToList(); - } - catch (Exception ex) - { - throw new UnauthorizedAccessException($"When we try find friends by pattern {query} something wrong", ex); - } + public FriendRequestService(IFriendshipsRepository friendshipsRepository) + { + _friendshipsRepository = friendshipsRepository; } public async Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId) { if (fromUserId == toUserId) throw new InvalidOperationException("Cannot send a request to self user"); - + if (_friendshipsRepository.Exist(fromUserId, toUserId)) throw new RequestAlreadySentException(fromUserId, toUserId); - - await _friendshipsRepository.AddAsync(new Friendship() + + await _friendshipsRepository.AddAsync(new Friendship { Id = Guid.NewGuid(), RequesterId = fromUserId, @@ -77,26 +47,30 @@ public class FriendsService : IFriendsService friendship.Status = FriendshipStatus.Accepted; await _friendshipsRepository.UpdateAsync(friendship); } - catch (NotFoundByKeyException e) + catch (NotFoundByKeyException ex) { - throw new InvalidOperationException("Friendship not found! You cant accept request!", e); + throw new InvalidOperationException("Friendship not found! You cant accept request!", ex); } } - public async Task> GetFriendsAsync(Guid userId) + public async Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId) { try { - var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); + var friendship = await _friendshipsRepository.GetByIdAsync(requestId); - return friendships - .Where(f => f.Status == FriendshipStatus.Accepted) - .Select(f => f.RequesterId == userId ? f.Addressee : f.Requester) - .ToList(); + if (friendship.AddresseeId != currentUserId) + throw new UnauthorizedAccessException("You cannot accept this request"); + + if (friendship.Status != FriendshipStatus.Pending && friendship.Status != FriendshipStatus.Rejected) + throw new InvalidOperationException($"Request is already {friendship.Status}"); + + friendship.Status = FriendshipStatus.Rejected; + await _friendshipsRepository.UpdateAsync(friendship); } catch (NotFoundByKeyException ex) { - throw new InvalidOperationException("User not found", ex); + throw new InvalidOperationException("Friendship not found! You cant reject request!", ex); } } @@ -113,5 +87,18 @@ public class FriendsService : IFriendsService throw new InvalidOperationException("User not exist", ex); } } -} + public async Task> GetResponsesAsync(Guid userId) + { + try + { + var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); + return friendships.Where(f => f.RequesterId == userId && f.Status != FriendshipStatus.Accepted).ToList() + ?? new List(); + } + catch (NotFoundByKeyException ex) + { + throw new InvalidOperationException("User not exist", ex); + } + } +} \ No newline at end of file diff --git a/Govor.Application/Services/Friends/FriendsBlockService.cs b/Govor.Application/Services/Friends/FriendsBlockService.cs new file mode 100644 index 0000000..991f760 --- /dev/null +++ b/Govor.Application/Services/Friends/FriendsBlockService.cs @@ -0,0 +1,16 @@ +using Govor.Application.Interfaces; + +namespace Govor.Application.Services.Friends; + +public class FriendsBlockService : IFriendsBlockService +{ + public Task BlockFriendRequestAsync(Guid userId, Guid currentUserId) + { + throw new NotImplementedException(); + } + + public Task UnblockFriendRequestAsync(Guid userId, Guid currentUserId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/Friends/FriendshipService.cs b/Govor.Application/Services/Friends/FriendshipService.cs new file mode 100644 index 0000000..522b2f5 --- /dev/null +++ b/Govor.Application/Services/Friends/FriendshipService.cs @@ -0,0 +1,64 @@ +using Govor.Application.Exceptions.FriendsService; +using Govor.Application.Interfaces.Friends; +using Govor.Core.Models; +using Govor.Core.Repositories.Friendships; +using Govor.Core.Repositories.Users; +using Govor.Data.Repositories.Exceptions; + +namespace Govor.Application.Services.Friends; + +public class FriendshipService : IFriendshipService +{ + private readonly IUsersRepository _usersRepository; + private readonly IFriendshipsRepository _friendshipsRepository; + + public FriendshipService(IUsersRepository usersRepository, IFriendshipsRepository friendshipsRepository) + { + _usersRepository = usersRepository; + _friendshipsRepository = friendshipsRepository; + } + + public async Task> SearchUsersAsync(string query, Guid currentId) + { + List all = new List(); + + try + { + all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query); + + return all + .Where(u => u.Id != currentId) + .ToList(); + } + catch (NotFoundByKeyException<(string, Guid)> ex) + { + throw new SearchUsersException( + $"Users with given query: \"{query}\" for user with id {currentId} was not found", ex); + } + catch (NotFoundByKeyException ex) + { + return all.Where(u => u.Id != currentId).ToList(); + } + catch (Exception ex) + { + throw new UnauthorizedAccessException($"When we try find friends by pattern {query} something wrong", ex); + } + } + + public async Task> GetFriendsAsync(Guid userId) + { + try + { + var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); + + return friendships + .Where(f => f.Status == FriendshipStatus.Accepted) + .Select(f => f.RequesterId == userId ? f.Addressee : f.Requester) + .ToList(); + } + catch (NotFoundByKeyException ex) + { + throw new InvalidOperationException("User not found", ex); + } + } +} \ No newline at end of file diff --git a/Govor.Application/Services/MessagesLoader.cs b/Govor.Application/Services/MessagesLoader.cs new file mode 100644 index 0000000..406a974 --- /dev/null +++ b/Govor.Application/Services/MessagesLoader.cs @@ -0,0 +1,43 @@ +using Govor.Application.Interfaces; +using Govor.Core.Models; +using Govor.Core.Repositories.Groups; +using Govor.Core.Repositories.Messages; +using Govor.Data.Repositories.Exceptions; + +namespace Govor.Application.Services; + +public class MessagesLoader : IMessagesLoader +{ + private IVerifyFriendship _friendship; + private IGroupsRepository _groupsRepository; + private IMessagesRepository _messagesRepository; + + public async Task> LoadLastMessagesInUserChat(Guid userId, Guid currentUser, Guid? startMessageId, int pageSize = 20) + { + await _friendship.VerifyAsync(userId, currentUser); + try + { + throw new NotImplementedException(); + //return await _groups.GetMessages(userId, startMessageId, pageSize); + } + catch (NotFoundException ex) + { + return new List(); + } + } + + public async Task> LoadLastMessagesInChatGroup(Guid chatId, Guid currentUser, Guid? startMessageId, int pageSize = 20) + { + if(!await _groupsRepository.IsUserMemberOfGroupAsync(currentUser, chatId)) + throw new UnauthorizedAccessException("You are not in a group."); + try + { + throw new NotImplementedException(); + //return await _groups.GetMessages(chatId, startMessageId, pageSize, RecipientType.Group); + } + catch (NotFoundException ex) + { + return new List(); + } + } +} \ No newline at end of file diff --git a/Govor.Console/Program.cs b/Govor.Console/Program.cs index 2a622ef..3383d4b 100644 --- a/Govor.Console/Program.cs +++ b/Govor.Console/Program.cs @@ -181,7 +181,7 @@ namespace Govor.ConsoleClient var requests = await friendsClient.GetIncomingRequestsAsync(); foreach (var r in requests) { - Console.WriteLine($"Запрос от: {r.Requester.Username}| {r.RequesterId} | Был онлайн: {r.Requester.WasOnline} (добавьте через /accept {r.Id})"); + Console.WriteLine($"Запрос от: {r.RequesterId} (добавьте через /accept {r.Id})"); } break; case "/chat": diff --git a/Govor.Contracts/DTOs/FriendshipDto.cs b/Govor.Contracts/DTOs/FriendshipDto.cs index f2b6ca1..85065e1 100644 --- a/Govor.Contracts/DTOs/FriendshipDto.cs +++ b/Govor.Contracts/DTOs/FriendshipDto.cs @@ -8,5 +8,4 @@ public class FriendshipDto public Guid RequesterId { get; set; } public Guid AddresseeId { get; set; } public FriendshipStatus Status { get; set; } - public UserDto Requester { get; set; } } \ No newline at end of file diff --git a/Govor.Core/Models/GroupMembership.cs b/Govor.Core/Models/GroupMembership.cs index d70341e..866cc59 100644 --- a/Govor.Core/Models/GroupMembership.cs +++ b/Govor.Core/Models/GroupMembership.cs @@ -5,6 +5,6 @@ public class GroupMembership public Guid Id { get; set; } public Guid GroupId { get; set; } public Guid UserId { get; set; } - public Guid InvitationId { get; set; } + public Guid? InvitationId { get; set; } public bool IsBanned { get; set; } } \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs b/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs index 3f5d9ab..aed1ffc 100644 --- a/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs +++ b/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs @@ -4,5 +4,5 @@ namespace Govor.Core.Repositories.Groups; public interface IGroupMessagesReader { - public Task> GetMessages(Guid chatId, Guid? startMessageId, int pageSize = 20); + public Task> GetMessages(Guid chatId, Guid? startMessageId, int pageSize = 20, RecipientType type = RecipientType.User); } \ No newline at end of file diff --git a/Govor.Data/Migrations/20250630111826_InitialCreate.Designer.cs b/Govor.Data/Migrations/20250707142430_InitialCreate.Designer.cs similarity index 74% rename from Govor.Data/Migrations/20250630111826_InitialCreate.Designer.cs rename to Govor.Data/Migrations/20250707142430_InitialCreate.Designer.cs index fc022ce..219d0c6 100644 --- a/Govor.Data/Migrations/20250630111826_InitialCreate.Designer.cs +++ b/Govor.Data/Migrations/20250707142430_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Govor.Data.Migrations { [DbContext(typeof(GovorDbContext))] - [Migration("20250630111826_InitialCreate")] + [Migration("20250707142430_InitialCreate")] partial class InitialCreate { /// @@ -20,7 +20,7 @@ namespace Govor.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -41,13 +41,13 @@ namespace Govor.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); - b.PrimitiveCollection("Admins") + b.Property("Description") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(500) + .HasColumnType("varchar(500)"); - b.PrimitiveCollection("InviteCode") - .IsRequired() - .HasColumnType("longtext"); + b.Property("ImageId") + .HasColumnType("char(36)"); b.Property("IsChannel") .HasColumnType("tinyint(1)"); @@ -57,7 +57,8 @@ namespace Govor.Data.Migrations b.Property("Name") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.HasKey("Id"); @@ -102,9 +103,51 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("GroupId"); + b.ToTable("GroupAdmins"); }); + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.Property("UserMakerId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserMakerId"); + + b.ToTable("GroupInvitations"); + }); + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => { b.Property("Id") @@ -114,6 +157,10 @@ namespace Govor.Data.Migrations b.Property("GroupId") .HasColumnType("char(36)"); + b.Property("InvitationId") + .IsRequired() + .HasColumnType("char(36)"); + b.Property("IsBanned") .HasColumnType("tinyint(1)"); @@ -122,6 +169,10 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("GroupId"); + + b.HasIndex("InvitationId"); + b.ToTable("GroupMemberships"); }); @@ -165,30 +216,46 @@ namespace Govor.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); - b.Property("EncryptedKey") - .HasMaxLength(512) - .HasColumnType("varchar(512)"); - - b.Property("FilePath") - .IsRequired() - .HasColumnType("longtext"); + b.Property("MediaFileId") + .HasColumnType("char(36)"); b.Property("MessageId") .HasColumnType("char(36)"); - b.Property("MimeType") + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("MediaType") .IsRequired() .HasColumnType("longtext"); - b.Property("Type") + b.Property("MineType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Url") .IsRequired() .HasColumnType("longtext"); b.HasKey("Id"); - b.HasIndex("MessageId"); - - b.ToTable("MediaAttachments"); + b.ToTable("MediaFiles"); }); modelBuilder.Entity("Govor.Core.Models.Message", b => @@ -229,8 +296,6 @@ namespace Govor.Data.Migrations b.HasIndex("PrivateChatId"); - b.HasIndex("ReplyToMessageId"); - b.ToTable("Messages"); }); @@ -371,14 +436,63 @@ namespace Govor.Data.Migrations b.Navigation("Requester"); }); + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Admins") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("InviteCodes") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "UserMaker") + .WithMany() + .HasForeignKey("UserMakerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("UserMaker"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.GroupInvitation", null) + .WithMany() + .HasForeignKey("InvitationId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + }); + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => { + b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.HasOne("Govor.Core.Models.Message", "Message") .WithMany("MediaAttachments") .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("MediaFile"); + b.Navigation("Message"); }); @@ -387,13 +501,6 @@ namespace Govor.Data.Migrations b.HasOne("Govor.Core.Models.PrivateChat", null) .WithMany("Messages") .HasForeignKey("PrivateChatId"); - - b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") - .WithMany() - .HasForeignKey("ReplyToMessageId") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("ReplyToMessage"); }); modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => @@ -435,6 +542,15 @@ namespace Govor.Data.Migrations b.Navigation("Invite"); }); + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Navigation("Admins"); + + b.Navigation("InviteCodes"); + + b.Navigation("Members"); + }); + modelBuilder.Entity("Govor.Core.Models.Invitation", b => { b.Navigation("Users"); diff --git a/Govor.Data/Migrations/20250630111826_InitialCreate.cs b/Govor.Data/Migrations/20250707142430_InitialCreate.cs similarity index 74% rename from Govor.Data/Migrations/20250630111826_InitialCreate.cs rename to Govor.Data/Migrations/20250707142430_InitialCreate.cs index 9fe9cbd..081bcc6 100644 --- a/Govor.Data/Migrations/20250630111826_InitialCreate.cs +++ b/Govor.Data/Migrations/20250707142430_InitialCreate.cs @@ -19,14 +19,13 @@ namespace Govor.Data.Migrations columns: table => new { Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - Name = table.Column(type: "longtext", nullable: false) + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), - InviteCode = table.Column(type: "longtext", nullable: false) + Description = table.Column(type: "varchar(500)", maxLength: 500, nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), + ImageId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), IsChannel = table.Column(type: "tinyint(1)", nullable: false), - IsPrivate = table.Column(type: "tinyint(1)", nullable: false), - Admins = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4") + IsPrivate = table.Column(type: "tinyint(1)", nullable: false) }, constraints: table => { @@ -34,35 +33,6 @@ namespace Govor.Data.Migrations }) .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( - name: "GroupAdmins", - columns: table => new - { - Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") - }, - constraints: table => - { - table.PrimaryKey("PK_GroupAdmins", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "GroupMemberships", - columns: table => new - { - Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - IsBanned = table.Column(type: "tinyint(1)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_GroupMemberships", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( name: "Invitations", columns: table => new @@ -84,6 +54,25 @@ namespace Govor.Data.Migrations }) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "MediaFiles", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Url = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MediaType = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MineType = table.Column(type: "varchar(128)", maxLength: 128, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + DateCreated = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MediaFiles", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( name: "PrivateChats", columns: table => new @@ -98,6 +87,26 @@ namespace Govor.Data.Migrations }) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "GroupAdmins", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_GroupAdmins", x => x.Id); + table.ForeignKey( + name: "FK_GroupAdmins_ChatGroups_GroupId", + column: x => x.GroupId, + principalTable: "ChatGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( name: "Users", columns: table => new @@ -145,12 +154,6 @@ namespace Govor.Data.Migrations constraints: table => { table.PrimaryKey("PK_Messages", x => x.Id); - table.ForeignKey( - name: "FK_Messages_Messages_ReplyToMessageId", - column: x => x.ReplyToMessageId, - principalTable: "Messages", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "FK_Messages_PrivateChats_PrivateChatId", column: x => x.PrivateChatId, @@ -204,24 +207,56 @@ namespace Govor.Data.Migrations }) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "GroupInvitations", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserMakerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + InvitationCode = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(500)", maxLength: 500, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EndDate = table.Column(type: "datetime(6)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + MaxParticipants = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupInvitations", x => x.Id); + table.ForeignKey( + name: "FK_GroupInvitations_ChatGroups_GroupId", + column: x => x.GroupId, + principalTable: "ChatGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GroupInvitations_Users_UserMakerId", + column: x => x.UserMakerId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( name: "MediaAttachments", columns: table => new { Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), MessageId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - Type = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - FilePath = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - MimeType = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - EncryptedKey = table.Column(type: "varchar(512)", maxLength: 512, nullable: true) - .Annotation("MySql:CharSet", "utf8mb4") + MediaFileId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") }, constraints: table => { table.PrimaryKey("PK_MediaAttachments", x => x.Id); + table.ForeignKey( + name: "FK_MediaAttachments_MediaFiles_MediaFileId", + column: x => x.MediaFileId, + principalTable: "MediaFiles", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "FK_MediaAttachments_Messages_MessageId", column: x => x.MessageId, @@ -281,6 +316,34 @@ namespace Govor.Data.Migrations }) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "GroupMemberships", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + InvitationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + IsBanned = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupMemberships", x => x.Id); + table.ForeignKey( + name: "FK_GroupMemberships_ChatGroups_GroupId", + column: x => x.GroupId, + principalTable: "ChatGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GroupMemberships_GroupInvitations_InvitationId", + column: x => x.InvitationId, + principalTable: "GroupInvitations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateIndex( name: "IX_Friendships_AddresseeId", table: "Friendships", @@ -291,6 +354,36 @@ namespace Govor.Data.Migrations table: "Friendships", column: "RequesterId"); + migrationBuilder.CreateIndex( + name: "IX_GroupAdmins_GroupId", + table: "GroupAdmins", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_GroupInvitations_GroupId", + table: "GroupInvitations", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_GroupInvitations_UserMakerId", + table: "GroupInvitations", + column: "UserMakerId"); + + migrationBuilder.CreateIndex( + name: "IX_GroupMemberships_GroupId", + table: "GroupMemberships", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_GroupMemberships_InvitationId", + table: "GroupMemberships", + column: "InvitationId"); + + migrationBuilder.CreateIndex( + name: "IX_MediaAttachments_MediaFileId", + table: "MediaAttachments", + column: "MediaFileId"); + migrationBuilder.CreateIndex( name: "IX_MediaAttachments_MessageId", table: "MediaAttachments", @@ -312,11 +405,6 @@ namespace Govor.Data.Migrations table: "Messages", column: "PrivateChatId"); - migrationBuilder.CreateIndex( - name: "IX_Messages_ReplyToMessageId", - table: "Messages", - column: "ReplyToMessageId"); - migrationBuilder.CreateIndex( name: "IX_MessageViews_MessageId_UserId", table: "MessageViews", @@ -335,9 +423,6 @@ namespace Govor.Data.Migrations migrationBuilder.DropTable( name: "Admins"); - migrationBuilder.DropTable( - name: "ChatGroups"); - migrationBuilder.DropTable( name: "Friendships"); @@ -357,16 +442,25 @@ namespace Govor.Data.Migrations name: "MessageViews"); migrationBuilder.DropTable( - name: "Users"); + name: "GroupInvitations"); + + migrationBuilder.DropTable( + name: "MediaFiles"); migrationBuilder.DropTable( name: "Messages"); migrationBuilder.DropTable( - name: "Invitations"); + name: "ChatGroups"); + + migrationBuilder.DropTable( + name: "Users"); migrationBuilder.DropTable( name: "PrivateChats"); + + migrationBuilder.DropTable( + name: "Invitations"); } } } diff --git a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs index 4b9f74d..13582b2 100644 --- a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Govor.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -38,13 +38,13 @@ namespace Govor.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); - b.PrimitiveCollection("Admins") + b.Property("Description") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(500) + .HasColumnType("varchar(500)"); - b.PrimitiveCollection("InviteCode") - .IsRequired() - .HasColumnType("longtext"); + b.Property("ImageId") + .HasColumnType("char(36)"); b.Property("IsChannel") .HasColumnType("tinyint(1)"); @@ -54,7 +54,8 @@ namespace Govor.Data.Migrations b.Property("Name") .IsRequired() - .HasColumnType("longtext"); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.HasKey("Id"); @@ -99,9 +100,51 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("GroupId"); + b.ToTable("GroupAdmins"); }); + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.Property("UserMakerId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserMakerId"); + + b.ToTable("GroupInvitations"); + }); + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => { b.Property("Id") @@ -111,6 +154,10 @@ namespace Govor.Data.Migrations b.Property("GroupId") .HasColumnType("char(36)"); + b.Property("InvitationId") + .IsRequired() + .HasColumnType("char(36)"); + b.Property("IsBanned") .HasColumnType("tinyint(1)"); @@ -119,6 +166,10 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("GroupId"); + + b.HasIndex("InvitationId"); + b.ToTable("GroupMemberships"); }); @@ -162,30 +213,46 @@ namespace Govor.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); - b.Property("EncryptedKey") - .HasMaxLength(512) - .HasColumnType("varchar(512)"); - - b.Property("FilePath") - .IsRequired() - .HasColumnType("longtext"); + b.Property("MediaFileId") + .HasColumnType("char(36)"); b.Property("MessageId") .HasColumnType("char(36)"); - b.Property("MimeType") + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("MediaType") .IsRequired() .HasColumnType("longtext"); - b.Property("Type") + b.Property("MineType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Url") .IsRequired() .HasColumnType("longtext"); b.HasKey("Id"); - b.HasIndex("MessageId"); - - b.ToTable("MediaAttachments"); + b.ToTable("MediaFiles"); }); modelBuilder.Entity("Govor.Core.Models.Message", b => @@ -226,8 +293,6 @@ namespace Govor.Data.Migrations b.HasIndex("PrivateChatId"); - b.HasIndex("ReplyToMessageId"); - b.ToTable("Messages"); }); @@ -368,14 +433,63 @@ namespace Govor.Data.Migrations b.Navigation("Requester"); }); + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Admins") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("InviteCodes") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.User", "UserMaker") + .WithMany() + .HasForeignKey("UserMakerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("UserMaker"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.GroupInvitation", null) + .WithMany() + .HasForeignKey("InvitationId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + }); + modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => { + b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.HasOne("Govor.Core.Models.Message", "Message") .WithMany("MediaAttachments") .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("MediaFile"); + b.Navigation("Message"); }); @@ -384,13 +498,6 @@ namespace Govor.Data.Migrations b.HasOne("Govor.Core.Models.PrivateChat", null) .WithMany("Messages") .HasForeignKey("PrivateChatId"); - - b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") - .WithMany() - .HasForeignKey("ReplyToMessageId") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("ReplyToMessage"); }); modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => @@ -432,6 +539,15 @@ namespace Govor.Data.Migrations b.Navigation("Invite"); }); + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Navigation("Admins"); + + b.Navigation("InviteCodes"); + + b.Navigation("Members"); + }); + modelBuilder.Entity("Govor.Core.Models.Invitation", b => { b.Navigation("Users");