rework and starts for new friends system

This commit is contained in:
Artemy
2025-07-08 22:28:04 +07:00
parent 92c1ff6458
commit b1f3aa0266
29 changed files with 1028 additions and 309 deletions
@@ -1,5 +1,6 @@
using AutoFixture; using AutoFixture;
using Govor.API.Hubs; using Govor.API.Hubs;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Messages; using Govor.Application.Interfaces.Messages;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
@@ -11,6 +12,7 @@ public class ChatsHubTests
{ {
private Mock<ILogger<ChatsHub>> _loggerMock; private Mock<ILogger<ChatsHub>> _loggerMock;
private Mock<IMessageService> _messageServiceMock; private Mock<IMessageService> _messageServiceMock;
private Mock<IUserGroupsService> _userGroupsServiceMock;
private Fixture _fixture; private Fixture _fixture;
private ChatsHub _chatsHub; private ChatsHub _chatsHub;
@@ -22,14 +24,20 @@ public class ChatsHubTests
_fixture.Behaviors.Add(new OmitOnRecursionBehavior()); _fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_messageServiceMock = new Mock<IMessageService>(); _messageServiceMock = new Mock<IMessageService>();
_userGroupsServiceMock = new Mock<IUserGroupsService>();
_loggerMock = new Mock<ILogger<ChatsHub>>(); _loggerMock = new Mock<ILogger<ChatsHub>>();
_chatsHub = new ChatsHub( _chatsHub = new ChatsHub(
_loggerMock.Object, _loggerMock.Object,
_messageServiceMock.Object _messageServiceMock.Object,
_userGroupsServiceMock.Object
); );
} }
// Test for Send action // Test for Send action
[Test]
public void SendMessage_Success()
{
}
} }
@@ -1,22 +1,22 @@
using AutoFixture; using AutoFixture;
using Govor.Application.Exceptions.FriendsService; using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces; using Govor.Application.Interfaces.Friends;
using Govor.Application.Services; using Govor.Application.Services.Friends;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Friendships; using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users; using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions; using Govor.Data.Repositories.Exceptions;
using Moq; using Moq;
namespace Govor.API.Tests.UnitTests.Services; namespace Govor.API.Tests.UnitTests.Services.Friends;
[TestFixture] [TestFixture]
public class FriendsServiceTests public class FriendRequestServiceTests
{ {
private Fixture _fixture; private Fixture _fixture;
private Mock<IUsersRepository> _usersRepositoryMock; private Mock<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock; private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
private IFriendsService _service; private IFriendRequestService _service;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@@ -31,96 +31,10 @@ public class FriendsServiceTests
_usersRepositoryMock = new Mock<IUsersRepository>(); _usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>(); _friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendsService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object); _service = new FriendRequestService(_friendshipsRepositoryMock.Object);
}
// SearchUsersAsync
[Test]
public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(new List<Friendship>()); // 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<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test"));
// Axt & Assert
Assert.ThrowsAsync<SearchUsersException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(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<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository()
{
// Arrange
_friendshipsRepositoryMock
.Setup(u => u.FindByUserIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
} }
// SendFriendRequestAsync // SendFriendRequestAsync
[Test] [Test]
public void SendFriendRequestAsync_SendingToSelf_ThrowsInvalidOperationException() public void SendFriendRequestAsync_SendingToSelf_ThrowsInvalidOperationException()
{ {
@@ -270,45 +184,104 @@ public class FriendsServiceTests
Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Accepted)); Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Accepted));
} }
// GetFriendsAsync // RejectFriend
[Test] [Test]
public async Task GetFriendsAsync_ReturnsUsers_IfUserExists() public async Task RejectFriendRequestAsync_ValidRequest_CallsRemoveAsync()
{ {
// Arrange var friendship = _fixture.Build<Friendship>()
var userId = Guid.NewGuid(); .With(f => f.Status, FriendshipStatus.Pending)
var friendships = _fixture.CreateMany<Friendship>().ToList(); .Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
friendships.ForEach(f => await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId);
{
f.RequesterId = userId; // Assert
f.Status = FriendshipStatus.Accepted; _friendshipsRepositoryMock.Verify(r => r.UpdateAsync(It.Is<Friendship>(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<Friendship>()
.With(f => f.Status, FriendshipStatus.Accepted)
.Create();
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId)) _friendshipsRepositoryMock
.ReturnsAsync(friendships); .Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act
var result = await _service.GetFriendsAsync(userId); // Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId));
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result, Is.EquivalentTo(friendships.Select(f => f.Addressee)));
} }
[Test] [Test]
public void GetFriendsAsync_Throws_InvalidOperationException_IfUserNotExists() public void RejectFriendRequestAsync_Throws_UnauthorizedAccessException_IfCurrentUserIdIsNotAddressee()
{ {
// Arrange // Arrange
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(It.IsAny<Guid>())) var friendship = _fixture.Build<Friendship>()
.ThrowsAsync(new NotFoundByKeyException<Guid>(Guid.NewGuid())); .With(f => f.Status, FriendshipStatus.Pending)
.Create();
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.GetFriendsAsync(Guid.NewGuid())); _friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
var wrongUserId = Guid.NewGuid(); // не совпадает с AddresseeId
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(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<Guid>(requestId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.RejectFriendRequestAsync(requestId, userId));
}
[Test]
public async Task RejectFriendRequestAsync_ChangesStatusToAccepted()
{
var friendship = _fixture.Build<Friendship>()
.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<Friendship>()))
.Callback<Friendship>(f => updatedFriendship = f)
.Returns(Task.CompletedTask);
await _service.RejectFriendRequestAsync(friendship.Id, friendship.AddresseeId);
Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Rejected));
} }
// GetIncomingRequestsAsync // GetIncomingRequestsAsync
[Test] [Test]
public async Task GetIncomingRequestsAsync_ReturnsFriendships_IfFriendshipsExists() public async Task GetIncomingRequestsAsync_ReturnsFriendships_IfFriendshipsExists()
{ {
@@ -354,5 +327,4 @@ public class FriendsServiceTests
Assert.ThrowsAsync<InvalidOperationException>(async () => Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.GetIncomingRequestsAsync(userId)); await _service.GetIncomingRequestsAsync(userId));
} }
}
}
@@ -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<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
IFriendshipService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendshipService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object);
}
// SearchUsersAsync
[Test]
public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(new List<Friendship>()); // 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<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test"));
// Axt & Assert
Assert.ThrowsAsync<SearchUsersException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(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<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository()
{
// Arrange
_friendshipsRepositoryMock
.Setup(u => u.FindByUserIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
// GetFriendsAsync
[Test]
public async Task GetFriendsAsync_ReturnsUsers_IfUserExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().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<Guid>()))
.ThrowsAsync(new NotFoundByKeyException<Guid>(Guid.NewGuid()));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.GetFriendsAsync(Guid.NewGuid()));
}
}
@@ -4,7 +4,7 @@ using Govor.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
namespace Govor.Tests.Application.Services; namespace Govor.API.Tests.UnitTests.Services;
[TestFixture] [TestFixture]
public class PingHandlerServiceTests public class PingHandlerServiceTests
@@ -104,6 +104,5 @@ public class FriendshipsController : Controller
Status = f.Status, Status = f.Status,
AddresseeId = f.AddresseeId, AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId, RequesterId = f.RequesterId,
Requester = BuildUserDtos([f.Requester]).First(),
}).ToList(); }).ToList();
} }
@@ -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<ChatLoadController> logger, IMessagesLoader messagesLoader)
{
}
}
+48 -1
View File
@@ -97,6 +97,26 @@ public class FriendsController : Controller
} }
} }
[HttpGet("responses")]
public async Task<IActionResult> 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<FriendshipDto>());
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
[HttpPost("accept")] [HttpPost("accept")]
public async Task<IActionResult> AcceptFriend([FromQuery] Guid friendshipId) public async Task<IActionResult> AcceptFriend([FromQuery] Guid friendshipId)
{ {
@@ -125,6 +145,34 @@ public class FriendsController : Controller
} }
} }
[HttpPost("reject")]
public async Task<IActionResult> 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] [HttpGet]
public async Task<IActionResult> GetFriends() public async Task<IActionResult> GetFriends()
{ {
@@ -160,6 +208,5 @@ public class FriendsController : Controller
Status = f.Status, Status = f.Status,
AddresseeId = f.AddresseeId, AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId, RequesterId = f.RequesterId,
Requester = BuildUserDtos([f.Requester]).First(),
}).ToList(); }).ToList();
} }
@@ -5,9 +5,11 @@ using Govor.Application.Infrastructure.Validators;
using Govor.Application.Interfaces; using Govor.Application.Interfaces;
using Govor.Application.Interfaces.AdminsStuff; using Govor.Application.Interfaces.AdminsStuff;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Messages; using Govor.Application.Interfaces.Messages;
using Govor.Application.Services; using Govor.Application.Services;
using Govor.Application.Services.Friends;
using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models; using Govor.Core.Models;
@@ -36,7 +38,11 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IInvitesService, InvitesService>(); services.AddScoped<IInvitesService, InvitesService>();
services.AddScoped<IInvitationGenerator, InvitationGenerator>(); services.AddScoped<IInvitationGenerator, InvitationGenerator>();
services.AddScoped<IUsernameValidator, UsernameValidator>(); services.AddScoped<IUsernameValidator, UsernameValidator>();
services.AddScoped<IFriendsService, FriendsService>();
// Friends services
services.AddScoped<IFriendshipService, FriendshipService>();
services.AddScoped<IFriendRequestService, FriendRequestService>();
services.AddScoped<IFriendsBlockService, FriendsBlockService>();
services.AddScoped<IStorageService>(sp => services.AddScoped<IStorageService>(sp =>
{ {
@@ -53,6 +59,7 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IMessageService, MessageService>(); services.AddScoped<IMessageService, MessageService>();
services.AddScoped<IVerifyFriendship, VerifyFriendship>(); services.AddScoped<IVerifyFriendship, VerifyFriendship>();
services.AddScoped<IUserGroupsService, UserGroupsService>(); services.AddScoped<IUserGroupsService, UserGroupsService>();
services.AddScoped<IMessagesLoader, MessagesLoader>();
} }
public static void AddRepositories(this IServiceCollection services) public static void AddRepositories(this IServiceCollection services)
@@ -65,6 +72,8 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IFriendshipsRepository, FriendshipsRepository>(); services.AddScoped<IFriendshipsRepository, FriendshipsRepository>();
services.AddScoped<IPrivateChatsRepository, PrivateChatsRepository>(); services.AddScoped<IPrivateChatsRepository, PrivateChatsRepository>();
services.AddScoped<IGroupsRepository, GroupRepository>(); services.AddScoped<IGroupsRepository, GroupRepository>();
// other
} }
public static void AddValidators(this IServiceCollection services) public static void AddValidators(this IServiceCollection services)
+4
View File
@@ -23,5 +23,9 @@
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" /> <ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Controllers\Friends\" />
</ItemGroup>
</Project> </Project>
+2 -1
View File
@@ -17,10 +17,11 @@ public class ChatsHub : Hub
private readonly IMessageService _messageService; private readonly IMessageService _messageService;
private readonly IUserGroupsService _userService; private readonly IUserGroupsService _userService;
public ChatsHub(ILogger<ChatsHub> logger, IMessageService messageService) public ChatsHub(ILogger<ChatsHub> logger, IMessageService messageService, IUserGroupsService userService)
{ {
_logger = logger; _logger = logger;
_messageService = messageService; _messageService = messageService;
_userService = userService;
} }
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
+38
View File
@@ -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);
}
}
-8
View File
@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
public class UsersHub : Hub
{
}
@@ -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<List<Friendship>> GetIncomingRequestsAsync(Guid userId);
Task<List<Friendship>> GetResponsesAsync(Guid userId);
}
@@ -0,0 +1,7 @@
namespace Govor.Application.Interfaces;
public interface IFriendsBlockService
{
Task BlockFriendRequestAsync(Guid userId, Guid currentUserId);
Task UnblockFriendRequestAsync(Guid userId, Guid currentUserId);
}
@@ -0,0 +1,9 @@
using Govor.Core.Models;
namespace Govor.Application.Interfaces.Friends;
public interface IFriendshipService
{
Task<List<User>> GetFriendsAsync(Guid userId);
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
}
@@ -7,6 +7,10 @@ public interface IFriendsService
Task<List<User>> SearchUsersAsync(string query, Guid currentId); Task<List<User>> SearchUsersAsync(string query, Guid currentId);
Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId); Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId);
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId); Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId);
Task<List<User>> GetFriendsAsync(Guid userId); Task<List<User>> GetFriendsAsync(Guid userId);
Task<List<Friendship>> GetResponsesAsync(Guid userId);
Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId); Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId);
} }
@@ -0,0 +1,9 @@
using Govor.Core.Models;
namespace Govor.Application.Interfaces;
public interface IMessagesLoader
{
Task<List<Message>> LoadLastMessagesInUserChat(Guid userId,Guid currentId, Guid? startMessageId, int pageSize = 20);
Task<List<Message>> LoadLastMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int pageSize = 20);
}
+2 -2
View File
@@ -55,7 +55,7 @@ public class AuthService : IAccountService
await _usersRepository.AddAsync(user); await _usersRepository.AddAsync(user);
SetRole(user, invitation); await SetRole(user, invitation);
return await _jwtService.GenerateJwtTokenAsync(user); return await _jwtService.GenerateJwtTokenAsync(user);
} }
@@ -74,7 +74,7 @@ public class AuthService : IAccountService
return await _jwtService.GenerateJwtTokenAsync(user); return await _jwtService.GenerateJwtTokenAsync(user);
} }
private async void SetRole(User user, Invitation invitation) private async Task SetRole(User user, Invitation invitation)
{ {
if(invitation.IsAdmin) if(invitation.IsAdmin)
await _adminsRepository.AddAsync(new Admin() { UserId = user.Id }); await _adminsRepository.AddAsync(new Admin() { UserId = user.Id });
@@ -1,59 +1,29 @@
using Govor.Application.Exceptions.FriendsService; using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces; using Govor.Application.Interfaces.Friends;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Friendships; using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions; 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 readonly IFriendshipsRepository _friendshipsRepository;
private IFriendshipsRepository _friendshipsRepository;
public FriendsService(IUsersRepository usersRepository, IFriendshipsRepository relationshipsRepository)
{
_usersRepository = usersRepository;
_friendshipsRepository = relationshipsRepository;
}
public async Task<List<User>> SearchUsersAsync(string query, Guid currentId)
{
List<User> all = new List<User>();
try
{
all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query);
return all public FriendRequestService(IFriendshipsRepository friendshipsRepository)
.Where(u => u.Id != currentId) {
.ToList(); _friendshipsRepository = friendshipsRepository;
}
catch (NotFoundByKeyException<(string, Guid)> ex)
{
throw new SearchUsersException(
$"Users with given query: \"{query}\" for user with id {currentId} was not found", ex);
}
catch (NotFoundByKeyException<Guid> 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 SendFriendRequestAsync(Guid fromUserId, Guid toUserId) public async Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId)
{ {
if (fromUserId == toUserId) if (fromUserId == toUserId)
throw new InvalidOperationException("Cannot send a request to self user"); throw new InvalidOperationException("Cannot send a request to self user");
if (_friendshipsRepository.Exist(fromUserId, toUserId)) if (_friendshipsRepository.Exist(fromUserId, toUserId))
throw new RequestAlreadySentException(fromUserId, toUserId); throw new RequestAlreadySentException(fromUserId, toUserId);
await _friendshipsRepository.AddAsync(new Friendship() await _friendshipsRepository.AddAsync(new Friendship
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
RequesterId = fromUserId, RequesterId = fromUserId,
@@ -77,26 +47,30 @@ public class FriendsService : IFriendsService
friendship.Status = FriendshipStatus.Accepted; friendship.Status = FriendshipStatus.Accepted;
await _friendshipsRepository.UpdateAsync(friendship); await _friendshipsRepository.UpdateAsync(friendship);
} }
catch (NotFoundByKeyException<Guid> e) catch (NotFoundByKeyException<Guid> 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<List<User>> GetFriendsAsync(Guid userId) public async Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId)
{ {
try try
{ {
var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); var friendship = await _friendshipsRepository.GetByIdAsync(requestId);
return friendships if (friendship.AddresseeId != currentUserId)
.Where(f => f.Status == FriendshipStatus.Accepted) throw new UnauthorizedAccessException("You cannot accept this request");
.Select(f => f.RequesterId == userId ? f.Addressee : f.Requester)
.ToList(); 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<Guid> ex) catch (NotFoundByKeyException<Guid> 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); throw new InvalidOperationException("User not exist", ex);
} }
} }
}
public async Task<List<Friendship>> GetResponsesAsync(Guid userId)
{
try
{
var friendships = await _friendshipsRepository.FindByUserIdAsync(userId);
return friendships.Where(f => f.RequesterId == userId && f.Status != FriendshipStatus.Accepted).ToList()
?? new List<Friendship>();
}
catch (NotFoundByKeyException<Guid> ex)
{
throw new InvalidOperationException("User not exist", ex);
}
}
}
@@ -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();
}
}
@@ -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<List<User>> SearchUsersAsync(string query, Guid currentId)
{
List<User> all = new List<User>();
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<Guid> 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<List<User>> 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<Guid> ex)
{
throw new InvalidOperationException("User not found", ex);
}
}
}
@@ -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<List<Message>> 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<Message>();
}
}
public async Task<List<Message>> 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<Message>();
}
}
}
+1 -1
View File
@@ -181,7 +181,7 @@ namespace Govor.ConsoleClient
var requests = await friendsClient.GetIncomingRequestsAsync(); var requests = await friendsClient.GetIncomingRequestsAsync();
foreach (var r in requests) 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; break;
case "/chat": case "/chat":
-1
View File
@@ -8,5 +8,4 @@ public class FriendshipDto
public Guid RequesterId { get; set; } public Guid RequesterId { get; set; }
public Guid AddresseeId { get; set; } public Guid AddresseeId { get; set; }
public FriendshipStatus Status { get; set; } public FriendshipStatus Status { get; set; }
public UserDto Requester { get; set; }
} }
+1 -1
View File
@@ -5,6 +5,6 @@ public class GroupMembership
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid GroupId { get; set; } public Guid GroupId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public Guid InvitationId { get; set; } public Guid? InvitationId { get; set; }
public bool IsBanned { get; set; } public bool IsBanned { get; set; }
} }
@@ -4,5 +4,5 @@ namespace Govor.Core.Repositories.Groups;
public interface IGroupMessagesReader public interface IGroupMessagesReader
{ {
public Task<IEnumerable<Message>> GetMessages(Guid chatId, Guid? startMessageId, int pageSize = 20); public Task<List<Message>> GetMessages(Guid chatId, Guid? startMessageId, int pageSize = 20, RecipientType type = RecipientType.User);
} }
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Govor.Data.Migrations namespace Govor.Data.Migrations
{ {
[DbContext(typeof(GovorDbContext))] [DbContext(typeof(GovorDbContext))]
[Migration("20250630111826_InitialCreate")] [Migration("20250707142430_InitialCreate")]
partial class InitialCreate partial class InitialCreate
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -20,7 +20,7 @@ namespace Govor.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@@ -41,13 +41,13 @@ namespace Govor.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.PrimitiveCollection<string>("Admins") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasMaxLength(500)
.HasColumnType("varchar(500)");
b.PrimitiveCollection<string>("InviteCode") b.Property<Guid>("ImageId")
.IsRequired() .HasColumnType("char(36)");
.HasColumnType("longtext");
b.Property<bool>("IsChannel") b.Property<bool>("IsChannel")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -57,7 +57,8 @@ namespace Govor.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id"); b.HasKey("Id");
@@ -102,9 +103,51 @@ namespace Govor.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GroupId");
b.ToTable("GroupAdmins"); b.ToTable("GroupAdmins");
}); });
modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("EndDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("GroupId")
.HasColumnType("char(36)");
b.Property<string>("InvitationCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<int>("MaxParticipants")
.HasColumnType("int");
b.Property<Guid>("UserMakerId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("UserMakerId");
b.ToTable("GroupInvitations");
});
modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => modelBuilder.Entity("Govor.Core.Models.GroupMembership", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -114,6 +157,10 @@ namespace Govor.Data.Migrations
b.Property<Guid>("GroupId") b.Property<Guid>("GroupId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid?>("InvitationId")
.IsRequired()
.HasColumnType("char(36)");
b.Property<bool>("IsBanned") b.Property<bool>("IsBanned")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -122,6 +169,10 @@ namespace Govor.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("InvitationId");
b.ToTable("GroupMemberships"); b.ToTable("GroupMemberships");
}); });
@@ -165,30 +216,46 @@ namespace Govor.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<string>("EncryptedKey") b.Property<Guid>("MediaFileId")
.HasMaxLength(512) .HasColumnType("char(36)");
.HasColumnType("varchar(512)");
b.Property<string>("FilePath")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("MessageId") b.Property<Guid>("MessageId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<string>("MimeType") b.HasKey("Id");
b.HasIndex("MediaFileId");
b.HasIndex("MessageId");
b.ToTable("MediaAttachments");
});
modelBuilder.Entity("Govor.Core.Models.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("DateCreated")
.HasColumnType("datetime(6)");
b.Property<string>("MediaType")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Type") b.Property<string>("MineType")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("MessageId"); b.ToTable("MediaFiles");
b.ToTable("MediaAttachments");
}); });
modelBuilder.Entity("Govor.Core.Models.Message", b => modelBuilder.Entity("Govor.Core.Models.Message", b =>
@@ -229,8 +296,6 @@ namespace Govor.Data.Migrations
b.HasIndex("PrivateChatId"); b.HasIndex("PrivateChatId");
b.HasIndex("ReplyToMessageId");
b.ToTable("Messages"); b.ToTable("Messages");
}); });
@@ -371,14 +436,63 @@ namespace Govor.Data.Migrations
b.Navigation("Requester"); 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 => 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") b.HasOne("Govor.Core.Models.Message", "Message")
.WithMany("MediaAttachments") .WithMany("MediaAttachments")
.HasForeignKey("MessageId") .HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MediaFile");
b.Navigation("Message"); b.Navigation("Message");
}); });
@@ -387,13 +501,6 @@ namespace Govor.Data.Migrations
b.HasOne("Govor.Core.Models.PrivateChat", null) b.HasOne("Govor.Core.Models.PrivateChat", null)
.WithMany("Messages") .WithMany("Messages")
.HasForeignKey("PrivateChatId"); .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 => modelBuilder.Entity("Govor.Core.Models.MessageReaction", b =>
@@ -435,6 +542,15 @@ namespace Govor.Data.Migrations
b.Navigation("Invite"); 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 => modelBuilder.Entity("Govor.Core.Models.Invitation", b =>
{ {
b.Navigation("Users"); b.Navigation("Users");
@@ -19,14 +19,13 @@ namespace Govor.Data.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Name = table.Column<string>(type: "longtext", nullable: false) Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"), .Annotation("MySql:CharSet", "utf8mb4"),
InviteCode = table.Column<string>(type: "longtext", nullable: false) Description = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"), .Annotation("MySql:CharSet", "utf8mb4"),
ImageId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
IsChannel = table.Column<bool>(type: "tinyint(1)", nullable: false), IsChannel = table.Column<bool>(type: "tinyint(1)", nullable: false),
IsPrivate = table.Column<bool>(type: "tinyint(1)", nullable: false), IsPrivate = table.Column<bool>(type: "tinyint(1)", nullable: false)
Admins = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
}, },
constraints: table => constraints: table =>
{ {
@@ -34,35 +33,6 @@ namespace Govor.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "GroupAdmins",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(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<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
IsBanned = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GroupMemberships", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Invitations", name: "Invitations",
columns: table => new columns: table => new
@@ -84,6 +54,25 @@ namespace Govor.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "MediaFiles",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Url = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MediaType = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MineType = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
DateCreated = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MediaFiles", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PrivateChats", name: "PrivateChats",
columns: table => new columns: table => new
@@ -98,6 +87,26 @@ namespace Govor.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "GroupAdmins",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(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( migrationBuilder.CreateTable(
name: "Users", name: "Users",
columns: table => new columns: table => new
@@ -145,12 +154,6 @@ namespace Govor.Data.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Messages", x => x.Id); 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( table.ForeignKey(
name: "FK_Messages_PrivateChats_PrivateChatId", name: "FK_Messages_PrivateChats_PrivateChatId",
column: x => x.PrivateChatId, column: x => x.PrivateChatId,
@@ -204,24 +207,56 @@ namespace Govor.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "GroupInvitations",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserMakerId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvitationCode = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
MaxParticipants = table.Column<int>(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( migrationBuilder.CreateTable(
name: "MediaAttachments", name: "MediaAttachments",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
MessageId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), MessageId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Type = table.Column<string>(type: "longtext", nullable: false) MediaFileId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
.Annotation("MySql:CharSet", "utf8mb4"),
FilePath = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MimeType = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EncryptedKey = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MediaAttachments", x => x.Id); 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( table.ForeignKey(
name: "FK_MediaAttachments_Messages_MessageId", name: "FK_MediaAttachments_Messages_MessageId",
column: x => x.MessageId, column: x => x.MessageId,
@@ -281,6 +316,34 @@ namespace Govor.Data.Migrations
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "GroupMemberships",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
GroupId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvitationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
IsBanned = table.Column<bool>(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( migrationBuilder.CreateIndex(
name: "IX_Friendships_AddresseeId", name: "IX_Friendships_AddresseeId",
table: "Friendships", table: "Friendships",
@@ -291,6 +354,36 @@ namespace Govor.Data.Migrations
table: "Friendships", table: "Friendships",
column: "RequesterId"); 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( migrationBuilder.CreateIndex(
name: "IX_MediaAttachments_MessageId", name: "IX_MediaAttachments_MessageId",
table: "MediaAttachments", table: "MediaAttachments",
@@ -312,11 +405,6 @@ namespace Govor.Data.Migrations
table: "Messages", table: "Messages",
column: "PrivateChatId"); column: "PrivateChatId");
migrationBuilder.CreateIndex(
name: "IX_Messages_ReplyToMessageId",
table: "Messages",
column: "ReplyToMessageId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MessageViews_MessageId_UserId", name: "IX_MessageViews_MessageId_UserId",
table: "MessageViews", table: "MessageViews",
@@ -335,9 +423,6 @@ namespace Govor.Data.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Admins"); name: "Admins");
migrationBuilder.DropTable(
name: "ChatGroups");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Friendships"); name: "Friendships");
@@ -357,16 +442,25 @@ namespace Govor.Data.Migrations
name: "MessageViews"); name: "MessageViews");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Users"); name: "GroupInvitations");
migrationBuilder.DropTable(
name: "MediaFiles");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Messages"); name: "Messages");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Invitations"); name: "ChatGroups");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PrivateChats"); name: "PrivateChats");
migrationBuilder.DropTable(
name: "Invitations");
} }
} }
} }
@@ -17,7 +17,7 @@ namespace Govor.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@@ -38,13 +38,13 @@ namespace Govor.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.PrimitiveCollection<string>("Admins") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasMaxLength(500)
.HasColumnType("varchar(500)");
b.PrimitiveCollection<string>("InviteCode") b.Property<Guid>("ImageId")
.IsRequired() .HasColumnType("char(36)");
.HasColumnType("longtext");
b.Property<bool>("IsChannel") b.Property<bool>("IsChannel")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -54,7 +54,8 @@ namespace Govor.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id"); b.HasKey("Id");
@@ -99,9 +100,51 @@ namespace Govor.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GroupId");
b.ToTable("GroupAdmins"); b.ToTable("GroupAdmins");
}); });
modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime>("EndDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("GroupId")
.HasColumnType("char(36)");
b.Property<string>("InvitationCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<int>("MaxParticipants")
.HasColumnType("int");
b.Property<Guid>("UserMakerId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("UserMakerId");
b.ToTable("GroupInvitations");
});
modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => modelBuilder.Entity("Govor.Core.Models.GroupMembership", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -111,6 +154,10 @@ namespace Govor.Data.Migrations
b.Property<Guid>("GroupId") b.Property<Guid>("GroupId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid?>("InvitationId")
.IsRequired()
.HasColumnType("char(36)");
b.Property<bool>("IsBanned") b.Property<bool>("IsBanned")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -119,6 +166,10 @@ namespace Govor.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GroupId");
b.HasIndex("InvitationId");
b.ToTable("GroupMemberships"); b.ToTable("GroupMemberships");
}); });
@@ -162,30 +213,46 @@ namespace Govor.Data.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<string>("EncryptedKey") b.Property<Guid>("MediaFileId")
.HasMaxLength(512) .HasColumnType("char(36)");
.HasColumnType("varchar(512)");
b.Property<string>("FilePath")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("MessageId") b.Property<Guid>("MessageId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<string>("MimeType") b.HasKey("Id");
b.HasIndex("MediaFileId");
b.HasIndex("MessageId");
b.ToTable("MediaAttachments");
});
modelBuilder.Entity("Govor.Core.Models.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("DateCreated")
.HasColumnType("datetime(6)");
b.Property<string>("MediaType")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Type") b.Property<string>("MineType")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("MessageId"); b.ToTable("MediaFiles");
b.ToTable("MediaAttachments");
}); });
modelBuilder.Entity("Govor.Core.Models.Message", b => modelBuilder.Entity("Govor.Core.Models.Message", b =>
@@ -226,8 +293,6 @@ namespace Govor.Data.Migrations
b.HasIndex("PrivateChatId"); b.HasIndex("PrivateChatId");
b.HasIndex("ReplyToMessageId");
b.ToTable("Messages"); b.ToTable("Messages");
}); });
@@ -368,14 +433,63 @@ namespace Govor.Data.Migrations
b.Navigation("Requester"); 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 => 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") b.HasOne("Govor.Core.Models.Message", "Message")
.WithMany("MediaAttachments") .WithMany("MediaAttachments")
.HasForeignKey("MessageId") .HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MediaFile");
b.Navigation("Message"); b.Navigation("Message");
}); });
@@ -384,13 +498,6 @@ namespace Govor.Data.Migrations
b.HasOne("Govor.Core.Models.PrivateChat", null) b.HasOne("Govor.Core.Models.PrivateChat", null)
.WithMany("Messages") .WithMany("Messages")
.HasForeignKey("PrivateChatId"); .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 => modelBuilder.Entity("Govor.Core.Models.MessageReaction", b =>
@@ -432,6 +539,15 @@ namespace Govor.Data.Migrations
b.Navigation("Invite"); 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 => modelBuilder.Entity("Govor.Core.Models.Invitation", b =>
{ {
b.Navigation("Users"); b.Navigation("Users");