diff --git a/Govor.API/Controllers/InviteController.cs b/Govor.API/Controllers/InviteController.cs index 0da0c13..26ff900 100644 --- a/Govor.API/Controllers/InviteController.cs +++ b/Govor.API/Controllers/InviteController.cs @@ -1,4 +1,5 @@ -using Govor.API.Services; +using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Infrastructure.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -9,24 +10,36 @@ namespace Govor.API.Controllers; public class InviteController : ControllerBase { private readonly IGroupService _groupService; - - public InviteController(IGroupService groupService) + private readonly ILogger _logger; + private readonly ICurrentUserService _currentUser; + public InviteController(IGroupService groupService, + ILogger logger) { _groupService = groupService; + _logger = logger; } - [HttpGet("{code}")] [Authorize] + [HttpGet("{code}")] public IActionResult JoinGroup(string code) { - var group = _groupService.GetGroupByInvite(code); - if (group == null) return NotFound(); - - return Ok(new + try { - groupId = group.Id, - name = group.Name, - isChannel = group.IsChannel - }); + _groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code); + + var group = _groupService.GetGroupByInviteCode(code); + + return Ok(new + { + groupId = group.Id, + name = group.Name, + isChannel = group.IsChannel + }); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, ex.Message); + } } } diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index b61dba0..dd7f4eb 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -13,42 +13,138 @@ namespace Govor.API.Hubs; public class ChatsHub : Hub { private readonly ILogger _logger; - private readonly IChatService _chatService; - private readonly IGroupService _groupService; + private readonly IMessageService _messageService; - public ChatsHub(ILogger logger, - IChatService chatService - ) + public ChatsHub(ILogger logger, IMessageService messageService) { _logger = logger; - _chatService = chatService; + _messageService = messageService; } - + public override async Task OnConnectedAsync() { var userId = GetUserId(); - - if (userId != Guid.Empty) + if (userId == Guid.Empty) { - // Binding ConnectionId to UserId - await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); - _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId); + _logger.LogWarning("User connected with invalid UserID claim."); + Context.Abort(); // Abort connection if userID is invalid + return; } - + + // Add user to their own group (for private messages and notifications) + await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); + _logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group", userId, Context.ConnectionId); + + // TODO: Add user to their chat groups - this might require fetching user's groups from a service + // var userGroups = await _userService.GetUserGroupsAsync(userId); + // foreach (var group in userGroups) + // { + // await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}"); + // } + await base.OnConnectedAsync(); } - + public override async Task OnDisconnectedAsync(Exception? exception) { - var userId = GetUserId(); + var userId = GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early) if (userId != Guid.Empty) { + // Remove user from their own group await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); - _logger.LogInformation("User {UserId} disconnected", userId); + _logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, Context.ConnectionId); + + // TODO: Remove user from their chat groups + // var userGroups = await _userService.GetUserGroupsAsync(userId); // This might be problematic if the service relies on the user being connected + // foreach (var group in userGroups) + // { + // await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}"); + // } } + else if (exception != null) + { + _logger.LogWarning(exception, "User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}", Context.ConnectionId); + } + else + { + _logger.LogInformation("User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}", Context.ConnectionId); + } + await base.OnDisconnectedAsync(exception); } + public async Task Send(MessageRequest request) + { + if (string.IsNullOrWhiteSpace(request.EncryptedContent) && (request.MediaAttachments == null || !request.MediaAttachments.Any())) + { + _logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}", GetUserId()); + throw new ArgumentException("Message cannot be empty (must have content or attachments)."); + } + + var senderId = GetUserId(); + _logger.LogInformation("Message send initiated by {SenderId} to {RecipientId} of type {RecipientType}", senderId, request.RecipientId, request.RecipientType); + + var sendMessageParams = new SendMessage( + EncryptContent: request.EncryptedContent, + ReplyToMessageId: request.ReplyToMessageId, + FromUserId: senderId, + RecipientId: request.RecipientId, + RecipientType: request.RecipientType, + SendAt: DateTime.UtcNow, + Media: request.MediaAttachments?.Select(f => new SendMedia( + f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty() + ); + + try + { + var result = await _messageService.SendMessageAsync(sendMessageParams); + + if (!result.IsSuccess || result.MessageId == Guid.Empty) + { + _logger.LogError(result.Exception, "Failed to send message from {SenderId} to {RecipientId}. Error: {ErrorMessage}", senderId, request.RecipientId, result.Exception?.Message ?? "Unknown error"); + if (result.Exception != null) throw result.Exception; + throw new HubException("Failed to send message due to an internal error."); + } + + var messageResponse = new UserMessageResponse // Assuming a response DTO + { + MessageId = result.MessageId, + SenderId = senderId, + RecipientId = request.RecipientId, + RecipientType = request.RecipientType, + EncryptedContent = request.EncryptedContent, + SentAt = sendMessageParams.SendAt, + IsEdited = false, + MediaAttachments = request.MediaAttachments, + ReplyToMessageId = request.ReplyToMessageId + }; + + // Notify recipient (user or group) + if (request.RecipientType == RecipientType.User) + { + // Send to the recipient's personal group + await Clients.Group(request.RecipientId.ToString()).SendAsync("ReceiveMessage", messageResponse); + } + else if (request.RecipientType == RecipientType.Group) + { + // Send to all members of the group, including the sender if they are part of the group via a different connection + await Clients.Group($"group_{request.RecipientId}").SendAsync("ReceiveMessage", messageResponse); + } + + // Notify sender (confirmation) on their connection + await Clients.Caller.SendAsync("MessageSent", messageResponse); // Or use "ReceiveMessage" if the sender should also just get it like anyone else + + _logger.LogInformation("Message {MessageId} sent successfully from {SenderId} to {RecipientId} ({RecipientType})", result.MessageId, senderId, request.RecipientId, request.RecipientType); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId); + // Consider sending a specific error message to the caller instead of a generic HubException or rethrowing. + // For example: await Clients.Caller.SendAsync("SendMessageFailed", new { Error = ex.Message }); + throw new HubException("An error occurred while sending the message.", ex); + } + } + public async Task Remove(Guid recipientId, Guid messageId) { @@ -59,79 +155,19 @@ public class ChatsHub : Hub } - public async Task SendGroup(GroupMessageRequest request) - { - var senderId = GetUserId(); - - // Создание сообщения - var message = new SendMessage( - EncryptContent: request.EncryptedContent, - ReplyToMessageId: request.ReplyToMessageId, - FromUserId: senderId, - RecipientId: request.GroupId, - SendAt: DateTime.UtcNow, - Media: request.MediaAttachments?.Select(f => new SendMedia( - f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty()); - - var result = await _groupService.SendMessageAsync(message); - - if (!result.IsSuccess) - throw result.Exception!; - - // Шлём всем участникам группы, кроме отправителя - await Clients.GroupExcept($"group_{request.GroupId}", Context.ConnectionId) - .SendAsync("ReceiveGroupMessage", message); - - // Отправителю тоже - await Clients.Caller.SendAsync("ReceiveGroupMessage", message); - } - public async Task Send(MessageRequest request) - { - if (string.IsNullOrWhiteSpace(request.EncryptedContent)) - { - _logger.LogWarning("Empty message received from user {UserId}", GetUserId()); - throw new ArgumentException("Message cannot be empty", nameof(request.EncryptedContent)); - } - - var senderId = GetUserId(); - - try - { - _logger.LogInformation("Message sent from {SenderId} to {RecipientId} at {UtcNow}", senderId, request.RecipientId, DateTime.UtcNow); - - var message = new SendMessage( - EncryptContent: request.EncryptedContent, - ReplyToMessageId: request.ReplyToMessageId, - FromUserId: senderId, - RecipientId: request.RecipientId, - SendAt: DateTime.UtcNow, - Media: request.MediaAttachments?.Select(f => new SendMedia( - f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty()); - - Result result = await _chatService.SendMessageAsync(message); - if(result.IsSuccess == false) - throw result.Exception; - - // Sending a message to the sender and recipient - await Clients.Group(message.RecipientId.ToString()).SendAsync("Receive", message); - await Clients.Group(message.FromUserId.ToString()).SendAsync("Receive", message); - // TODO: Send to Group - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId); - throw; - } - } - private Guid GetUserId() + private Guid GetUserId(bool suppressException = false) { var userIdClaim = Context.User?.FindFirst("userID")?.Value; if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) { - _logger.LogError("Could not retrieve sender userId"); - throw new UnauthorizedAccessException("userID claim is missing or invalid"); + if (!suppressException) + { + _logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim); + throw new UnauthorizedAccessException("userID claim is missing or invalid."); + } + return Guid.Empty; } return userId; } diff --git a/Govor.Application/Interfaces/IGroupService.cs b/Govor.Application/Interfaces/IGroupService.cs index 91ed51e..e57119c 100644 --- a/Govor.Application/Interfaces/IGroupService.cs +++ b/Govor.Application/Interfaces/IGroupService.cs @@ -1,9 +1,16 @@ -using Govor.Application.Interfaces.Messages; +using Govor.Application.Interfaces.Messages.Parameters; using Govor.Core.Models; -namespace Govor.API.Services; +namespace Govor.Application.Interfaces; -public interface IGroupService : IMessageSendingService, IMessageManagementService +public interface IGroupService { - ChatGroup GetGroupByInvite(string code); + Task GetGroupByIdAsync(Guid groupId); + Task CreateGroupAsync(string name, Guid creatorId, IEnumerable initialMemberIds); + Task AddUserToGroupByInvitationAsync(Guid userId, string invitationCode); + Task RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId); + Task DeleteGroupAsync(Guid groupId, Guid userId); + Task> GetGroupMembersAsync(Guid groupId); + Task>GetUserGroupsAsync(Guid userId); + ChatGroup GetGroupByInviteCode(string code); } \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IChatService.cs b/Govor.Application/Interfaces/Messages/IChatService.cs deleted file mode 100644 index e44dc9b..0000000 --- a/Govor.Application/Interfaces/Messages/IChatService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Govor.Application.Interfaces.Messages; - -public interface IChatService : IMessageSendingService, IMessageManagementService -{ - -} - - diff --git a/Govor.Application/Interfaces/Messages/IMessageManagementService.cs b/Govor.Application/Interfaces/Messages/IMessageManagementService.cs deleted file mode 100644 index 9b35c43..0000000 --- a/Govor.Application/Interfaces/Messages/IMessageManagementService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Govor.Application.Interfaces.Messages.Parameters; - -namespace Govor.Application.Interfaces.Messages; - -public interface IMessageManagementService -{ - Task EditMessageAsync(Guid editorId, Guid messageId, string newContent); - Task DeleteMessageAsync(Guid editorId, Guid messageId); -} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageReactionService.cs b/Govor.Application/Interfaces/Messages/IMessageReactionService.cs deleted file mode 100644 index c8b624a..0000000 --- a/Govor.Application/Interfaces/Messages/IMessageReactionService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Govor.Application.Interfaces.Messages; - -public interface IMessageReactionService -{ - -} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageSendingService.cs b/Govor.Application/Interfaces/Messages/IMessageSendingService.cs deleted file mode 100644 index 1bee95f..0000000 --- a/Govor.Application/Interfaces/Messages/IMessageSendingService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Govor.Application.Interfaces.Messages.Parameters; - -namespace Govor.Application.Interfaces.Messages; - -public interface IMessageSendingService -{ - Task SendMessageAsync(SendMessage newMessage); -} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/IMessageService.cs b/Govor.Application/Interfaces/Messages/IMessageService.cs new file mode 100644 index 0000000..850afcb --- /dev/null +++ b/Govor.Application/Interfaces/Messages/IMessageService.cs @@ -0,0 +1,30 @@ +using Govor.Application.Interfaces.Messages.Parameters; +using Govor.Core.Models; + +namespace Govor.Application.Interfaces.Messages; + +// Combining IChatService and IGroupService functionalities relevant to messages +public interface IMessageService +{ + Task SendMessageAsync(SendMessage messageParameters); + Task EditMessageAsync(EditMessage messageParameters); + Task DeleteMessageAsync(DeleteMessage messageParameters); + + // Potentially other message-related methods like: + // Task GetMessagesAsync(Guid userId, Guid chatId, RecipientType chatType, int pageNumber, int pageSize); + // Task MarkMessageAsReadAsync(Guid userId, Guid messageId); +} + +// Define specific result types for clarity, including original message for notifications if needed + +public record SendMessageResult(bool IsSuccess, Exception? Exception, Guid MessageId) + : Result(IsSuccess, Exception, MessageId); + +public record EditMessageResult(bool IsSuccess, Exception? Exception, Message? OriginalMessage) + : Result(IsSuccess, Exception, OriginalMessage?.Id ?? Guid.Empty) +{ + // OriginalMessage can be useful for the Hub to know details like RecipientType, RecipientId for notifications +} + +public record DeleteMessageResult(bool IsSuccess, Exception? Exception, Message? OriginalMessage) + : Result(IsSuccess, Exception, OriginalMessage?.Id ?? Guid.Empty); diff --git a/Govor.Application/Interfaces/Messages/Parameters/DeleteMessage.cs b/Govor.Application/Interfaces/Messages/Parameters/DeleteMessage.cs new file mode 100644 index 0000000..ceed39d --- /dev/null +++ b/Govor.Application/Interfaces/Messages/Parameters/DeleteMessage.cs @@ -0,0 +1,5 @@ +namespace Govor.Application.Interfaces.Messages.Parameters; + +public record DeleteMessage( + Guid DeleterId, + Guid MessageId); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/EditMessage.cs b/Govor.Application/Interfaces/Messages/Parameters/EditMessage.cs new file mode 100644 index 0000000..4c0e6af --- /dev/null +++ b/Govor.Application/Interfaces/Messages/Parameters/EditMessage.cs @@ -0,0 +1,7 @@ +namespace Govor.Application.Interfaces.Messages.Parameters; + +public record EditMessage( + Guid EditorId, + Guid MessageId, + string NewContent, + DateTime EditedAt); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs b/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs index 06a002d..1d87a71 100644 --- a/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs +++ b/Govor.Application/Interfaces/Messages/Parameters/SendMedia.cs @@ -2,7 +2,4 @@ using Govor.Core.Models; namespace Govor.Application.Interfaces.Messages.Parameters; -public record SendMedia(Guid Id, - string EncryptedKey, - MediaType Type, - string MimeType); \ No newline at end of file +public record SendMedia(Guid MediaId, string EncryptedKey, MediaType Type, string MimeType); \ No newline at end of file diff --git a/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs b/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs index 23c7cc8..b302276 100644 --- a/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs +++ b/Govor.Application/Interfaces/Messages/Parameters/SendMessage.cs @@ -1,8 +1,11 @@ +using Govor.Core.Models; + namespace Govor.Application.Interfaces.Messages.Parameters; public record SendMessage( string EncryptContent, Guid? ReplyToMessageId, + RecipientType RecipientType, Guid RecipientId, Guid FromUserId, DateTime SendAt, diff --git a/Govor.Application/Services/MessageService.cs b/Govor.Application/Services/MessageService.cs new file mode 100644 index 0000000..7131404 --- /dev/null +++ b/Govor.Application/Services/MessageService.cs @@ -0,0 +1,186 @@ +using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Messages; +using Govor.Application.Interfaces.Messages.Parameters; +using Govor.Core.Models; +using Govor.Core.Repositories.Groups; +using Govor.Core.Repositories.Messages; +using Govor.Core.Repositories.Users; +using Microsoft.Extensions.Logging; + +namespace Govor.Application.Services; + +public class MessageService : IMessageService +{ + private readonly IMessagesRepository _messagesRepository; + private readonly IUsersRepository _usersRepository; // For validating user recipients + private readonly IGroupsRepository _groupsRepository; // For validating group recipients and fetching members + private readonly IVerifyFriendship _verifyFriendship; // For private messages + private readonly ILogger _logger; + + public MessageService( + IMessagesRepository messagesRepository, + IUsersRepository usersRepository, + IGroupsRepository groupsRepository, + IVerifyFriendship verifyFriendship, + ILogger logger) + { + _messagesRepository = messagesRepository; + _usersRepository = usersRepository; + _groupsRepository = groupsRepository; + _verifyFriendship = verifyFriendship; + _logger = logger; + } + + public async Task SendMessageAsync(SendMessage sendParams) + { + try + { + // Validate recipient + if (sendParams.RecipientType == RecipientType.User) + { + if (!await _usersRepository.ExistsByIdAsync(sendParams.RecipientId)) // Changed to ExistsByIdAsync + { + _logger.LogWarning("Attempt to send message to non-existent user {RecipientId}", sendParams.RecipientId); + return new SendMessageResult(false, new KeyNotFoundException($"Recipient user {sendParams.RecipientId} not found."), Guid.Empty); + } + // Verify friendship for private messages + await _verifyFriendship.VerifyAsync(sendParams.FromUserId, sendParams.RecipientId); + } + else if (sendParams.RecipientType == RecipientType.Group) + { + if (!_groupsRepository.Exists(sendParams.RecipientId)) + { + _logger.LogWarning("Attempt to send message to non-existent group {GroupId}", sendParams.RecipientId); + return new SendMessageResult(false, new KeyNotFoundException($"Recipient group {sendParams.RecipientId} not found."), Guid.Empty); + } + // TODO: Optionally, verify if sender is a member of the group + // bool isMember = await _groupsRepository.IsUserMemberOfGroupAsync(sendParams.FromUserId, sendParams.RecipientId); + // if (!isMember) + // { + // _logger.LogWarning("User {UserId} attempted to send message to group {GroupId} but is not a member", sendParams.FromUserId, sendParams.RecipientId); + // return new SendMessageResult(false, new UnauthorizedAccessException("Sender is not a member of the group."), Guid.Empty); + // } + } + else + { + _logger.LogError("Invalid recipient type specified: {RecipientType}", sendParams.RecipientType); + return new SendMessageResult(false, new ArgumentException("Invalid recipient type."), Guid.Empty); + } + + var messageId = Guid.NewGuid(); + var message = new Message + { + Id = messageId, + SenderId = sendParams.FromUserId, + RecipientId = sendParams.RecipientId, + RecipientType = sendParams.RecipientType, + EncryptedContent = sendParams.EncryptContent, + SentAt = sendParams.SendAt, + IsEdited = false, + ReplyToMessageId = sendParams.ReplyToMessageId, + MediaAttachments = sendParams.Media?.Select(m => new MediaAttachments + { + Id = m.MediaId, // Assuming SendMedia provides Id or it's generated here/by repo + MessageId = messageId, + Type = m.Type, + MimeType = m.MimeType, + EncryptedKey = m.EncryptedKey, + // FilePath will be set by storage service or handled by repository if media ID is pre-generated + }).ToList() ?? new List() + }; + + await _messagesRepository.AddAsync(message); + _logger.LogInformation("Message {MessageId} from {SenderId} to {RecipientId} ({RecipientType}) saved successfully.", messageId, sendParams.FromUserId, sendParams.RecipientId, sendParams.RecipientType); + return new SendMessageResult(true, null, messageId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId} ({RecipientType})", sendParams.FromUserId, sendParams.RecipientId, sendParams.RecipientType); + return new SendMessageResult(false, ex, Guid.Empty); + } + } + + public async Task EditMessageAsync(EditMessage editParams) + { + try + { + var message = await _messagesRepository.FindByIdAsync(editParams.MessageId); + + if (message.SenderId != editParams.EditorId) + { + _logger.LogWarning("User {EditorId} attempted to edit message {MessageId} not sent by them (sender was {SenderId})", editParams.EditorId, editParams.MessageId, message.SenderId); + return new EditMessageResult(false, new UnauthorizedAccessException("User is not authorized to edit this message."), null); + } + + // TODO: Add a time limit for editing messages? e.g., if (message.SentAt < DateTime.UtcNow.AddMinutes(-15)) throw new Exception("Edit time limit exceeded"); + + // Keep a copy of the original message state for the result, if needed by Hub for notifications + var originalMessageForNotification = new Message + { + Id = message.Id, + SenderId = message.SenderId, + RecipientId = message.RecipientId, + RecipientType = message.RecipientType, + // Populate other fields if necessary for the notification + }; + + message.EncryptedContent = editParams.NewContent; + message.IsEdited = true; + message.EditedAt = editParams.EditedAt; + + await _messagesRepository.UpdateAsync(message); + _logger.LogInformation("Message {MessageId} edited successfully by user {EditorId}", editParams.MessageId, editParams.EditorId); + return new EditMessageResult(true, null, originalMessageForNotification); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error editing message {MessageId} by user {EditorId}", editParams.MessageId, editParams.EditorId); + return new EditMessageResult(false, ex, null); + } + } + + public async Task DeleteMessageAsync(DeleteMessage deleteParams) + { + try + { + var message = await _messagesRepository.FindByIdAsync(deleteParams.MessageId); + + if (message.SenderId != deleteParams.DeleterId) + { + // TODO: Allow group admins to delete messages in their groups? + // if (message.RecipientType == RecipientType.Group) { + // bool isAdmin = await _groupsRepository.IsUserAdminOfGroupAsync(deleteParams.DeleterId, message.RecipientId); + // if (!isAdmin) { + // _logger.LogWarning("User {DeleterId} (not sender or admin) attempted to delete group message {MessageId}", deleteParams.DeleterId, deleteParams.MessageId); + // return new DeleteMessageResult(false, new UnauthorizedAccessException("User is not authorized to delete this message."), null); + // } + // } else { + _logger.LogWarning("User {DeleterId} attempted to delete message {MessageId} not sent by them (sender was {SenderId})", deleteParams.DeleterId, deleteParams.MessageId, message.SenderId); + return new DeleteMessageResult(false, new UnauthorizedAccessException("User is not authorized to delete this message."), null); + // } + } + + // Keep a copy of the original message state for the result, if needed by Hub for notifications + var originalMessageForNotification = new Message + { + Id = message.Id, + SenderId = message.SenderId, + RecipientId = message.RecipientId, + RecipientType = message.RecipientType, + // Populate other fields if necessary for the notification + }; + + await _messagesRepository.RemoveAsync(deleteParams.MessageId); + + // TODO: Delete associated media attachments from storage if they are no longer referenced. + + _logger.LogInformation("Message {MessageId} deleted successfully by user {DeleterId}", deleteParams.MessageId, deleteParams.DeleterId); + return new DeleteMessageResult(true, null, originalMessageForNotification); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting message {MessageId} by user {DeleterId}", deleteParams.MessageId, deleteParams.DeleterId); + return new DeleteMessageResult(false, ex, null); + } + } +} \ No newline at end of file diff --git a/Govor.Application/Services/PrivateChatService.cs b/Govor.Application/Services/PrivateChatService.cs deleted file mode 100644 index 786ad27..0000000 --- a/Govor.Application/Services/PrivateChatService.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Govor.Application.Interfaces; -using Govor.Application.Interfaces.Messages; -using Govor.Application.Interfaces.Messages.Parameters; -using Govor.Core.Models; -using Govor.Core.Repositories.Messages; -using Govor.Data; -using Microsoft.EntityFrameworkCore; - -namespace Govor.Application.Services; - -public class PrivateChatService : IChatService -{ - private readonly IVerifyFriendship _verifyFriendship; - private readonly IMessagesRepository _messages; - - public PrivateChatService(IMessagesRepository messages, IVerifyFriendship verifyFriendship) - { - _messages = messages; - _verifyFriendship = verifyFriendship; - } - - public async Task SendMessageAsync(SendMessage newMessage) - { - try - { - await _verifyFriendship.VerifyAsync(newMessage.FromUserId, newMessage.RecipientId); - - var messageId = Guid.NewGuid(); - - var message = new Message() - { - Id = messageId, - EncryptedContent = newMessage.EncryptContent, - IsEdited = false, - SenderId = newMessage.FromUserId, - RecipientId = newMessage.RecipientId, - RecipientType = RecipientType.User, - ReplyToMessageId = newMessage.ReplyToMessageId, - - MediaAttachments = newMessage.Media.Select(m => new MediaAttachments() - { - Id = m.Id, - MessageId = messageId, - Type = m.Type, - MimeType = m.MimeType, - EncryptedKey = m.EncryptedKey, - }).ToList() - }; - - await _messages.AddAsync(message); - - return new Result(true, null, messageId); - } - catch (Exception ex) - { - return new Result(false, ex, Guid.Empty); - } - } - - public Task EditMessageAsync(Guid editorId, Guid messageId, string newContent) - { - throw new NotImplementedException(); - } - - public Task DeleteMessageAsync(Guid editorId, Guid messageId) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Govor.Contracts/Requests/SignalR/MessageRequest.cs b/Govor.Contracts/Requests/SignalR/MessageRequest.cs index 09295d3..fdce57f 100644 --- a/Govor.Contracts/Requests/SignalR/MessageRequest.cs +++ b/Govor.Contracts/Requests/SignalR/MessageRequest.cs @@ -5,6 +5,7 @@ namespace Govor.Contracts.Requests.SignalR; public record MessageRequest { public Guid RecipientId { get; init; } + public RecipientType RecipientType { get; init; } public string EncryptedContent { get; init; } = string.Empty; public Guid? ReplyToMessageId { get; set; } public List MediaAttachments { get; set; } = new(); diff --git a/Govor.Contracts/Responses/SignalR/UserMessageResponse.cs b/Govor.Contracts/Responses/SignalR/UserMessageResponse.cs index fa98fc0..6380a05 100644 --- a/Govor.Contracts/Responses/SignalR/UserMessageResponse.cs +++ b/Govor.Contracts/Responses/SignalR/UserMessageResponse.cs @@ -1,12 +1,17 @@ using Govor.Contracts.Requests.SignalR; +using Govor.Core.Models; namespace Govor.Contracts.Responses.SignalR; public record UserMessageResponse { - public Guid Id { get; init; } + public Guid MessageId { get; init; } public Guid SenderId { get; init; } + public Guid RecipientId { get; init; } + public RecipientType RecipientType{get; init; } public string EncryptedContent { get; init; } = string.Empty; - public Guid? ReplyToMessageId { get; set; } - public List MediaReferences { get; init; } = new List(); + public Guid? ReplyToMessageId { get; init; } + public DateTime SentAt { get; init; } + public bool IsEdited { get; init; } = false; + public List MediaAttachments { get; init; } = new List(); } \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsExist.cs b/Govor.Core/Repositories/Groups/IGroupsExist.cs new file mode 100644 index 0000000..4e95afa --- /dev/null +++ b/Govor.Core/Repositories/Groups/IGroupsExist.cs @@ -0,0 +1,9 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Groups; + +public interface IGroupsExist +{ + public bool Exists(Guid groupId); + public bool Exists(ChatGroup chatGroup); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsReader.cs b/Govor.Core/Repositories/Groups/IGroupsReader.cs index 41f1924..df38bb8 100644 --- a/Govor.Core/Repositories/Groups/IGroupsReader.cs +++ b/Govor.Core/Repositories/Groups/IGroupsReader.cs @@ -1,6 +1,6 @@ namespace Govor.Core.Repositories.Groups; -public class IGroupsReader +public interface IGroupsReader { } \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsRepository.cs b/Govor.Core/Repositories/Groups/IGroupsRepository.cs new file mode 100644 index 0000000..cf9482f --- /dev/null +++ b/Govor.Core/Repositories/Groups/IGroupsRepository.cs @@ -0,0 +1,6 @@ +namespace Govor.Core.Repositories.Groups; + +public interface IGroupsRepository : IGroupsReader, IGroupsExist, IGroupsWriter +{ + +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsWriter.cs b/Govor.Core/Repositories/Groups/IGroupsWriter.cs new file mode 100644 index 0000000..2fc631c --- /dev/null +++ b/Govor.Core/Repositories/Groups/IGroupsWriter.cs @@ -0,0 +1,6 @@ +namespace Govor.Core.Repositories.Groups; + +public interface IGroupsWriter +{ + +} \ No newline at end of file