diff --git a/Govor.API/Controllers/ChatLoadController.cs b/Govor.API/Controllers/ChatLoadController.cs index fd9a537..9a18d3c 100644 --- a/Govor.API/Controllers/ChatLoadController.cs +++ b/Govor.API/Controllers/ChatLoadController.cs @@ -1,4 +1,7 @@ +using AutoMapper; using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Contracts.Responses; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -9,8 +12,85 @@ namespace Govor.API.Controllers; [Route("api/chats")] public class ChatLoadController : Controller { - public ChatLoadController(ILogger logger, IMessagesLoader messagesLoader) + private readonly ICurrentUserService _currentUser; + private readonly ILogger _logger; + private readonly IMessagesLoader _messagesLoader; + private readonly IMapper _mapper; + public ChatLoadController( + ILogger logger, + IMessagesLoader messagesLoader, + ICurrentUserService currentUser, + IMapper mapper) { - + _logger = logger; + _messagesLoader = messagesLoader; + _currentUser = currentUser; + _mapper = mapper; } + + [HttpGet("group-messages")] + public async Task GetChatMessages( + [FromQuery] Guid chatId, + [FromQuery] Guid? startMessageId, + [FromQuery] int pageSize = 20) + { + try + { + var result = await _messagesLoader.LoadLastMessagesInChatGroup( + chatId, + _currentUser.GetCurrentUserId(), + startMessageId, + pageSize); + + var response = _mapper.Map>(result); + + return Ok(response); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex.Message); + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } + + [HttpGet("user-messages")] + public async Task GetUserMessages( + [FromQuery] Guid userId, + [FromQuery] Guid? startMessageId, + [FromQuery] int pageSize = 20) + { + try + { + var result = await _messagesLoader.LoadLastMessagesInUserChat( + userId, + _currentUser.GetCurrentUserId(), + startMessageId, + pageSize); + + var response = _mapper.Map>(result); + + return Ok(response); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex.Message); + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, "Unexpected Error! Please try again later."); + } + } + } \ No newline at end of file diff --git a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs index 29fc7ee..02e86e3 100644 --- a/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs +++ b/Govor.API/Controllers/Friends/FriendsRequestQueryController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Govor.Application.Interfaces.Friends; using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Contracts.DTOs; @@ -15,12 +16,15 @@ public class FriendsRequestQueryController : Controller private readonly ILogger _logger; private readonly IFriendRequestQueryService _friendsService; private readonly ICurrentUserService _currentUserService; - + private readonly IMapper _mapper; + public FriendsRequestQueryController(ILogger logger, IFriendRequestQueryService friendsService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + IMapper mapper) { _logger = logger; + _mapper = mapper; _friendsService = friendsService; _currentUserService = currentUserService; } @@ -31,7 +35,9 @@ public class FriendsRequestQueryController : Controller try { var result = await _friendsService.GetIncomingAsync(_currentUserService.GetCurrentUserId()); - return Ok(BuildFriendshipDtos(result)); + var response = _mapper.Map>(result); + + return Ok(response); } catch (InvalidOperationException ex) { diff --git a/Govor.API/Controllers/Friends/FriendshipController.cs b/Govor.API/Controllers/Friends/FriendshipController.cs index e8bb89d..fa6cd10 100644 --- a/Govor.API/Controllers/Friends/FriendshipController.cs +++ b/Govor.API/Controllers/Friends/FriendshipController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Govor.Application.Exceptions.FriendsService; using Govor.Application.Interfaces.Friends; using Govor.Application.Interfaces.Infrastructure.Extensions; @@ -12,14 +13,16 @@ public class FriendshipController : Controller { private readonly ILogger _logger; private readonly IFriendshipService _friendsService; - //private readonly IUserDtoBuilder _builder; private readonly ICurrentUserService _currentUserService; - + private readonly IMapper _mapper; + public FriendshipController(ILogger logger, IFriendshipService friendsService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + IMapper mapper) { _logger = logger; + _mapper = mapper; _friendsService = friendsService; _currentUserService = currentUserService; } @@ -33,7 +36,10 @@ public class FriendshipController : Controller try { var result = await _friendsService.SearchUsersAsync(query, _currentUserService.GetCurrentUserId()); - return Ok(BuildUserDtos(result)); + + var response = _mapper.Map>(result); + + return Ok(response); } catch (SearchUsersException ex) { @@ -53,7 +59,10 @@ public class FriendshipController : Controller try { var result = await _friendsService.GetFriendsAsync(_currentUserService.GetCurrentUserId()); - return Ok(BuildUserDtos(result)); + + var response = _mapper.Map>(result); + + return Ok(response); } catch (InvalidOperationException ex) { @@ -66,14 +75,4 @@ public class FriendshipController : Controller return StatusCode(500, new { error = "Internal server error." }); } } - - private List BuildUserDtos(IEnumerable users) => users.Select(user => new UserDto - { - Id = user.Id, - Username = user.Username, - Description = user.Description, - WasOnline = user.WasOnline, - IconId = user.IconId - }).ToList(); - } \ No newline at end of file diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index b687326..c0b5812 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -65,6 +65,9 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // Auto Mapper + services.AddAutoMapper(typeof(MappingProfile)); } public static void AddRepositories(this IServiceCollection services) diff --git a/Govor.API/Extensions/MappingProfile.cs b/Govor.API/Extensions/MappingProfile.cs new file mode 100644 index 0000000..69045d6 --- /dev/null +++ b/Govor.API/Extensions/MappingProfile.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Govor.Contracts.DTOs; +using Govor.Contracts.Responses; +using Govor.Core.Models; +using Govor.Core.Models.Messages; +using Govor.Core.Models.Users; + +namespace Govor.API.Extensions; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Govor.API/Govor.API.csproj b/Govor.API/Govor.API.csproj index b88211f..c7f5397 100644 --- a/Govor.API/Govor.API.csproj +++ b/Govor.API/Govor.API.csproj @@ -7,6 +7,8 @@ + + diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index cdd9d73..5db3c95 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -193,16 +193,25 @@ public class ChatsHub : Hub request.MessageId, request.NewEncryptedContent, DateTime.UtcNow); - + try { var result = await _messageCommandService.EditMessageAsync(editMessageParam); - - if(!result.IsSuccess) - return LogAndError(editor, request.MessageId, "Edit message error", result.Exception); - + + if (!result.IsSuccess) + return LogAndError(editor, request.MessageId, "Edit message error", + result.Exception); + return HubResult.Ok(); } + catch (UnauthorizedAccessException ex) + { + return LogAndUnauthorized(ex, "Unauthorized editing", editor, request.MessageId); + } + catch (KeyNotFoundException ex) + { + return LogAndNotFound(ex, "Message not found", editor, request.MessageId); + } catch (Exception ex) { return LogAndError(editor, request.MessageId, "Unhandled exception error", ex); diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index cacd149..1b4b4cb 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -96,6 +96,7 @@ builder.Services.AddSwaggerGen(options => }); }); + //builder.Services.AddOpenApi(); var app = builder.Build(); @@ -104,7 +105,7 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { //app.MapOpenApi(); - + } app.UseSwagger(); diff --git a/Govor.Application/Services/Messages/MessagesLoader.cs b/Govor.Application/Services/Messages/MessagesLoader.cs index 3734002..541a400 100644 --- a/Govor.Application/Services/Messages/MessagesLoader.cs +++ b/Govor.Application/Services/Messages/MessagesLoader.cs @@ -1,24 +1,52 @@ using Govor.Application.Interfaces; +using Govor.Core.Infrastructure.Extensions; using Govor.Core.Models.Messages; using Govor.Core.Repositories.Groups; using Govor.Core.Repositories.Messages; +using Govor.Core.Repositories.PrivateChats; +using Govor.Data; using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; namespace Govor.Application.Services.Messages; public class MessagesLoader : IMessagesLoader { - private IVerifyFriendship _friendship; private IGroupsRepository _groupsRepository; - private IMessagesRepository _messagesRepository; + private IPrivateChatsRepository _privateChatsRepository; + private GovorDbContext _dbContext; + + public MessagesLoader( + IGroupsRepository groupsRepository, + IPrivateChatsRepository privateChatsRepository, + GovorDbContext dbContext) + { + _groupsRepository = groupsRepository; + _privateChatsRepository = privateChatsRepository; + _dbContext = dbContext; + } public async Task> LoadLastMessagesInUserChat(Guid userId, Guid currentUser, Guid? startMessageId, int pageSize = 20) { - await _friendship.VerifyAsync(userId, currentUser); + if(userId == Guid.Empty) + throw new ArgumentException("User id cannot be empty"); + + if(!_privateChatsRepository.Exist(userId, currentUser)) + throw new InvalidOperationException("Private chat not found"); + try { - throw new NotImplementedException(); - //return await _groups.GetMessages(userId, startMessageId, pageSize); + var chat = await _privateChatsRepository.GetByMembersAsync(userId, currentUser); + + return await _dbContext.Messages + .AsNoTracking() + .Include(m => m.MediaAttachments) + .ThenInclude(m => m.MediaFile) + .AsSplitQuery() + .Where(m => m.RecipientType == RecipientType.User && + m.RecipientId == chat.Id) + .Take(pageSize) + .ToListOrThrowIfEmpty(new NotFoundException("Messages not found")); } catch (NotFoundException ex) { @@ -28,12 +56,23 @@ public class MessagesLoader : IMessagesLoader public async Task> LoadLastMessagesInChatGroup(Guid chatId, Guid currentUser, Guid? startMessageId, int pageSize = 20) { + if(chatId == Guid.Empty) + throw new ArgumentException("Chat id cannot be empty"); + 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); + return await _dbContext.Messages + .AsNoTracking() + .Include(m => m.MediaAttachments) + .ThenInclude(m => m.MediaFile) + .AsSplitQuery() + .Where(m => m.RecipientType == RecipientType.Group && + m.RecipientId == chatId) + .Take(pageSize) + .ToListOrThrowIfEmpty(new NotFoundException("Messages not found")); } catch (NotFoundException ex) { diff --git a/Govor.Console/Program.cs b/Govor.Console/Program.cs index bc21795..2247a05 100644 --- a/Govor.Console/Program.cs +++ b/Govor.Console/Program.cs @@ -28,8 +28,8 @@ namespace Govor.ConsoleClient { class Program { - //static string baseUrl = "https://govor-team-govor-88b3.twc1.net"; - static string baseUrl = "https://localhost:7155"; + static string baseUrl = "https://govor-team-govor-88b3.twc1.net"; + //static string baseUrl = "https://localhost:7155"; static string? AuthToken = null; static HttpClientService HttpService = new(baseUrl); private static FriendsClient? _friendsClient; // Renamed to avoid conflict with BaseCommand @@ -42,6 +42,12 @@ namespace Govor.ConsoleClient static async Task Main() { + FriendsHubClient client = new FriendsHubClient($"{baseUrl}/hubs/friends", "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDRjNzkyMi1hNjdlLTRlMzctYWI0MC0wNThlZDE5NzM5NjMiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsImV4cCI6MTc1MjMzMjE2NX0.mGSjSCvguEXkdHqHcbZ3eUH0S8c82kz3XP89potEh1k"); + + await client.StartAsync(); + await client.AcceptRequest(Guid.Parse("3ba77c0c-7522-47be-8e77-ea47bd6c6e69")); + + return; InitializeCommands(); BaseCommand.InitializeServices( _friendsClient, // Initially null, will be set by Login/Register commands diff --git a/Govor.Contracts/Responses/MediaAttachmentResponse.cs b/Govor.Contracts/Responses/MediaAttachmentResponse.cs new file mode 100644 index 0000000..17a2600 --- /dev/null +++ b/Govor.Contracts/Responses/MediaAttachmentResponse.cs @@ -0,0 +1,8 @@ +namespace Govor.Contracts.Responses; + +public class MediaAttachmentResponse +{ + public Guid Id { get; set; } + public Guid MessageId { get; set; } + public Guid MediaFileId { get; set; } +} \ No newline at end of file diff --git a/Govor.Contracts/Responses/MessageReactionResponse.cs b/Govor.Contracts/Responses/MessageReactionResponse.cs new file mode 100644 index 0000000..d512e7d --- /dev/null +++ b/Govor.Contracts/Responses/MessageReactionResponse.cs @@ -0,0 +1,10 @@ +namespace Govor.Contracts.Responses; + +public class MessageReactionResponse +{ + public Guid Id { get; set; } + public Guid MessageId { get; set; } + public Guid UserId { get; set; } + public string ReactionCode { get; set; } // "❤️", "🔥", "👍", ":custom_emoji:" + public DateTime ReactedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Govor.Contracts/Responses/MessageResponse.cs b/Govor.Contracts/Responses/MessageResponse.cs new file mode 100644 index 0000000..60ea4ba --- /dev/null +++ b/Govor.Contracts/Responses/MessageResponse.cs @@ -0,0 +1,20 @@ +using Govor.Core.Models.Messages; + +namespace Govor.Contracts.Responses; + +public class MessageResponse +{ + public Guid Id { get; set; } + public Guid SenderId { get; set; } + public Guid RecipientId { get; set; } // or GroupId + public RecipientType RecipientType { get; set; } + public string EncryptedContent { get; set; } = string.Empty; + public DateTime SentAt { get; set; } + public bool IsEdited { get; set; } = false; + public DateTime? EditedAt { get; set; } + public Guid? ReplyToMessageId { get; set; } + + public List MediaAttachments { get; set; } = new(); + public List Reactions { get; set; } = new(); + public List MessageViews { get; set; } = new(); +} \ No newline at end of file diff --git a/Govor.Contracts/Responses/MessageViewResponse.cs b/Govor.Contracts/Responses/MessageViewResponse.cs new file mode 100644 index 0000000..f988a4e --- /dev/null +++ b/Govor.Contracts/Responses/MessageViewResponse.cs @@ -0,0 +1,9 @@ +namespace Govor.Contracts.Responses; + +public class MessageViewResponse +{ + public Guid Id { get; set; } + public Guid MessageId { get; set; } + public Guid UserId { get; set; } + public DateTime ViewedAt { get; set; } +} \ No newline at end of file diff --git a/Govor.Contracts/Responses/SignalR/HubResult.cs b/Govor.Contracts/Responses/SignalR/HubResult.cs index a75ca80..0875275 100644 --- a/Govor.Contracts/Responses/SignalR/HubResult.cs +++ b/Govor.Contracts/Responses/SignalR/HubResult.cs @@ -62,7 +62,7 @@ public class HubResult }; } -public enum HubResultStatus +public enum HubResultStatus : int { Success = 200, Created = 201, diff --git a/Govor.Core/Models/ChatGroup.cs b/Govor.Core/Models/ChatGroup.cs index beea1ee..406854e 100644 --- a/Govor.Core/Models/ChatGroup.cs +++ b/Govor.Core/Models/ChatGroup.cs @@ -1,3 +1,5 @@ +using Govor.Core.Models.Messages; + namespace Govor.Core.Models; public class ChatGroup @@ -11,7 +13,8 @@ public class ChatGroup public List Admins { get; set; } = new(); public List Members { get; set; } = new(); public List InviteCodes { get; set; } = new(); - + public List Messages { get; set; } = new(); + public override bool Equals(object? obj) { ChatGroup chatGroup = obj as ChatGroup; diff --git a/Govor.Core/Models/PrivateChat.cs b/Govor.Core/Models/PrivateChat.cs index df8cc29..e043a92 100644 --- a/Govor.Core/Models/PrivateChat.cs +++ b/Govor.Core/Models/PrivateChat.cs @@ -7,7 +7,7 @@ public class PrivateChat public Guid Id { get; set; } public Guid UserAId { get; set; } public Guid UserBId { get; set; } - public List Messages { get; set; } = new List(); + public List Messages { get; set; } = new(); public override bool Equals(object? obj) { diff --git a/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs b/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs index de632d6..7a9829c 100644 --- a/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs +++ b/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs @@ -1,3 +1,5 @@ +using Govor.Core.Repositories.Groups; + namespace Govor.Core.Repositories.PrivateChats; public interface IPrivateChatsRepository : IPrivateChatsReader, IPrivateChatsWriter, IPrivateChatsExist diff --git a/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.Designer.cs b/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.Designer.cs new file mode 100644 index 0000000..a320abd --- /dev/null +++ b/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.Designer.cs @@ -0,0 +1,593 @@ +// +using System; +using Govor.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Govor.Data.Migrations +{ + [DbContext(typeof(GovorDbContext))] + [Migration("20250712090733_AddChatGroupIdToMessage")] + partial class AddChatGroupIdToMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ImageId") + .HasColumnType("char(36)"); + + b.Property("IsChannel") + .HasColumnType("tinyint(1)"); + + b.Property("IsPrivate") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.ToTable("ChatGroups"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AddresseeId") + .HasColumnType("char(36)"); + + b.Property("RequesterId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AddresseeId"); + + b.HasIndex("RequesterId"); + + b.ToTable("Friendships"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupAdmins", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupAdmins"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.Property("UserMakerId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("UserMakerId"); + + b.ToTable("GroupInvitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InvitationId") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("IsBanned") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("InvitationId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("MaxParticipants") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Invitations"); + }); + + modelBuilder.Entity("Govor.Core.Models.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MineType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MediaFileId") + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatGroupId") + .HasColumnType("char(36)"); + + b.Property("EditedAt") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedContent") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsEdited") + .HasColumnType("tinyint(1)"); + + b.Property("PrivateChatId") + .HasColumnType("char(36)"); + + b.Property("RecipientId") + .HasColumnType("char(36)"); + + b.Property("RecipientType") + .HasColumnType("int"); + + b.Property("ReplyToMessageId") + .HasColumnType("char(36)"); + + b.Property("SenderId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ChatGroupId"); + + b.HasIndex("PrivateChatId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.Property("ReactedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReactionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageReactions"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("ViewedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique(); + + b.ToTable("MessageViews"); + }); + + modelBuilder.Entity("Govor.Core.Models.PrivateChat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("UserAId") + .HasColumnType("char(36)"); + + b.Property("UserBId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("PrivateChats"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedOn") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IconId") + .HasColumnType("char(36)"); + + b.Property("InviteId") + .HasColumnType("char(36)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WasOnline") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("InviteId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Friendship", b => + { + b.HasOne("Govor.Core.Models.Users.User", "Addressee") + .WithMany("ReceivedFriendRequests") + .HasForeignKey("AddresseeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "Requester") + .WithMany("SentFriendRequests") + .HasForeignKey("RequesterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Addressee"); + + 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.Users.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.Messages.MediaAttachments", b => + { + b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") + .WithMany() + .HasForeignKey("MediaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("MediaAttachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaFile"); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Messages") + .HasForeignKey("ChatGroupId"); + + b.HasOne("Govor.Core.Models.PrivateChat", null) + .WithMany("Messages") + .HasForeignKey("PrivateChatId"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => + { + b.HasOne("Govor.Core.Models.Messages.Message", null) + .WithMany("MessageViews") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Users.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.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.ChatGroup", b => + { + b.Navigation("Admins"); + + b.Navigation("InviteCodes"); + + b.Navigation("Members"); + + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Govor.Core.Models.Invitation", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.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.Users.User", b => + { + b.Navigation("ReceivedFriendRequests"); + + b.Navigation("SentFriendRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.cs b/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.cs new file mode 100644 index 0000000..6eaab86 --- /dev/null +++ b/Govor.Data/Migrations/20250712090733_AddChatGroupIdToMessage.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Govor.Data.Migrations +{ + /// + public partial class AddChatGroupIdToMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ChatGroupId", + table: "Messages", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.CreateIndex( + name: "IX_Messages_ChatGroupId", + table: "Messages", + column: "ChatGroupId"); + + migrationBuilder.AddForeignKey( + name: "FK_Messages_ChatGroups_ChatGroupId", + table: "Messages", + column: "ChatGroupId", + principalTable: "ChatGroups", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Messages_ChatGroups_ChatGroupId", + table: "Messages"); + + migrationBuilder.DropIndex( + name: "IX_Messages_ChatGroupId", + table: "Messages"); + + migrationBuilder.DropColumn( + name: "ChatGroupId", + table: "Messages"); + } + } +} diff --git a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs index 13582b2..6937a5e 100644 --- a/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs +++ b/Govor.Data/Migrations/GovorDbContextModelSnapshot.cs @@ -22,16 +22,6 @@ namespace Govor.Data.Migrations MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - modelBuilder.Entity("Govor.Core.Models.Admin", b => - { - b.Property("UserId") - .HasColumnType("char(36)"); - - b.HasKey("UserId"); - - b.ToTable("Admins"); - }); - modelBuilder.Entity("Govor.Core.Models.ChatGroup", b => { b.Property("Id") @@ -207,27 +197,6 @@ namespace Govor.Data.Migrations b.ToTable("Invitations"); }); - modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("char(36)"); - - b.Property("MediaFileId") - .HasColumnType("char(36)"); - - b.Property("MessageId") - .HasColumnType("char(36)"); - - b.HasKey("Id"); - - b.HasIndex("MediaFileId"); - - b.HasIndex("MessageId"); - - b.ToTable("MediaAttachments"); - }); - modelBuilder.Entity("Govor.Core.Models.MediaFile", b => { b.Property("Id") @@ -255,12 +224,36 @@ namespace Govor.Data.Migrations b.ToTable("MediaFiles"); }); - modelBuilder.Entity("Govor.Core.Models.Message", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("MediaFileId") + .HasColumnType("char(36)"); + + b.Property("MessageId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MediaFileId"); + + b.HasIndex("MessageId"); + + b.ToTable("MediaAttachments"); + }); + + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatGroupId") + .HasColumnType("char(36)"); + b.Property("EditedAt") .HasColumnType("datetime(6)"); @@ -291,12 +284,14 @@ namespace Govor.Data.Migrations b.HasKey("Id"); + b.HasIndex("ChatGroupId"); + b.HasIndex("PrivateChatId"); b.ToTable("Messages"); }); - modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -326,7 +321,7 @@ namespace Govor.Data.Migrations b.ToTable("MessageReactions"); }); - modelBuilder.Entity("Govor.Core.Models.MessageView", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -366,7 +361,17 @@ namespace Govor.Data.Migrations b.ToTable("PrivateChats"); }); - modelBuilder.Entity("Govor.Core.Models.User", b => + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("UserId"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -403,26 +408,15 @@ namespace Govor.Data.Migrations 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") + b.HasOne("Govor.Core.Models.Users.User", "Addressee") .WithMany("ReceivedFriendRequests") .HasForeignKey("AddresseeId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("Govor.Core.Models.User", "Requester") + b.HasOne("Govor.Core.Models.Users.User", "Requester") .WithMany("SentFriendRequests") .HasForeignKey("RequesterId") .OnDelete(DeleteBehavior.Restrict) @@ -450,7 +444,7 @@ namespace Govor.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Govor.Core.Models.User", "UserMaker") + b.HasOne("Govor.Core.Models.Users.User", "UserMaker") .WithMany() .HasForeignKey("UserMakerId") .OnDelete(DeleteBehavior.Restrict) @@ -474,7 +468,7 @@ namespace Govor.Data.Migrations .IsRequired(); }); - modelBuilder.Entity("Govor.Core.Models.MediaAttachments", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MediaAttachments", b => { b.HasOne("Govor.Core.Models.MediaFile", "MediaFile") .WithMany() @@ -482,7 +476,7 @@ namespace Govor.Data.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); - b.HasOne("Govor.Core.Models.Message", "Message") + b.HasOne("Govor.Core.Models.Messages.Message", "Message") .WithMany("MediaAttachments") .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) @@ -493,22 +487,26 @@ namespace Govor.Data.Migrations b.Navigation("Message"); }); - modelBuilder.Entity("Govor.Core.Models.Message", b => + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => { + b.HasOne("Govor.Core.Models.ChatGroup", null) + .WithMany("Messages") + .HasForeignKey("ChatGroupId"); + b.HasOne("Govor.Core.Models.PrivateChat", null) .WithMany("Messages") .HasForeignKey("PrivateChatId"); }); - modelBuilder.Entity("Govor.Core.Models.MessageReaction", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MessageReaction", b => { - b.HasOne("Govor.Core.Models.Message", "Message") + b.HasOne("Govor.Core.Models.Messages.Message", "Message") .WithMany("Reactions") .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Govor.Core.Models.User", "User") + b.HasOne("Govor.Core.Models.Users.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -519,16 +517,27 @@ namespace Govor.Data.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Govor.Core.Models.MessageView", b => + modelBuilder.Entity("Govor.Core.Models.Messages.MessageView", b => { - b.HasOne("Govor.Core.Models.Message", null) + b.HasOne("Govor.Core.Models.Messages.Message", null) .WithMany("MessageViews") .HasForeignKey("MessageId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Govor.Core.Models.User", b => + modelBuilder.Entity("Govor.Core.Models.Users.Admin", b => + { + b.HasOne("Govor.Core.Models.Users.User", "User") + .WithOne() + .HasForeignKey("Govor.Core.Models.Users.Admin", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.HasOne("Govor.Core.Models.Invitation", "Invite") .WithMany("Users") @@ -546,6 +555,8 @@ namespace Govor.Data.Migrations b.Navigation("InviteCodes"); b.Navigation("Members"); + + b.Navigation("Messages"); }); modelBuilder.Entity("Govor.Core.Models.Invitation", b => @@ -553,7 +564,7 @@ namespace Govor.Data.Migrations b.Navigation("Users"); }); - modelBuilder.Entity("Govor.Core.Models.Message", b => + modelBuilder.Entity("Govor.Core.Models.Messages.Message", b => { b.Navigation("MediaAttachments"); @@ -567,7 +578,7 @@ namespace Govor.Data.Migrations b.Navigation("Messages"); }); - modelBuilder.Entity("Govor.Core.Models.User", b => + modelBuilder.Entity("Govor.Core.Models.Users.User", b => { b.Navigation("ReceivedFriendRequests"); diff --git a/Govor.Data/Repositories/PrivateChatsRepository.cs b/Govor.Data/Repositories/PrivateChatsRepository.cs index 07836ec..616362c 100644 --- a/Govor.Data/Repositories/PrivateChatsRepository.cs +++ b/Govor.Data/Repositories/PrivateChatsRepository.cs @@ -1,6 +1,7 @@ using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; +using Govor.Core.Models.Messages; using Govor.Core.Repositories.PrivateChats; using Govor.Data.Repositories.Exceptions; using Microsoft.EntityFrameworkCore;