FriendsController

This commit is contained in:
Artemy
2025-06-27 19:12:31 +07:00
parent ec44347bbe
commit 05298c004c
17 changed files with 1008 additions and 22 deletions
@@ -28,6 +28,7 @@ public class FriendshipValidatorTests
[Test] [Test]
public void Given_ValidFriendship_When_Validate_Then_Returns_True() public void Given_ValidFriendship_When_Validate_Then_Returns_True()
{ {
// Arrange
var friendship = _fixture.Create<Friendship>(); var friendship = _fixture.Create<Friendship>();
// Act & Assert // Act & Assert
@@ -38,6 +39,7 @@ public class FriendshipValidatorTests
[Test] [Test]
public void Given_EmptyFriendshipId_When_Validate_Then_Returns_False() public void Given_EmptyFriendshipId_When_Validate_Then_Returns_False()
{ {
// Arrange
var friendship = _fixture.Create<Friendship>(); var friendship = _fixture.Create<Friendship>();
friendship.Id = Guid.Empty; friendship.Id = Guid.Empty;
@@ -49,6 +51,7 @@ public class FriendshipValidatorTests
[Test] [Test]
public void Given_EmptyRequesterId_When_Validate_Then_Returns_False() public void Given_EmptyRequesterId_When_Validate_Then_Returns_False()
{ {
// Arrange
var friendship = _fixture.Create<Friendship>(); var friendship = _fixture.Create<Friendship>();
friendship.RequesterId = Guid.Empty; friendship.RequesterId = Guid.Empty;
@@ -57,9 +60,22 @@ public class FriendshipValidatorTests
Assert.That(_friendshipValidator.TryValidate(friendship), Is.False); Assert.That(_friendshipValidator.TryValidate(friendship), Is.False);
} }
[Test]
public void GivenSameRequesterId_When_Validate_Then_Returns_False()
{
// Arrange
var friendship = _fixture.Create<Friendship>();
friendship.RequesterId = friendship.AddresseeId;
// Act & Assert
Assert.Throws<InvalidObjectException<Friendship>>( () => _friendshipValidator.Validate(friendship));
Assert.That(_friendshipValidator.TryValidate(friendship), Is.False);
}
[Test] [Test]
public void Given_AddresseeId_When_Validate_Then_Returns_False() public void Given_AddresseeId_When_Validate_Then_Returns_False()
{ {
// Arrange
var friendship = _fixture.Create<Friendship>(); var friendship = _fixture.Create<Friendship>();
friendship.AddresseeId = Guid.Empty; friendship.AddresseeId = Guid.Empty;
+121 -14
View File
@@ -1,3 +1,7 @@
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces;
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -9,48 +13,151 @@ namespace Govor.API.Controllers;
public class FriendsController : Controller public class FriendsController : Controller
{ {
private readonly ILogger<FriendsController> _logger; private readonly ILogger<FriendsController> _logger;
private readonly IFriendsService _friendsService;
public FriendsController(ILogger<FriendsController> logger, IFriendsService friendsService)
{
_logger = logger;
_friendsService = friendsService;
}
[HttpGet("search")] [HttpGet("search")]
public async Task<IActionResult> Search([FromBody] string query) public async Task<IActionResult> Search(string query)
{ {
return BadRequest("Not a valid request"); try
{
var result = await _friendsService.SearchUsersAsync(query, GetCurrentUserId());
return Ok(BuildUserDtos(result));
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest("Something happened during the search... try again");
}
} }
[HttpPost("request")] [HttpPost("request")]
public async Task<IActionResult> SendRequest([FromBody] Guid targetUserId) public async Task<IActionResult> SendRequest(Guid targetUserId)
{ {
return BadRequest("Not a valid request"); try
{
await _friendsService.SendFriendRequestAsync(targetUserId, GetCurrentUserId());
return Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("The user tried to send a friend request to themselves");
return BadRequest("You can't send a friend request to themselves");
}
catch (RequestAlreadySentException ex)
{
_logger.LogWarning("The user tried to send a friend request but the request is already sent");
return BadRequest("You already sent a friend request");
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest("Something happened during the request... try again");
}
} }
[HttpGet("requests")] [HttpGet("requests")]
public async Task<IActionResult> GetIncomingRequests() public async Task<IActionResult> GetIncomingRequests()
{ {
return BadRequest("Not a valid request"); try
{
_logger.LogInformation($"Getting incoming requests for user {GetCurrentUserId()}...");
var result = await _friendsService.GetIncomingRequestsAsync(GetCurrentUserId());
return Ok(BuildFriendshipDtos(result));
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest("You can't get real friend requests because we can't find your user data! Try again");
}
} }
[HttpPost("accept")] [HttpPost("accept")]
public async Task<IActionResult> AcceptFriend([FromBody] Guid requesterId) public async Task<IActionResult> AcceptFriend(Guid requesterId)
{ {
return BadRequest("Not a valid request"); try
{
await _friendsService.AcceptFriendRequestAsync(requesterId, GetCurrentUserId());
return Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("The user tried to accept a friend request but the request not exists");
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning("User tried to accept a friend request but addresseeId not equal userId");
return BadRequest("You can't accept a friend request");
}
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetFriends() public async Task<IActionResult> GetFriends()
{ {
return BadRequest("Not a valid request"); try
{
_logger.LogInformation($"Getting friends by user {GetCurrentUserId()}...");
var result = await _friendsService.GetFriendsAsync(GetCurrentUserId());
return Ok(BuildUserDtos(result));
}
catch(UnauthorizedAccessException ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest("Something happened during the request... try again");
}
} }
private Guid GetCurrentUserId() private Guid GetCurrentUserId()
{ {
var userIdClaim = HttpContext.User?.FindFirst("userID")?.Value; var userIdClaim = HttpContext.User?.FindFirst("userID")?.Value;
_logger.LogInformation("Claims: {Claims}", string.Join(", ", HttpContext.User?.Claims.Select(c => $"{c.Type}: {c.Value}") ?? new string[0])); if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
if (string.IsNullOrEmpty(userIdClaim))
{ {
_logger.LogError("No userID claim found"); throw new UnauthorizedAccessException("userID claim is missing or invalid");
return Guid.Empty; }
return userId;
} }
return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty; private List<UserDto> BuildUserDtos(IEnumerable<User> users)
{
List<UserDto> userDtos = new List<UserDto>();
foreach (var user in users)
{
userDtos.Add(new UserDto()
{
Id = user.Id,
Description = user.Description,
Username = user.Username,
WasOnline = user.WasOnline,
IconId = user.IconId,
});
}
return userDtos;
}
private List<FriendshipDto> BuildFriendshipDtos(List<Friendship> result)
{
List<FriendshipDto> dtos = new List<FriendshipDto>();
foreach (var friendship in result)
{
dtos.Add(new FriendshipDto()
{
Id = friendship.Id,
Status = friendship.Status,
AddresseeId = friendship.AddresseeId,
RequesterId = friendship.RequesterId,
});
}
return dtos;
} }
} }
@@ -9,6 +9,7 @@ using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Admins; using Govor.Core.Repositories.Admins;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.Invaites;
using Govor.Core.Repositories.MediasAttachments; using Govor.Core.Repositories.MediasAttachments;
using Govor.Core.Repositories.Messages; using Govor.Core.Repositories.Messages;
@@ -30,6 +31,7 @@ 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>();
services.AddScoped<IStorageService>(sp => services.AddScoped<IStorageService>(sp =>
{ {
@@ -45,6 +47,7 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IInvitesRepository, InvitesRepository>(); services.AddScoped<IInvitesRepository, InvitesRepository>();
services.AddScoped<IAdminsRepository, AdminsRepository>(); services.AddScoped<IAdminsRepository, AdminsRepository>();
services.AddScoped<IMediaAttachmentsRepository, MediaAttachmentsRepository>(); services.AddScoped<IMediaAttachmentsRepository, MediaAttachmentsRepository>();
services.AddScoped<IFriendshipsRepository, FriendshipsRepository>();
} }
public static void AddValidators(this IServiceCollection services) public static void AddValidators(this IServiceCollection services)
@@ -54,6 +57,7 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IObjectValidator<MediaAttachments>, MediaAttachmentsValidator>(); services.AddScoped<IObjectValidator<MediaAttachments>, MediaAttachmentsValidator>();
services.AddScoped<IObjectValidator<Admin>, AdminValidator>(); services.AddScoped<IObjectValidator<Admin>, AdminValidator>();
services.AddScoped<IObjectValidator<Invitation>, InvitationValidator>(); services.AddScoped<IObjectValidator<Invitation>, InvitationValidator>();
services.AddScoped<IObjectValidator<Friendship>, FriendshipValidator>();
} }
public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration) public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration)
+3 -7
View File
@@ -98,14 +98,10 @@ public class ChatsHub : Hub
private Guid GetUserId() private Guid GetUserId()
{ {
var userIdClaim = Context.User?.FindFirst("userID")?.Value; var userIdClaim = Context.User?.FindFirst("userID")?.Value;
_logger.LogInformation("Claims: {Claims}", string.Join(", ", Context.User?.Claims.Select(c => $"{c.Type}: {c.Value}") ?? new string[0])); if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
if (string.IsNullOrEmpty(userIdClaim))
{ {
_logger.LogError("No userID claim found"); throw new UnauthorizedAccessException("userID claim is missing or invalid");
return Guid.Empty;
} }
return userId;
return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty;
} }
} }
@@ -0,0 +1,6 @@
using Govor.Core;
namespace Govor.Application.Exceptions.FriendsService;
public class RequestAlreadySentException(Guid fromId, Guid userId)
: GovorCoreException($"Request was already sent from {fromId} to {userId}") {}
@@ -0,0 +1,13 @@
using Govor.Core;
namespace Govor.Application.Exceptions.FriendsService;
public class SearchUsersException : GovorCoreException
{
public SearchUsersException(string message, Exception innerException)
: base(message, innerException){}
public SearchUsersException(string message)
: base(message){}
}
@@ -0,0 +1,6 @@
using Govor.Core;
namespace Govor.Application.Exceptions.FriendsService;
public class SendFriendRequestException(Guid fromUserId, Guid toUserId, Exception exception)
: GovorCoreException($"Something happened when we try to send request from {fromUserId} to {toUserId}", exception);
@@ -0,0 +1,12 @@
using Govor.Core.Models;
namespace Govor.Application.Interfaces;
public interface IFriendsService
{
Task<List<User>> SearchUsersAsync(string searchTerm, Guid currentId);
Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId);
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
Task<List<User>> GetFriendsAsync(Guid userId);
Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId);
}
@@ -0,0 +1,119 @@
using Govor.Application.Exceptions;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces;
using Govor.Core.Models;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
namespace Govor.Application.Services;
public class FriendsService : IFriendsService
{
private IUsersRepository _usersRepository;
private IFriendshipsRepository _friendshipsRepository;
public FriendsService(IUsersRepository usersRepository, IFriendshipsRepository relationshipsRepository)
{
_usersRepository = usersRepository;
_friendshipsRepository = relationshipsRepository;
}
public async Task<List<User>> SearchUsersAsync(string searchTerm, Guid currentId)
{
var all = await _usersRepository.SearchPotentialFriendsAsync(currentId, searchTerm);
try
{
var friends = await _friendshipsRepository.FindByUserIdAsync(currentId);
friends = friends.Where(f => f.Status == FriendshipStatus.Accepted).ToList();
return all
.Where(u => u.Id != currentId && !friends.Select(f => f.RequesterId).Contains(u.Id))
.ToList();
}
catch (NotFoundByKeyException<Guid> ex)
{
return all.Where(u => u.Id != currentId).ToList();
}
catch (Exception ex)
{
throw new SearchUsersException($"When we try find friends by pattern {searchTerm} something wrong", ex);
}
}
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()
{
Id = Guid.NewGuid(),
RequesterId = fromUserId,
AddresseeId = toUserId,
Status = FriendshipStatus.Pending
});
}
public async Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId)
{
try
{
var friendship = await _friendshipsRepository.GetByIdAsync(requestId);
if (friendship.AddresseeId != currentUserId)
throw new UnauthorizedAccessException("You cannot accept this request");
if (friendship.Status != FriendshipStatus.Pending)
throw new InvalidOperationException("Request is already accepted");
friendship.Status = FriendshipStatus.Accepted;
await _friendshipsRepository.UpdateAsync(friendship);
}
catch (NotFoundByKeyException<Guid> e)
{
throw new InvalidOperationException("Friendship not found! You cant accept request!", e);
}
}
public async Task<List<User>> GetFriendsAsync(Guid userId)
{
try
{
var user = await _usersRepository.FindByIdAsync(userId);
var sent = user.SentFriendRequests
.Where(f => f.Status == FriendshipStatus.Accepted)
.Select(f => f.Addressee);
var received = user.ReceivedFriendRequests
.Where(f => f.Status == FriendshipStatus.Accepted)
.Select(f => f.Requester);
return sent.Concat(received).ToList();
}
catch (NotFoundByKeyException<Guid> ex)
{
throw new InvalidOperationException("User not exist", ex);
}
}
public async Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId)
{
try
{
var user = await _usersRepository.FindByIdAsync(userId);
return user.ReceivedFriendRequests;
}
catch (NotFoundByKeyException<Guid> ex)
{
throw new InvalidOperationException("User not exist", ex);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
using Govor.Core.Models;
namespace Govor.Contracts.DTOs;
public class FriendshipDto
{
public Guid Id { get; set; }
public Guid RequesterId { get; set; }
public Guid AddresseeId { get; set; }
public FriendshipStatus Status { get; set; }
}
+10
View File
@@ -0,0 +1,10 @@
namespace Govor.Contracts.DTOs;
public class UserDto
{
public Guid Id { get; set; }
public string Username { get; set; }
public string Description { get; set; }
public DateTime WasOnline { get; set; }
public Guid IconId {get; set;}
}
@@ -16,6 +16,8 @@ public class FriendshipValidator : IObjectValidator<Friendship>
throw new ArgumentException("Addresses cannot be empty", nameof(friendship.AddresseeId)); throw new ArgumentException("Addresses cannot be empty", nameof(friendship.AddresseeId));
if(friendship.RequesterId == Guid.Empty) if(friendship.RequesterId == Guid.Empty)
throw new ArgumentException("Requester cannot be empty", nameof(friendship.RequesterId)); throw new ArgumentException("Requester cannot be empty", nameof(friendship.RequesterId));
if(friendship.AddresseeId == friendship.RequesterId)
throw new ArgumentException("Addresses cannot be same", nameof(friendship.AddresseeId));
} }
catch (Exception e) catch (Exception e)
{ {
@@ -9,5 +9,6 @@ public interface IUsersReader
public Task<List<User>> FindByRangeIdAsync(IEnumerable<Guid> ids); public Task<List<User>> FindByRangeIdAsync(IEnumerable<Guid> ids);
public Task<User> FindByUsernameAsync(string username); public Task<User> FindByUsernameAsync(string username);
public Task<List<User>> FindByRangeUsernamesAsync(IEnumerable<string> usernames); public Task<List<User>> FindByRangeUsernamesAsync(IEnumerable<string> usernames);
public Task<List<User>> SearchPotentialFriendsAsync(Guid currentUserId, string query);
public Task<List<User>> FindUsersByCreatedDateAsync(DateOnly createdDate); public Task<List<User>> FindUsersByCreatedDateAsync(DateOnly createdDate);
} }
@@ -0,0 +1,467 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Govor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Govor.Data.Migrations
{
[DbContext(typeof(GovorDbContext))]
[Migration("20250627101322_friendships")]
partial class friendships
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Govor.Core.Models.Admin", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("UserId");
b.ToTable("Admins");
});
modelBuilder.Entity("Govor.Core.Models.ChatGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.PrimitiveCollection<List<Guid>>("Admins")
.IsRequired()
.HasColumnType("uuid[]");
b.PrimitiveCollection<List<string>>("InviteCode")
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("IsChannel")
.HasColumnType("boolean");
b.Property<bool>("IsPrivate")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("ChatGroups");
});
modelBuilder.Entity("Govor.Core.Models.Friendship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AddresseeId")
.HasColumnType("uuid");
b.Property<Guid>("RequesterId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AddresseeId");
b.HasIndex("RequesterId");
b.ToTable("Friendships");
});
modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("GroupId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("GroupAdmins");
});
modelBuilder.Entity("Govor.Core.Models.GroupMembership", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("GroupId")
.HasColumnType("uuid");
b.Property<bool>("IsBanned")
.HasColumnType("boolean");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("GroupMemberships");
});
modelBuilder.Entity("Govor.Core.Models.Invitation", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<int>("MaxParticipants")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Invitations");
});
modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("EncryptedKey")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("FilePath")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("MessageId");
b.ToTable("MediaAttachments");
});
modelBuilder.Entity("Govor.Core.Models.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime?>("EditedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EncryptedContent")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsEdited")
.HasColumnType("boolean");
b.Property<Guid?>("PrivateChatId")
.HasColumnType("uuid");
b.Property<Guid>("RecipientId")
.HasColumnType("uuid");
b.Property<int>("RecipientType")
.HasColumnType("integer");
b.Property<Guid?>("ReplyToMessageId")
.HasColumnType("uuid");
b.Property<Guid>("SenderId")
.HasColumnType("uuid");
b.Property<DateTime>("SentAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("PrivateChatId");
b.HasIndex("ReplyToMessageId");
b.ToTable("Messages");
});
modelBuilder.Entity("Govor.Core.Models.MessageReaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<DateTime>("ReactedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReactionCode")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("MessageId", "UserId")
.IsUnique();
b.ToTable("MessageReactions");
});
modelBuilder.Entity("Govor.Core.Models.MessageView", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<DateTime>("ViewedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("MessageId", "UserId")
.IsUnique();
b.ToTable("MessageViews");
});
modelBuilder.Entity("Govor.Core.Models.PrivateChat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("UserAId")
.HasColumnType("uuid");
b.Property<Guid>("UserBId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("PrivateChats");
});
modelBuilder.Entity("Govor.Core.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateOnly>("CreatedOn")
.HasColumnType("date");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("IconId")
.HasColumnType("uuid");
b.Property<Guid>("InviteId")
.HasColumnType("uuid");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("WasOnline")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("InviteId");
b.ToTable("Users");
});
modelBuilder.Entity("Govor.Core.Models.Admin", b =>
{
b.HasOne("Govor.Core.Models.User", "User")
.WithOne()
.HasForeignKey("Govor.Core.Models.Admin", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Govor.Core.Models.Friendship", b =>
{
b.HasOne("Govor.Core.Models.User", "Addressee")
.WithMany("ReceivedFriendRequests")
.HasForeignKey("AddresseeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Govor.Core.Models.User", "Requester")
.WithMany("SentFriendRequests")
.HasForeignKey("RequesterId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Addressee");
b.Navigation("Requester");
});
modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b =>
{
b.HasOne("Govor.Core.Models.Message", "Message")
.WithMany("MediaAttachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Message");
});
modelBuilder.Entity("Govor.Core.Models.Message", b =>
{
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 =>
{
b.HasOne("Govor.Core.Models.Message", "Message")
.WithMany("Reactions")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Govor.Core.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Message");
b.Navigation("User");
});
modelBuilder.Entity("Govor.Core.Models.MessageView", b =>
{
b.HasOne("Govor.Core.Models.Message", null)
.WithMany("MessageViews")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Govor.Core.Models.User", b =>
{
b.HasOne("Govor.Core.Models.Invitation", "Invite")
.WithMany("Users")
.HasForeignKey("InviteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Invite");
});
modelBuilder.Entity("Govor.Core.Models.Invitation", b =>
{
b.Navigation("Users");
});
modelBuilder.Entity("Govor.Core.Models.Message", b =>
{
b.Navigation("MediaAttachments");
b.Navigation("MessageViews");
b.Navigation("Reactions");
});
modelBuilder.Entity("Govor.Core.Models.PrivateChat", b =>
{
b.Navigation("Messages");
});
modelBuilder.Entity("Govor.Core.Models.User", b =>
{
b.Navigation("ReceivedFriendRequests");
b.Navigation("SentFriendRequests");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Govor.Data.Migrations
{
/// <inheritdoc />
public partial class friendships : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "PrivateChatId",
table: "Messages",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "Friendships",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
RequesterId = table.Column<Guid>(type: "uuid", nullable: false),
AddresseeId = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Friendships", x => x.Id);
table.ForeignKey(
name: "FK_Friendships_Users_AddresseeId",
column: x => x.AddresseeId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Friendships_Users_RequesterId",
column: x => x.RequesterId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "PrivateChats",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserAId = table.Column<Guid>(type: "uuid", nullable: false),
UserBId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PrivateChats", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Messages_PrivateChatId",
table: "Messages",
column: "PrivateChatId");
migrationBuilder.CreateIndex(
name: "IX_Friendships_AddresseeId",
table: "Friendships",
column: "AddresseeId");
migrationBuilder.CreateIndex(
name: "IX_Friendships_RequesterId",
table: "Friendships",
column: "RequesterId");
migrationBuilder.AddForeignKey(
name: "FK_Messages_PrivateChats_PrivateChatId",
table: "Messages",
column: "PrivateChatId",
principalTable: "PrivateChats",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Messages_PrivateChats_PrivateChatId",
table: "Messages");
migrationBuilder.DropTable(
name: "Friendships");
migrationBuilder.DropTable(
name: "PrivateChats");
migrationBuilder.DropIndex(
name: "IX_Messages_PrivateChatId",
table: "Messages");
migrationBuilder.DropColumn(
name: "PrivateChatId",
table: "Messages");
}
}
}
@@ -62,6 +62,30 @@ namespace Govor.Data.Migrations
b.ToTable("ChatGroups"); b.ToTable("ChatGroups");
}); });
modelBuilder.Entity("Govor.Core.Models.Friendship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AddresseeId")
.HasColumnType("uuid");
b.Property<Guid>("RequesterId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AddresseeId");
b.HasIndex("RequesterId");
b.ToTable("Friendships");
});
modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -181,6 +205,9 @@ namespace Govor.Data.Migrations
b.Property<bool>("IsEdited") b.Property<bool>("IsEdited")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<Guid?>("PrivateChatId")
.HasColumnType("uuid");
b.Property<Guid>("RecipientId") b.Property<Guid>("RecipientId")
.HasColumnType("uuid"); .HasColumnType("uuid");
@@ -198,6 +225,8 @@ namespace Govor.Data.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("PrivateChatId");
b.HasIndex("ReplyToMessageId"); b.HasIndex("ReplyToMessageId");
b.ToTable("Messages"); b.ToTable("Messages");
@@ -256,6 +285,23 @@ namespace Govor.Data.Migrations
b.ToTable("MessageViews"); b.ToTable("MessageViews");
}); });
modelBuilder.Entity("Govor.Core.Models.PrivateChat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("UserAId")
.HasColumnType("uuid");
b.Property<Guid>("UserBId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("PrivateChats");
});
modelBuilder.Entity("Govor.Core.Models.User", b => modelBuilder.Entity("Govor.Core.Models.User", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -304,6 +350,25 @@ namespace Govor.Data.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("Govor.Core.Models.Friendship", b =>
{
b.HasOne("Govor.Core.Models.User", "Addressee")
.WithMany("ReceivedFriendRequests")
.HasForeignKey("AddresseeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Govor.Core.Models.User", "Requester")
.WithMany("SentFriendRequests")
.HasForeignKey("RequesterId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Addressee");
b.Navigation("Requester");
});
modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b =>
{ {
b.HasOne("Govor.Core.Models.Message", "Message") b.HasOne("Govor.Core.Models.Message", "Message")
@@ -317,6 +382,10 @@ namespace Govor.Data.Migrations
modelBuilder.Entity("Govor.Core.Models.Message", b => modelBuilder.Entity("Govor.Core.Models.Message", b =>
{ {
b.HasOne("Govor.Core.Models.PrivateChat", null)
.WithMany("Messages")
.HasForeignKey("PrivateChatId");
b.HasOne("Govor.Core.Models.Message", "ReplyToMessage") b.HasOne("Govor.Core.Models.Message", "ReplyToMessage")
.WithMany() .WithMany()
.HasForeignKey("ReplyToMessageId") .HasForeignKey("ReplyToMessageId")
@@ -377,6 +446,18 @@ namespace Govor.Data.Migrations
b.Navigation("Reactions"); b.Navigation("Reactions");
}); });
modelBuilder.Entity("Govor.Core.Models.PrivateChat", b =>
{
b.Navigation("Messages");
});
modelBuilder.Entity("Govor.Core.Models.User", b =>
{
b.Navigation("ReceivedFriendRequests");
b.Navigation("SentFriendRequests");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }
@@ -22,6 +22,9 @@ public class UsersRepository : IUsersRepository
{ {
return await _context.Users return await _context.Users
.AsNoTracking() .AsNoTracking()
.Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.ToListOrThrowIfEmpty(new NotFoundException("Users in Database not exists")); .ToListOrThrowIfEmpty(new NotFoundException("Users in Database not exists"));
} }
@@ -33,6 +36,8 @@ public class UsersRepository : IUsersRepository
return await _context.Users return await _context.Users
.AsNoTracking() .AsNoTracking()
.Include(u => u.Invite) .Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.FirstOrDefaultAsync(x => x.Id == id) .FirstOrDefaultAsync(x => x.Id == id)
?? throw new NotFoundByKeyException<Guid>(id, "User with given id does not exist"); ?? throw new NotFoundByKeyException<Guid>(id, "User with given id does not exist");
} }
@@ -46,6 +51,8 @@ public class UsersRepository : IUsersRepository
.AsNoTracking() .AsNoTracking()
.Where(x => ids.Contains(x.Id)) .Where(x => ids.Contains(x.Id))
.Include(u => u.Invite) .Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.ToListOrThrowIfEmpty(new NotFoundByKeyException<IEnumerable<Guid>>(ids,"Users with given ids not found")); .ToListOrThrowIfEmpty(new NotFoundByKeyException<IEnumerable<Guid>>(ids,"Users with given ids not found"));
} }
@@ -57,10 +64,30 @@ public class UsersRepository : IUsersRepository
return await _context.Users return await _context.Users
.AsNoTracking() .AsNoTracking()
.Include(u => u.Invite) .Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.FirstOrDefaultAsync(x => x.Username == username) .FirstOrDefaultAsync(x => x.Username == username)
?? throw new NotFoundByKeyException<string>(username, "User with given username does not exist"); ?? throw new NotFoundByKeyException<string>(username, "User with given username does not exist");
} }
public async Task<List<User>> SearchPotentialFriendsAsync(Guid currentUserId, string query)
{
return await _context.Users
.AsNoTracking()
.Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.Where(u => u.Id != currentUserId &&
u.Username.ToLower().Contains(query.ToLower()) &&
!_context.Friendships.Any(f =>
(f.RequesterId == currentUserId && f.AddresseeId == u.Id) ||
(f.RequesterId == u.Id && f.AddresseeId == currentUserId)))
.Take(20)
.OrderBy(u => u.Username)
.ToListAsync();
}
public async Task<List<User>> FindByRangeUsernamesAsync(IEnumerable<string> usernames) public async Task<List<User>> FindByRangeUsernamesAsync(IEnumerable<string> usernames)
{ {
if (usernames is null || !usernames.Any()) if (usernames is null || !usernames.Any())
@@ -70,6 +97,8 @@ public class UsersRepository : IUsersRepository
.AsNoTracking() .AsNoTracking()
.Where(x => usernames.Contains(x.Username)) .Where(x => usernames.Contains(x.Username))
.Include(u => u.Invite) .Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.ToListOrThrowIfEmpty(new NotFoundByKeyException<IEnumerable<string>>(usernames, "Users with given usernames not found")); .ToListOrThrowIfEmpty(new NotFoundByKeyException<IEnumerable<string>>(usernames, "Users with given usernames not found"));
} }
@@ -82,6 +111,8 @@ public class UsersRepository : IUsersRepository
.AsNoTracking() .AsNoTracking()
.Where(u => createdDate == u.CreatedOn) .Where(u => createdDate == u.CreatedOn)
.Include(u => u.Invite) .Include(u => u.Invite)
.Include(u => u.ReceivedFriendRequests)
.Include(u => u.SentFriendRequests)
.ToListOrThrowIfEmpty(new NotFoundByKeyException<DateOnly>(createdDate, "Users with given created date do not exist")); .ToListOrThrowIfEmpty(new NotFoundByKeyException<DateOnly>(createdDate, "Users with given created date do not exist"));
} }