Refactor: Revert to hard deletes for messages. Remove IsDeleted flag and update repository and configurations accordingly.

This commit is contained in:
google-labs-jules[bot]
2025-07-04 06:03:14 +00:00
parent fab3d6b67b
commit 204e8dba9c
21 changed files with 1299 additions and 185 deletions
@@ -0,0 +1,97 @@
using Govor.Application.Interfaces;
using Govor.Core.Models;
using Govor.Core.Repositories.Groups; // Assuming IGroupsRepository will be used
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Govor.Application.Services;
public class GroupService : IGroupService
{
private readonly IGroupsRepository _groupsRepository;
private readonly ILogger<GroupService> _logger;
public GroupService(IGroupsRepository groupsRepository, ILogger<GroupService> logger)
{
_groupsRepository = groupsRepository;
_logger = logger;
}
public Task<ChatGroup?> GetGroupByIdAsync(Guid groupId)
{
_logger.LogInformation("GetGroupByIdAsync called for {GroupId}", groupId);
// Implementation Note: Use _groupsRepository.GetByIdAsync(groupId);
throw new NotImplementedException();
}
public Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds)
{
_logger.LogInformation("CreateGroupAsync called by {CreatorId} with name {Name}", creatorId, name);
// Implementation Note:
// 1. Create ChatGroup entity.
// 2. Create GroupMembership entities for creator and initial members.
// 3. Save to repository.
throw new NotImplementedException();
}
public Task<Result> AddUserToGroupAsync(Guid groupId, Guid userId, Guid addedByUserId)
{
_logger.LogInformation("AddUserToGroupAsync: User {UserId} to Group {GroupId} by {AddedByUserId}", userId, groupId, addedByUserId);
// Implementation Note:
// 1. Verify group exists.
// 2. Verify addedByUserId has permission (e.g., is admin or member, depending on rules).
// 3. Verify userId is not already a member.
// 4. Create GroupMembership entity.
// 5. Save to repository.
throw new NotImplementedException();
}
public Task<Result> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId)
{
_logger.LogInformation("RemoveUserFromGroupAsync: User {UserId} from Group {GroupId} by {RemovedByUserId}", userId, groupId, removedByUserId);
// Implementation Note:
// 1. Verify group exists.
// 2. Verify removedByUserId has permission.
// 3. Verify userId is a member.
// 4. Remove GroupMembership entity.
// 5. Save to repository.
// 6. Consider what happens if the last admin/member is removed.
throw new NotImplementedException();
}
public Task<Result> DeleteGroupAsync(Guid groupId, Guid userId)
{
_logger.LogInformation("DeleteGroupAsync: Group {GroupId} by User {UserId}", groupId, userId);
// Implementation Note:
// 1. Verify group exists.
// 2. Verify userId has permission (e.g., is owner or admin).
// 3. Delete group and its memberships.
// 4. Save to repository.
throw new NotImplementedException();
}
public Task<List<User>> GetGroupMembersAsync(Guid groupId)
{
_logger.LogInformation("GetGroupMembersAsync for Group {GroupId}", groupId);
// Implementation Note: Use _groupsRepository to fetch members.
throw new NotImplementedException();
}
public Task<List<ChatGroup>> GetUserGroupsAsync(Guid userId)
{
_logger.LogInformation("GetUserGroupsAsync for User {UserId}", userId);
// Implementation Note: Use _groupsRepository to fetch groups for the user.
// This is important for ChatsHub.OnConnectedAsync to subscribe user to their group channels.
throw new NotImplementedException();
}
public ChatGroup? GetGroupByInviteCode(string code)
{
_logger.LogInformation("GetGroupByInviteCode called with code {Code}", code);
// Implementation Note:
// 1. Find group by invite code (may require a specific table/field for invite codes).
throw new NotImplementedException();
}
}
@@ -0,0 +1,201 @@
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.Core.Repositories.Groups; // Assuming an IGroupsRepository exists for group validation
using Govor.Core.Repositories.Users; // Assuming an IUsersRepository exists for user validation
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<MessageService> _logger;
public MessageService(
IMessagesRepository messagesRepository,
IUsersRepository usersRepository,
IGroupsRepository groupsRepository,
IVerifyFriendship verifyFriendship,
ILogger<MessageService> logger)
{
_messagesRepository = messagesRepository;
_usersRepository = usersRepository;
_groupsRepository = groupsRepository;
_verifyFriendship = verifyFriendship;
_logger = logger;
}
public async Task<SendMessageResult> 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 (!await _groupsRepository.ExistsAsync(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.Id, // 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<MediaAttachments>()
};
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<EditMessageResult> EditMessageAsync(EditMessage editParams)
{
try
{
var message = await _messagesRepository.GetByIdAsync(editParams.MessageId);
if (message == null)
{
_logger.LogWarning("Attempt to edit non-existent message {MessageId} by user {EditorId}", editParams.MessageId, editParams.EditorId);
return new EditMessageResult(false, new KeyNotFoundException($"Message {editParams.MessageId} not found."), null);
}
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<DeleteMessageResult> DeleteMessageAsync(DeleteMessage deleteParams)
{
try
{
var message = await _messagesRepository.GetByIdAsync(deleteParams.MessageId);
if (message == null)
{
_logger.LogWarning("Attempt to delete non-existent message {MessageId} by user {DeleterId}", deleteParams.MessageId, deleteParams.DeleterId);
return new DeleteMessageResult(false, new KeyNotFoundException($"Message {deleteParams.MessageId} not found."), null);
}
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
};
// Instead of physical delete, consider a soft delete:
// message.IsDeleted = true;
// message.DeletedAt = DateTime.UtcNow;
// await _messagesRepository.UpdateAsync(message);
// For now, doing a hard delete as per initial understanding.
await _messagesRepository.DeleteAsync(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);
}
}
}
@@ -1,69 +1,48 @@
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;
// using Govor.Application.Interfaces.Messages; // No longer needed
// using Govor.Application.Interfaces.Messages.Parameters; // No longer needed
// using Govor.Core.Models; // No longer needed unless other methods use Core models
// using Govor.Core.Repositories.Messages; // No longer needed
using Microsoft.Extensions.Logging;
namespace Govor.Application.Services;
public class PrivateChatService : IChatService
// This service might handle creation/deletion of private chat sessions if that's
// more than just sending a message to a user (e.g., if private chats are explicit entities).
// For now, it primarily relies on IVerifyFriendship.
// If friendship verification is the only role, its methods could be absorbed elsewhere,
// or this service could be enhanced with other private chat management features.
public class PrivateChatService // No longer implements IChatService
{
private readonly IVerifyFriendship _verifyFriendship;
private readonly IMessagesRepository _messages;
public PrivateChatService(IMessagesRepository messages, IVerifyFriendship verifyFriendship)
private readonly ILogger<PrivateChatService> _logger;
public PrivateChatService(IVerifyFriendship verifyFriendship, ILogger<PrivateChatService> logger)
{
_messages = messages;
_verifyFriendship = verifyFriendship;
}
public async Task<Result> 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);
}
_logger = logger;
}
public Task<Result> EditMessageAsync(Guid editorId, Guid messageId, string newContent)
{
throw new NotImplementedException();
}
// Example of a method that might remain or be added:
// public async Task<bool> CanUserChatWithAsync(Guid userId1, Guid userId2)
// {
// try
// {
// await _verifyFriendship.VerifyAsync(userId1, userId2);
// return true;
// }
// catch (FriendshipException) // Or whatever exception VerifyAsync throws
// {
// return false;
// }
// }
public Task<Result> DeleteMessageAsync(Guid editorId, Guid messageId)
// All message sending, editing, deleting methods are removed as they are now in MessageService.
// If this service has no other responsibilities, it could be a candidate for removal,
// and IVerifyFriendship could be injected directly where needed (e.g. MessageService).
// However, keeping it allows for future expansion of private chat-specific logic.
public void PlaceholderMethod()
{
throw new NotImplementedException();
_logger.LogInformation("PrivateChatService is currently a placeholder after message management refactoring.");
}
}