mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 19:54:55 +00:00
Refactor: Revert to hard deletes for messages. Remove IsDeleted flag and update repository and configurations accordingly.
This commit is contained in:
@@ -0,0 +1,248 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
using Govor.API.Hubs;
|
||||||
|
using Govor.Application.Interfaces.Messages;
|
||||||
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Govor.API; // Assuming Startup or Program is here for WebApplicationFactory
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.Text;
|
||||||
|
using Govor.Contracts.Requests.SignalR; // For MessageRequest etc.
|
||||||
|
using Govor.Contracts.Responses.SignalR; // For UserMessageResponse etc.
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Govor.API.Tests.IntegrationTests.Hubs;
|
||||||
|
|
||||||
|
// Base class for Hub tests to handle TestServer and client creation
|
||||||
|
public abstract class HubTestBase : IAsyncLifetime
|
||||||
|
{
|
||||||
|
protected TestServer TestServer;
|
||||||
|
protected HubConnection ClientConnection;
|
||||||
|
protected Mock<IMessageService> MockMessageService;
|
||||||
|
protected Guid TestUserId = Guid.NewGuid();
|
||||||
|
protected string TestUserToken;
|
||||||
|
|
||||||
|
public virtual async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
MockMessageService = new Mock<IMessageService>();
|
||||||
|
TestUserToken = GenerateTestToken(TestUserId.ToString(), "testuser");
|
||||||
|
|
||||||
|
var webHostBuilder = new WebHostBuilder()
|
||||||
|
.UseEnvironment("Testing") // Use a specific environment for tests
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// Replace the real IMessageService with our mock for testing Hub logic
|
||||||
|
services.AddSingleton(MockMessageService.Object);
|
||||||
|
|
||||||
|
// Configure Authentication
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateLifetime = false,
|
||||||
|
ValidateIssuerSigningKey = false,
|
||||||
|
SignatureValidator = (token, parameters) => new JwtSecurityToken(token), // Bypass signature validation for test tokens
|
||||||
|
NameClaimType = ClaimTypes.NameIdentifier
|
||||||
|
};
|
||||||
|
// For SignalR, need to allow token in query string for WebSocket
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"];
|
||||||
|
if (!string.IsNullOrEmpty(accessToken) &&
|
||||||
|
(context.HttpContext.Request.Path.StartsWithSegments("/hubs/chats"))) // Adjust path if needed
|
||||||
|
{
|
||||||
|
context.Token = accessToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
services.AddAuthorization();
|
||||||
|
services.AddSignalR();
|
||||||
|
|
||||||
|
// Add other necessary services that ChatsHub might depend on, possibly mocked
|
||||||
|
// services.AddSingleton(Mock.Of<IUserService>()); // If ChatsHub uses IUserService for groups
|
||||||
|
})
|
||||||
|
.ConfigureLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.AddDebug(); // Or any other logger
|
||||||
|
})
|
||||||
|
.UseStartup<Startup>(); // Assuming you have a Startup.cs, or use Program.cs for .NET 6+
|
||||||
|
|
||||||
|
TestServer = new TestServer(webHostBuilder);
|
||||||
|
ClientConnection = new HubConnectionBuilder()
|
||||||
|
.WithUrl($"ws://localhost/hubs/chats", options => // Adjust URL as per your hub route
|
||||||
|
{
|
||||||
|
options.HttpMessageHandlerFactory = _ => TestServer.CreateHandler();
|
||||||
|
options.AccessTokenProvider = () => Task.FromResult<string?>(TestUserToken);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await ClientConnection.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
if (ClientConnection != null)
|
||||||
|
{
|
||||||
|
await ClientConnection.DisposeAsync();
|
||||||
|
}
|
||||||
|
TestServer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateTestToken(string userId, string userName)
|
||||||
|
{
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim("userID", userId), // Ensure this matches the claim name used in ChatsHub.GetUserId()
|
||||||
|
new Claim(ClaimTypes.Name, userName)
|
||||||
|
};
|
||||||
|
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-super-secret-key-that-is-long-enough")); // Use a key from config or a test key
|
||||||
|
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
var token = new JwtSecurityToken("TestIssuer", "TestAudience", claims, expires: DateTime.Now.AddHours(1), signingCredentials: credentials);
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ChatsHubTests : HubTestBase
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Send_ValidMessageToUser_CallsServiceAndNotifiesRecipientAndSender()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var recipientId = Guid.NewGuid();
|
||||||
|
var messageContent = "Hello from integration test";
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
|
||||||
|
MockMessageService.Setup(s => s.SendMessageAsync(It.Is<SendMessage>(p =>
|
||||||
|
p.FromUserId == TestUserId &&
|
||||||
|
p.RecipientId == recipientId &&
|
||||||
|
p.RecipientType == RecipientType.User)))
|
||||||
|
.ReturnsAsync(new SendMessageResult(true, null, messageId));
|
||||||
|
|
||||||
|
var messageReceivedTcs = new TaskCompletionSource<UserMessageResponse>();
|
||||||
|
var messageSentTcs = new TaskCompletionSource<UserMessageResponse>();
|
||||||
|
|
||||||
|
// Simulate recipient client being connected and in their own group
|
||||||
|
// This is tricky without a second client connection. For now, we assume SignalR handles group delivery.
|
||||||
|
// A more thorough test would involve a second client.
|
||||||
|
ClientConnection.On<UserMessageResponse>("ReceiveMessage", (response) =>
|
||||||
|
{
|
||||||
|
// This would ideally be for the recipient. For a single client test, this might be tricky.
|
||||||
|
// If sender also receives their own message via "ReceiveMessage" to their own user group:
|
||||||
|
if(response.SenderId == TestUserId && response.RecipientId == recipientId)
|
||||||
|
{
|
||||||
|
//This is if sender also gets ReceiveMessage, for simplicity in single client test
|
||||||
|
messageReceivedTcs.TrySetResult(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ClientConnection.On<UserMessageResponse>("MessageSent", (response) => // Confirmation to sender
|
||||||
|
{
|
||||||
|
if(response.MessageId == messageId) messageSentTcs.TrySetResult(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = new MessageRequest
|
||||||
|
{
|
||||||
|
RecipientId = recipientId,
|
||||||
|
RecipientType = RecipientType.User,
|
||||||
|
EncryptedContent = messageContent,
|
||||||
|
MediaAttachments = new List<MediaReference>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await ClientConnection.InvokeAsync("Send", request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var sentConfirmation = await Task.WhenAny(messageSentTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageSentTcs.Task
|
||||||
|
? messageSentTcs.Task.Result : null;
|
||||||
|
|
||||||
|
// var receivedMessage = await Task.WhenAny(messageReceivedTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageReceivedTcs.Task
|
||||||
|
// ? messageReceivedTcs.Task.Result : null;
|
||||||
|
|
||||||
|
|
||||||
|
Assert.NotNull(sentConfirmation);
|
||||||
|
Assert.Equal(messageId, sentConfirmation.MessageId);
|
||||||
|
Assert.Equal(messageContent, sentConfirmation.EncryptedContent);
|
||||||
|
|
||||||
|
// Assert.NotNull(receivedMessage); // This part is harder to assert reliably with one client for recipient
|
||||||
|
// Assert.Equal(messageId, receivedMessage.MessageId);
|
||||||
|
|
||||||
|
|
||||||
|
MockMessageService.Verify(s => s.SendMessageAsync(It.Is<SendMessage>(p =>
|
||||||
|
p.FromUserId == TestUserId &&
|
||||||
|
p.RecipientId == recipientId &&
|
||||||
|
p.EncryptContent == messageContent)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Edit_ValidEditRequest_CallsServiceAndNotifies()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalSenderId = TestUserId; // Editor must be the sender
|
||||||
|
var messageToEditId = Guid.NewGuid();
|
||||||
|
var newContent = "Updated content";
|
||||||
|
var recipientId = Guid.NewGuid(); // Could be user or group
|
||||||
|
|
||||||
|
var originalMessage = new Message {
|
||||||
|
Id = messageToEditId,
|
||||||
|
SenderId = originalSenderId,
|
||||||
|
RecipientId = recipientId,
|
||||||
|
RecipientType = RecipientType.User
|
||||||
|
};
|
||||||
|
|
||||||
|
MockMessageService.Setup(s => s.EditMessageAsync(It.Is<EditMessage>(p =>
|
||||||
|
p.EditorId == TestUserId &&
|
||||||
|
p.MessageId == messageToEditId &&
|
||||||
|
p.NewContent == newContent)))
|
||||||
|
.ReturnsAsync(new EditMessageResult(true, null, originalMessage));
|
||||||
|
|
||||||
|
var messageEditedTcs = new TaskCompletionSource<MessageEditedResponse>();
|
||||||
|
ClientConnection.On<MessageEditedResponse>("MessageEdited", (response) =>
|
||||||
|
{
|
||||||
|
if(response.MessageId == messageToEditId) messageEditedTcs.TrySetResult(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = new EditMessageRequest
|
||||||
|
{
|
||||||
|
MessageId = messageToEditId,
|
||||||
|
NewEncryptedContent = newContent
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await ClientConnection.InvokeAsync("Edit", request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var editedNotification = await Task.WhenAny(messageEditedTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageEditedTcs.Task
|
||||||
|
? messageEditedTcs.Task.Result : null;
|
||||||
|
|
||||||
|
Assert.NotNull(editedNotification);
|
||||||
|
Assert.Equal(messageToEditId, editedNotification.MessageId);
|
||||||
|
Assert.Equal(newContent, editedNotification.NewEncryptedContent);
|
||||||
|
Assert.Equal(originalSenderId, editedNotification.SenderId); // Verify original sender is preserved
|
||||||
|
|
||||||
|
MockMessageService.Verify(s => s.EditMessageAsync(It.Is<EditMessage>(p =>
|
||||||
|
p.EditorId == TestUserId &&
|
||||||
|
p.MessageId == messageToEditId &&
|
||||||
|
p.NewContent == newContent)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests for Remove method
|
||||||
|
// TODO: Add tests for OnConnectedAsync (e.g., joining groups - might require IUserService mock)
|
||||||
|
// TODO: Add tests for unauthorized scenarios (e.g., editing/deleting someone else's message)
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
using Moq;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Govor.Application.Services;
|
||||||
|
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.Users;
|
||||||
|
using Govor.Core.Repositories.Groups;
|
||||||
|
using Xunit;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Govor.Application.Exceptions.VerifyFriendship; // Assuming this exception type
|
||||||
|
|
||||||
|
namespace Govor.API.Tests.UnitTests.Services;
|
||||||
|
|
||||||
|
public class MessageServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IMessagesRepository> _mockMessagesRepo;
|
||||||
|
private readonly Mock<IUsersRepository> _mockUsersRepo;
|
||||||
|
private readonly Mock<IGroupsRepository> _mockGroupsRepo;
|
||||||
|
private readonly Mock<IVerifyFriendship> _mockVerifyFriendship;
|
||||||
|
private readonly Mock<ILogger<MessageService>> _mockLogger;
|
||||||
|
private readonly MessageService _messageService;
|
||||||
|
|
||||||
|
public MessageServiceTests()
|
||||||
|
{
|
||||||
|
_mockMessagesRepo = new Mock<IMessagesRepository>();
|
||||||
|
_mockUsersRepo = new Mock<IUsersRepository>();
|
||||||
|
_mockGroupsRepo = new Mock<IGroupsRepository>();
|
||||||
|
_mockVerifyFriendship = new Mock<IVerifyFriendship>();
|
||||||
|
_mockLogger = new Mock<ILogger<MessageService>>();
|
||||||
|
|
||||||
|
_messageService = new MessageService(
|
||||||
|
_mockMessagesRepo.Object,
|
||||||
|
_mockUsersRepo.Object,
|
||||||
|
_mockGroupsRepo.Object,
|
||||||
|
_mockVerifyFriendship.Object,
|
||||||
|
_mockLogger.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SendMessageAsync Tests ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessageAsync_ToUser_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var senderId = Guid.NewGuid();
|
||||||
|
var recipientId = Guid.NewGuid();
|
||||||
|
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
|
||||||
|
|
||||||
|
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
|
||||||
|
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).Returns(Task.CompletedTask);
|
||||||
|
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Null(result.Exception);
|
||||||
|
Assert.NotEqual(Guid.Empty, result.MessageId);
|
||||||
|
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m =>
|
||||||
|
m.SenderId == senderId &&
|
||||||
|
m.RecipientId == recipientId &&
|
||||||
|
m.RecipientType == RecipientType.User &&
|
||||||
|
m.EncryptedContent == "Hello")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessageAsync_ToGroup_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var senderId = Guid.NewGuid();
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var sendMessageParams = new SendMessage("Hello Group", null, groupId, RecipientType.Group, senderId, DateTime.UtcNow, new List<SendMedia>());
|
||||||
|
|
||||||
|
_mockGroupsRepo.Setup(r => r.ExistsAsync(groupId)).ReturnsAsync(true);
|
||||||
|
// _mockGroupsRepo.Setup(r => r.IsUserMemberOfGroupAsync(senderId, groupId)).ReturnsAsync(true); // Assuming membership check
|
||||||
|
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotEqual(Guid.Empty, result.MessageId);
|
||||||
|
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m => m.RecipientId == groupId && m.RecipientType == RecipientType.Group)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessageAsync_ToUser_RecipientNotFound_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var senderId = Guid.NewGuid();
|
||||||
|
var recipientId = Guid.NewGuid();
|
||||||
|
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
|
||||||
|
|
||||||
|
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Exception);
|
||||||
|
Assert.IsType<KeyNotFoundException>(result.Exception);
|
||||||
|
Assert.Equal(Guid.Empty, result.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessageAsync_ToUser_FriendshipVerificationFails_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var senderId = Guid.NewGuid();
|
||||||
|
var recipientId = Guid.NewGuid();
|
||||||
|
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
|
||||||
|
|
||||||
|
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
|
||||||
|
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).ThrowsAsync(new FriendshipException("Not friends"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Exception);
|
||||||
|
Assert.IsType<FriendshipException>(result.Exception);
|
||||||
|
Assert.Equal(Guid.Empty, result.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- EditMessageAsync Tests ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditMessageAsync_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var editorId = Guid.NewGuid();
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var originalMessage = new Message { Id = messageId, SenderId = editorId, EncryptedContent = "Old", RecipientId = Guid.NewGuid(), RecipientType = RecipientType.User };
|
||||||
|
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
|
||||||
|
_mockMessagesRepo.Setup(r => r.UpdateAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.EditMessageAsync(editParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.OriginalMessage);
|
||||||
|
Assert.Equal(messageId, result.OriginalMessage!.Id);
|
||||||
|
_mockMessagesRepo.Verify(r => r.UpdateAsync(It.Is<Message>(m =>
|
||||||
|
m.Id == messageId &&
|
||||||
|
m.EncryptedContent == "New Content" &&
|
||||||
|
m.IsEdited == true &&
|
||||||
|
m.EditedAt == editParams.EditedAt)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditMessageAsync_MessageNotFound_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var editorId = Guid.NewGuid();
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync((Message?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.EditMessageAsync(editParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.IsType<KeyNotFoundException>(result.Exception);
|
||||||
|
Assert.Null(result.OriginalMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EditMessageAsync_NotSender_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var editorId = Guid.NewGuid();
|
||||||
|
var senderId = Guid.NewGuid(); // Different from editorId
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var originalMessage = new Message { Id = messageId, SenderId = senderId, EncryptedContent = "Old" };
|
||||||
|
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.EditMessageAsync(editParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.IsType<UnauthorizedAccessException>(result.Exception);
|
||||||
|
Assert.Null(result.OriginalMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DeleteMessageAsync Tests ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteMessageAsync_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var deleterId = Guid.NewGuid();
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var originalMessage = new Message { Id = messageId, SenderId = deleterId, RecipientId = Guid.NewGuid(), RecipientType = RecipientType.User };
|
||||||
|
var deleteParams = new DeleteMessage(deleterId, messageId);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
|
||||||
|
// For soft delete, the repo's DeleteAsync would internally mark IsDeleted=true and save.
|
||||||
|
// If it were hard delete, it would be _mockMessagesRepo.Setup(r => r.DeleteAsync(messageId)).Returns(Task.CompletedTask);
|
||||||
|
// Assuming DeleteAsync in repo handles the soft delete logic (sets IsDeleted, DeletedAt, then calls UpdateAsync or similar)
|
||||||
|
// For this test, we verify that the service calls the repo's DeleteAsync method.
|
||||||
|
// The actual soft delete implementation is tested at the repository level.
|
||||||
|
// However, MessageService is responsible for *initiating* the delete.
|
||||||
|
// If MessageService directly manipulated IsDeleted, we'd mock repo.UpdateAsync.
|
||||||
|
// Since it calls repo.DeleteAsync, we mock that.
|
||||||
|
_mockMessagesRepo.Setup(r => r.DeleteAsync(messageId)).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.DeleteMessageAsync(deleteParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.OriginalMessage);
|
||||||
|
Assert.Equal(messageId, result.OriginalMessage!.Id);
|
||||||
|
_mockMessagesRepo.Verify(r => r.DeleteAsync(messageId), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteMessageAsync_MessageNotFound_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var deleterId = Guid.NewGuid();
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var deleteParams = new DeleteMessage(deleterId, messageId);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync((Message?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.DeleteMessageAsync(deleteParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.IsType<KeyNotFoundException>(result.Exception);
|
||||||
|
Assert.Null(result.OriginalMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteMessageAsync_NotSender_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var deleterId = Guid.NewGuid();
|
||||||
|
var senderId = Guid.NewGuid(); // Different
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var originalMessage = new Message { Id = messageId, SenderId = senderId };
|
||||||
|
var deleteParams = new DeleteMessage(deleterId, messageId);
|
||||||
|
|
||||||
|
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _messageService.DeleteMessageAsync(deleteParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.IsType<UnauthorizedAccessException>(result.Exception);
|
||||||
|
Assert.Null(result.OriginalMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
-72
@@ -1,8 +1,6 @@
|
|||||||
using Govor.API.Services;
|
|
||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Application.Interfaces.Messages;
|
||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
using Govor.Contracts.Requests.SignalR;
|
using Govor.Contracts.Requests.SignalR;
|
||||||
using Govor.Contracts.Responses.SignalR;
|
|
||||||
using Govor.Core.Models;
|
using Govor.Core.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
@@ -13,125 +11,297 @@ namespace Govor.API.Hubs;
|
|||||||
public class ChatsHub : Hub
|
public class ChatsHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ILogger<ChatsHub> _logger;
|
private readonly ILogger<ChatsHub> _logger;
|
||||||
private readonly IChatService _chatService;
|
private readonly IMessageService _messageService; // Consolidated service
|
||||||
private readonly IGroupService _groupService;
|
|
||||||
|
|
||||||
public ChatsHub(ILogger<ChatsHub> logger,
|
public ChatsHub(ILogger<ChatsHub> logger, IMessageService messageService)
|
||||||
IChatService chatService
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_chatService = chatService;
|
_messageService = messageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = GetUserId();
|
var userId = GetUserId();
|
||||||
|
if (userId == Guid.Empty)
|
||||||
if (userId != Guid.Empty)
|
|
||||||
{
|
{
|
||||||
// Binding ConnectionId to UserId
|
_logger.LogWarning("User connected with invalid UserID claim.");
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
|
Context.Abort(); // Abort connection if userID is invalid
|
||||||
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId);
|
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();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
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)
|
if (userId != Guid.Empty)
|
||||||
{
|
{
|
||||||
|
// Remove user from their own group
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
|
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);
|
await base.OnDisconnectedAsync(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Remove(Guid recipientId, Guid messageId)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Edit(string newMessage, Guid messageId)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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<SendMedia>());
|
|
||||||
|
|
||||||
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)
|
public async Task Send(MessageRequest request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent))
|
if (string.IsNullOrWhiteSpace(request.EncryptedContent) && (request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Empty message received from user {UserId}", GetUserId());
|
_logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}", GetUserId());
|
||||||
throw new ArgumentException("Message cannot be empty", nameof(request.EncryptedContent));
|
// Consider whether to throw an exception or just ignore
|
||||||
|
// For now, let's throw, as it's likely an issue with the client
|
||||||
|
throw new ArgumentException("Message cannot be empty (must have content or attachments).");
|
||||||
}
|
}
|
||||||
|
|
||||||
var senderId = GetUserId();
|
var senderId = GetUserId();
|
||||||
|
_logger.LogInformation("Message send initiated by {SenderId} to {RecipientId} of type {RecipientType}", senderId, request.RecipientId, request.RecipientType);
|
||||||
|
|
||||||
try
|
var sendMessageParams = new SendMessage(
|
||||||
{
|
|
||||||
_logger.LogInformation("Message sent from {SenderId} to {RecipientId} at {UtcNow}", senderId, request.RecipientId, DateTime.UtcNow);
|
|
||||||
|
|
||||||
var message = new SendMessage(
|
|
||||||
EncryptContent: request.EncryptedContent,
|
EncryptContent: request.EncryptedContent,
|
||||||
ReplyToMessageId: request.ReplyToMessageId,
|
ReplyToMessageId: request.ReplyToMessageId,
|
||||||
FromUserId: senderId,
|
FromUserId: senderId,
|
||||||
RecipientId: request.RecipientId,
|
RecipientId: request.RecipientId,
|
||||||
|
RecipientType: request.RecipientType, // Added RecipientType
|
||||||
SendAt: DateTime.UtcNow,
|
SendAt: DateTime.UtcNow,
|
||||||
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
||||||
f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty<SendMedia>());
|
f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty<SendMedia>()
|
||||||
|
);
|
||||||
|
|
||||||
Result result = await _chatService.SendMessageAsync(message);
|
try
|
||||||
if(result.IsSuccess == false)
|
{
|
||||||
throw result.Exception;
|
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");
|
||||||
|
// It might be better to send an error message back to the caller rather than throwing an exception that disconnects them.
|
||||||
|
// For now, rethrow to maintain previous behavior if an exception was present.
|
||||||
|
if (result.Exception != null) throw result.Exception;
|
||||||
|
throw new HubException("Failed to send message due to an internal error.");
|
||||||
|
}
|
||||||
|
|
||||||
// Sending a message to the sender and recipient
|
var messageResponse = new UserMessageResponse // Assuming a response DTO
|
||||||
await Clients.Group(message.RecipientId.ToString()).SendAsync("Receive", message);
|
{
|
||||||
await Clients.Group(message.FromUserId.ToString()).SendAsync("Receive", message);
|
MessageId = result.MessageId,
|
||||||
// TODO: Send to Group
|
SenderId = senderId,
|
||||||
|
RecipientId = request.RecipientId,
|
||||||
|
RecipientType = request.RecipientType,
|
||||||
|
EncryptedContent = request.EncryptedContent,
|
||||||
|
SentAt = sendMessageParams.SendAt, // Use the time from params
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId);
|
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId);
|
||||||
throw;
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid GetUserId()
|
public async Task Edit(EditMessageRequest request)
|
||||||
|
{
|
||||||
|
var editorId = GetUserId();
|
||||||
|
_logger.LogInformation("Message edit initiated by {EditorId} for message {MessageId}", editorId, request.MessageId);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.NewEncryptedContent))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Edit request for message {MessageId} by user {EditorId} has empty new content.", request.MessageId, editorId);
|
||||||
|
throw new ArgumentException("New message content cannot be empty.", nameof(request.NewEncryptedContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
var editParams = new EditMessage(
|
||||||
|
EditorId: editorId,
|
||||||
|
MessageId: request.MessageId,
|
||||||
|
NewContent: request.NewEncryptedContent,
|
||||||
|
EditedAt: DateTime.UtcNow
|
||||||
|
);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _messageService.EditMessageAsync(editParams);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.LogError(result.Exception, "Failed to edit message {MessageId} by {EditorId}. Error: {ErrorMessage}", request.MessageId, editorId, result.Exception?.Message ?? "Unknown error");
|
||||||
|
if (result.Exception != null) throw result.Exception; // Or specific HubException
|
||||||
|
throw new HubException("Failed to edit message.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalMessage = result.OriginalMessage; // Assuming service returns this
|
||||||
|
if (originalMessage == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("EditMessageAsync succeeded but did not return the original message details for message {MessageId}", request.MessageId);
|
||||||
|
throw new HubException("Failed to process message edit due to missing message details.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var editNotification = new MessageEditedResponse
|
||||||
|
{
|
||||||
|
MessageId = request.MessageId,
|
||||||
|
NewEncryptedContent = request.NewEncryptedContent,
|
||||||
|
EditedAt = editParams.EditedAt,
|
||||||
|
SenderId = originalMessage.SenderId, // Keep original sender
|
||||||
|
RecipientId = originalMessage.RecipientId,
|
||||||
|
RecipientType = originalMessage.RecipientType
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify relevant clients
|
||||||
|
if (originalMessage.RecipientType == RecipientType.User)
|
||||||
|
{
|
||||||
|
// Notify sender and recipient
|
||||||
|
await Clients.Group(originalMessage.SenderId.ToString()).SendAsync("MessageEdited", editNotification);
|
||||||
|
if (originalMessage.SenderId != originalMessage.RecipientId) // Avoid double sending if sender is recipient
|
||||||
|
{
|
||||||
|
await Clients.Group(originalMessage.RecipientId.ToString()).SendAsync("MessageEdited", editNotification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (originalMessage.RecipientType == RecipientType.Group)
|
||||||
|
{
|
||||||
|
await Clients.Group($"group_{originalMessage.RecipientId}").SendAsync("MessageEdited", editNotification);
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Message {MessageId} edited successfully by {EditorId}", request.MessageId, editorId);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unauthorized attempt to edit message {MessageId} by user {EditorId}", request.MessageId, editorId);
|
||||||
|
throw new HubException("You are not authorized to edit this message.", ex);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex) // Or a custom NotFoundException
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Attempt to edit non-existent message {MessageId} by user {EditorId}", request.MessageId, editorId);
|
||||||
|
throw new HubException("Message not found.", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error editing message {MessageId} by {EditorId}", request.MessageId, editorId);
|
||||||
|
throw new HubException("An error occurred while editing the message.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Remove(RemoveMessageRequest request)
|
||||||
|
{
|
||||||
|
var removerId = GetUserId();
|
||||||
|
_logger.LogInformation("Message removal initiated by {RemoverId} for message {MessageId}", removerId, request.MessageId);
|
||||||
|
|
||||||
|
var removeParams = new DeleteMessage(
|
||||||
|
DeleterId: removerId,
|
||||||
|
MessageId: request.MessageId
|
||||||
|
);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _messageService.DeleteMessageAsync(removeParams);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.LogError(result.Exception, "Failed to remove message {MessageId} by {RemoverId}. Error: {ErrorMessage}", request.MessageId, removerId, result.Exception?.Message ?? "Unknown error");
|
||||||
|
if (result.Exception != null) throw result.Exception;
|
||||||
|
throw new HubException("Failed to remove message.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalMessage = result.OriginalMessage; // Assuming service returns this
|
||||||
|
if (originalMessage == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("DeleteMessageAsync succeeded but did not return the original message details for message {MessageId}", request.MessageId);
|
||||||
|
throw new HubException("Failed to process message deletion due to missing message details.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var removalNotification = new MessageRemovedResponse
|
||||||
|
{
|
||||||
|
MessageId = request.MessageId,
|
||||||
|
SenderId = originalMessage.SenderId,
|
||||||
|
RecipientId = originalMessage.RecipientId,
|
||||||
|
RecipientType = originalMessage.RecipientType
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify relevant clients
|
||||||
|
if (originalMessage.RecipientType == RecipientType.User)
|
||||||
|
{
|
||||||
|
await Clients.Group(originalMessage.SenderId.ToString()).SendAsync("MessageRemoved", removalNotification);
|
||||||
|
if (originalMessage.SenderId != originalMessage.RecipientId)
|
||||||
|
{
|
||||||
|
await Clients.Group(originalMessage.RecipientId.ToString()).SendAsync("MessageRemoved", removalNotification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (originalMessage.RecipientType == RecipientType.Group)
|
||||||
|
{
|
||||||
|
await Clients.Group($"group_{originalMessage.RecipientId}").SendAsync("MessageRemoved", removalNotification);
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Message {MessageId} removed successfully by {RemoverId}", request.MessageId, removerId);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unauthorized attempt to remove message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
||||||
|
throw new HubException("You are not authorized to remove this message.", ex);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException ex) // Or a custom NotFoundException
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Attempt to remove non-existent message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
||||||
|
throw new HubException("Message not found.", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error removing message {MessageId} by {RemoverId}", request.MessageId, removerId);
|
||||||
|
throw new HubException("An error occurred while removing the message.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid GetUserId(bool suppressException = false)
|
||||||
{
|
{
|
||||||
var userIdClaim = Context.User?.FindFirst("userID")?.Value;
|
var userIdClaim = Context.User?.FindFirst("userID")?.Value;
|
||||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||||
{
|
{
|
||||||
_logger.LogError("Could not retrieve sender userId");
|
if (!suppressException)
|
||||||
throw new UnauthorizedAccessException("userID claim is missing or invalid");
|
{
|
||||||
|
_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;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Core.Models; // For ChatGroup and User
|
||||||
using Govor.Core.Models;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Govor.API.Services;
|
namespace Govor.Application.Interfaces; // Corrected namespace
|
||||||
|
|
||||||
public interface IGroupService : IMessageSendingService, IMessageManagementService
|
public interface IGroupService
|
||||||
{
|
{
|
||||||
ChatGroup GetGroupByInvite(string code);
|
Task<ChatGroup?> GetGroupByIdAsync(Guid groupId); // Added
|
||||||
|
Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds); // Added
|
||||||
|
Task<Result> AddUserToGroupAsync(Guid groupId, Guid userId, Guid addedByUserId); // Added
|
||||||
|
Task<Result> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId); // Added
|
||||||
|
Task<Result> DeleteGroupAsync(Guid groupId, Guid userId); // Added
|
||||||
|
Task<List<User>> GetGroupMembersAsync(Guid groupId); // Added
|
||||||
|
Task<List<ChatGroup>>GetUserGroupsAsync(Guid userId); // Added to support ChatsHub group joining
|
||||||
|
|
||||||
|
// Existing method, assuming it's still relevant for group operations (e.g., joining via invite link)
|
||||||
|
// Consider if this should return Task<ChatGroup?> or throw if not found.
|
||||||
|
ChatGroup? GetGroupByInviteCode(string code); // Renamed parameter for clarity, made nullable
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Govor.Application.Interfaces.Messages;
|
|
||||||
|
|
||||||
public interface IChatService : IMessageSendingService, IMessageManagementService
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
using Govor.Core.Models; // For Result
|
||||||
|
|
||||||
|
namespace Govor.Application.Interfaces.Messages;
|
||||||
|
|
||||||
|
// Combining IChatService and IGroupService functionalities relevant to messages
|
||||||
|
public interface IMessageService
|
||||||
|
{
|
||||||
|
Task<SendMessageResult> SendMessageAsync(SendMessage messageParameters);
|
||||||
|
Task<EditMessageResult> EditMessageAsync(EditMessage messageParameters);
|
||||||
|
Task<DeleteMessageResult> DeleteMessageAsync(DeleteMessage messageParameters);
|
||||||
|
|
||||||
|
// Potentially other message-related methods like:
|
||||||
|
// Task<GetMessagesResult> GetMessagesAsync(Guid userId, Guid chatId, RecipientType chatType, int pageNumber, int pageSize);
|
||||||
|
// Task<Result> 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);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
|
||||||
|
public record DeleteMessage(
|
||||||
|
Guid DeleterId,
|
||||||
|
Guid MessageId);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
|
||||||
|
public record EditMessage(
|
||||||
|
Guid EditorId,
|
||||||
|
Guid MessageId,
|
||||||
|
string NewContent,
|
||||||
|
DateTime EditedAt);
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
using Govor.Core.Models; // Added for RecipientType
|
||||||
|
|
||||||
namespace Govor.Application.Interfaces.Messages.Parameters;
|
namespace Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
|
||||||
public record SendMessage(
|
public record SendMessage(
|
||||||
string EncryptContent,
|
string EncryptContent,
|
||||||
Guid? ReplyToMessageId,
|
Guid? ReplyToMessageId,
|
||||||
Guid RecipientId,
|
Guid RecipientId,
|
||||||
|
RecipientType RecipientType, // Added this field
|
||||||
Guid FromUserId,
|
Guid FromUserId,
|
||||||
DateTime SendAt,
|
DateTime SendAt,
|
||||||
IEnumerable<SendMedia> Media);
|
IEnumerable<SendMedia> Media);
|
||||||
@@ -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;
|
||||||
using Govor.Application.Interfaces.Messages;
|
// using Govor.Application.Interfaces.Messages; // No longer needed
|
||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
// using Govor.Application.Interfaces.Messages.Parameters; // No longer needed
|
||||||
using Govor.Core.Models;
|
// using Govor.Core.Models; // No longer needed unless other methods use Core models
|
||||||
using Govor.Core.Repositories.Messages;
|
// using Govor.Core.Repositories.Messages; // No longer needed
|
||||||
using Govor.Data;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Govor.Application.Services;
|
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 IVerifyFriendship _verifyFriendship;
|
||||||
private readonly IMessagesRepository _messages;
|
private readonly ILogger<PrivateChatService> _logger;
|
||||||
|
|
||||||
public PrivateChatService(IMessagesRepository messages, IVerifyFriendship verifyFriendship)
|
public PrivateChatService(IVerifyFriendship verifyFriendship, ILogger<PrivateChatService> logger)
|
||||||
{
|
{
|
||||||
_messages = messages;
|
|
||||||
_verifyFriendship = verifyFriendship;
|
_verifyFriendship = verifyFriendship;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> SendMessageAsync(SendMessage newMessage)
|
// Example of a method that might remain or be added:
|
||||||
{
|
// public async Task<bool> CanUserChatWithAsync(Guid userId1, Guid userId2)
|
||||||
try
|
// {
|
||||||
{
|
// try
|
||||||
await _verifyFriendship.VerifyAsync(newMessage.FromUserId, newMessage.RecipientId);
|
// {
|
||||||
|
// await _verifyFriendship.VerifyAsync(userId1, userId2);
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// catch (FriendshipException) // Or whatever exception VerifyAsync throws
|
||||||
|
// {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
var messageId = Guid.NewGuid();
|
// 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,
|
||||||
var message = new Message()
|
// 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()
|
||||||
{
|
{
|
||||||
Id = messageId,
|
_logger.LogInformation("PrivateChatService is currently a placeholder after message management refactoring.");
|
||||||
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<Result> EditMessageAsync(Guid editorId, Guid messageId, string newContent)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Result> DeleteMessageAsync(Guid editorId, Guid messageId)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.Contracts.Requests.SignalR;
|
||||||
|
|
||||||
|
public class EditMessageRequest
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
public string NewEncryptedContent { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Govor.Contracts.Requests.SignalR;
|
|||||||
public record MessageRequest
|
public record MessageRequest
|
||||||
{
|
{
|
||||||
public Guid RecipientId { get; init; }
|
public Guid RecipientId { get; init; }
|
||||||
|
public RecipientType RecipientType { get; init; } // Added this field
|
||||||
public string EncryptedContent { get; init; } = string.Empty;
|
public string EncryptedContent { get; init; } = string.Empty;
|
||||||
public Guid? ReplyToMessageId { get; set; }
|
public Guid? ReplyToMessageId { get; set; }
|
||||||
public List<MediaReference> MediaAttachments { get; set; } = new();
|
public List<MediaReference> MediaAttachments { get; set; } = new();
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Govor.Contracts.Requests.SignalR;
|
||||||
|
|
||||||
|
public class RemoveMessageRequest
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Govor.Core.Models; // For RecipientType
|
||||||
|
|
||||||
|
namespace Govor.Contracts.Responses.SignalR;
|
||||||
|
|
||||||
|
public class MessageEditedResponse
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
public string NewEncryptedContent { get; set; } = string.Empty;
|
||||||
|
public DateTime EditedAt { get; set; }
|
||||||
|
public Guid SenderId { get; set; } // Original Sender
|
||||||
|
public Guid RecipientId { get; set; }
|
||||||
|
public RecipientType RecipientType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Govor.Core.Models; // For RecipientType
|
||||||
|
|
||||||
|
namespace Govor.Contracts.Responses.SignalR;
|
||||||
|
|
||||||
|
public class MessageRemovedResponse
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
public Guid SenderId { get; set; } // Original Sender
|
||||||
|
public Guid RecipientId { get; set; }
|
||||||
|
public RecipientType RecipientType { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
namespace Govor.Core.Repositories.Groups;
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
public class IGroupsReader
|
public interface IGroupsReader // Changed to interface
|
||||||
{
|
{
|
||||||
|
Task<bool> ExistsAsync(Guid groupId);
|
||||||
|
Task<bool> IsUserMemberOfGroupAsync(Guid userId, Guid groupId);
|
||||||
|
// Potentially other read methods like:
|
||||||
|
// Task<ChatGroup?> GetByIdAsync(Guid groupId);
|
||||||
|
// Task<List<User>> GetGroupMembersAsync(Guid groupId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
|
public interface IGroupsRepository : IGroupsReader
|
||||||
|
{
|
||||||
|
// This interface will combine IGroupsReader and IGroupsWriter (once created)
|
||||||
|
// For now, it only includes IGroupsReader.
|
||||||
|
}
|
||||||
@@ -1,6 +1,56 @@
|
|||||||
|
using Govor.Core.Models;
|
||||||
|
using Govor.Core.Repositories.Groups;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System;
|
||||||
|
using System.Linq; // Required for AnyAsync
|
||||||
|
|
||||||
namespace Govor.Data.Repositories;
|
namespace Govor.Data.Repositories;
|
||||||
|
|
||||||
public class GroupRepository
|
public class GroupRepository : IGroupsRepository
|
||||||
{
|
{
|
||||||
|
private readonly GovorDbContext _context;
|
||||||
|
|
||||||
|
public GroupRepository(GovorDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsAsync(Guid groupId)
|
||||||
|
{
|
||||||
|
return await _context.ChatGroups.AnyAsync(g => g.Id == groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsUserMemberOfGroupAsync(Guid userId, Guid groupId)
|
||||||
|
{
|
||||||
|
// Assuming ChatGroup has a navigation property 'Members' or a 'GroupMemberships' table
|
||||||
|
// Adjust based on your actual data model for group membership
|
||||||
|
var group = await _context.ChatGroups
|
||||||
|
.Include(g => g.Memberships) // Assuming GroupMembership links User and Group
|
||||||
|
.FirstOrDefaultAsync(g => g.Id == groupId);
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
{
|
||||||
|
return false; // Group doesn't exist
|
||||||
|
}
|
||||||
|
// Check if any membership for this group includes the user
|
||||||
|
return group.Memberships.Any(m => m.UserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement other IGroupsReader methods if any were defined beyond ExistsAsync and IsUserMemberOfGroupAsync
|
||||||
|
// For example:
|
||||||
|
// public async Task<ChatGroup?> GetByIdAsync(Guid groupId)
|
||||||
|
// {
|
||||||
|
// return await _context.ChatGroups.FindAsync(groupId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public async Task<List<User>> GetGroupMembersAsync(Guid groupId)
|
||||||
|
// {
|
||||||
|
// var group = await _context.ChatGroups
|
||||||
|
// .Include(g => g.Memberships)
|
||||||
|
// .ThenInclude(gm => gm.User)
|
||||||
|
// .FirstOrDefaultAsync(g => g.Id == groupId);
|
||||||
|
// if (group == null) return new List<User>();
|
||||||
|
// return group.Memberships.Select(gm => gm.User).ToList();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@@ -17,10 +17,14 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
_validator = validator;
|
_validator = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<Message> GetBaseQuery()
|
||||||
|
{
|
||||||
|
return _context.Messages.AsNoTracking();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Message>> GetAllAsync()
|
public async Task<List<Message>> GetAllAsync()
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ToListOrThrowIfEmpty(new NotFoundException("No messages found in the database"));
|
.ToListOrThrowIfEmpty(new NotFoundException("No messages found in the database"));
|
||||||
@@ -28,18 +32,17 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
|
|
||||||
public async Task<Message> FindByIdAsync(Guid messageId)
|
public async Task<Message> FindByIdAsync(Guid messageId)
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
|
.Include(m => m.MediaAttachments)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.FirstOrDefaultAsync(m => m.Id == messageId)
|
.FirstOrDefaultAsync(m => m.Id == messageId)
|
||||||
?? throw new NotFoundByKeyException<Guid>(messageId, "Message with given id does not exist");
|
?? throw new NotFoundByKeyException<Guid>(messageId, "Message with given id does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Message>> FindBySenderIdAsync(Guid senderId)
|
public async Task<List<Message>> FindBySenderIdAsync(Guid senderId)
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Where(m => m.SenderId == senderId)
|
.Where(m => m.SenderId == senderId)
|
||||||
@@ -48,8 +51,7 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
|
|
||||||
public async Task<List<Message>> FindByReceiverIdAsync(Guid receiverId)
|
public async Task<List<Message>> FindByReceiverIdAsync(Guid receiverId)
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Where(m => m.RecipientId == receiverId)
|
.Where(m => m.RecipientId == receiverId)
|
||||||
@@ -58,8 +60,7 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
|
|
||||||
public async Task<List<Message>> FindBySenderAndReceiverIdAsync(Guid senderId, Guid receiverId, RecipientType recipientType = RecipientType.User)
|
public async Task<List<Message>> FindBySenderAndReceiverIdAsync(Guid senderId, Guid receiverId, RecipientType recipientType = RecipientType.User)
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Where(m => m.SenderId == senderId
|
.Where(m => m.SenderId == senderId
|
||||||
@@ -70,8 +71,7 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
|
|
||||||
public async Task<List<Message>> FindBySentAtAsync(DateTime date)
|
public async Task<List<Message>> FindBySentAtAsync(DateTime date)
|
||||||
{
|
{
|
||||||
return await _context.Messages
|
return await GetBaseQuery()
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.ReplyToMessage)
|
.Include(m => m.ReplyToMessage)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Where(m => m.SentAt == date)
|
.Where(m => m.SentAt == date)
|
||||||
@@ -107,45 +107,42 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
var rowsAffected = await _context.Messages
|
var rowsAffected = await _context.Messages
|
||||||
.Where(m => m.Id == message.Id)
|
.Where(m => m.Id == message.Id)
|
||||||
.ExecuteUpdateAsync(u => u
|
.ExecuteUpdateAsync(u => u
|
||||||
.SetProperty(m => m.SenderId, message.SenderId)
|
|
||||||
.SetProperty(m => m.RecipientId, message.RecipientId)
|
|
||||||
.SetProperty(m => m.SentAt, message.SentAt)
|
|
||||||
.SetProperty(m => m.RecipientType, message.RecipientType)
|
|
||||||
.SetProperty(m => m.EncryptedContent, message.EncryptedContent)
|
.SetProperty(m => m.EncryptedContent, message.EncryptedContent)
|
||||||
.SetProperty(m => m.EditedAt, message.EditedAt)
|
.SetProperty(m => m.EditedAt, message.EditedAt)
|
||||||
.SetProperty(m => m.IsEdited, message.IsEdited)
|
.SetProperty(m => m.IsEdited, message.IsEdited)
|
||||||
.SetProperty(m => m.Reactions, message.Reactions)
|
|
||||||
.SetProperty(m => m.ReplyToMessageId, message.ReplyToMessageId)
|
|
||||||
.SetProperty(m => m.MessageViews, message.MessageViews)
|
|
||||||
.SetProperty(m => m.MediaAttachments, message.MediaAttachments)
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rowsAffected == 0)
|
if (rowsAffected == 0)
|
||||||
throw new UpdateException($"Not found message by given id {message.Id}");
|
throw new UpdateException($"Message with ID {message.Id} not found or no changes detected.");
|
||||||
|
}
|
||||||
|
catch (InvalidObjectException<Message> ex)
|
||||||
|
{
|
||||||
|
throw new UpdateException($"Invalid message data for ID {message.Id}", ex);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new UpdateException($"Error when updating the message {message.Id}", ex);
|
throw new UpdateException($"Error when updating message {message.Id}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveAsync(Guid messageId)
|
public async Task RemoveAsync(Guid messageId) // Reverted to Hard Delete
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await FindByIdAsync(messageId);
|
var message = await _context.Messages.FindAsync(messageId); // Find first, to ensure it exists
|
||||||
|
if (message == null)
|
||||||
|
throw new NotFoundByKeyException<Guid>(messageId, "Message not found.");
|
||||||
|
|
||||||
_context.Messages.Remove(result);
|
_context.Messages.Remove(message);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
catch (NotFoundByKeyException<Guid> ex)
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
{
|
{
|
||||||
throw new RemoveException($"Not found message by given id {messageId}", ex);
|
throw new RemoveException($"Message with ID {messageId} not found.", ex);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) // Catches DbUpdateException if there are FK constraints, etc.
|
||||||
{
|
{
|
||||||
throw new RemoveException("Error when removing the message", ex);
|
throw new RemoveException($"Error when deleting message {messageId}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,13 +150,11 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
{
|
{
|
||||||
_validator.Validate(message);
|
_validator.Validate(message);
|
||||||
|
|
||||||
return _context.Messages.Any(
|
return GetBaseQuery().Any(
|
||||||
m => m.Id == message.Id &&
|
m => m.Id == message.Id &&
|
||||||
m.SenderId == message.SenderId &&
|
m.SenderId == message.SenderId &&
|
||||||
m.RecipientId == message.RecipientId &&
|
m.RecipientId == message.RecipientId &&
|
||||||
m.EncryptedContent == message.EncryptedContent &&
|
m.EncryptedContent == message.EncryptedContent &&
|
||||||
m.EditedAt == message.EditedAt &&
|
|
||||||
m.IsEdited == message.IsEdited &&
|
|
||||||
m.SentAt == message.SentAt &&
|
m.SentAt == message.SentAt &&
|
||||||
m.ReplyToMessageId == message.ReplyToMessageId
|
m.ReplyToMessageId == message.ReplyToMessageId
|
||||||
);
|
);
|
||||||
@@ -167,6 +162,6 @@ public class MessagesRepository : IMessagesRepository
|
|||||||
|
|
||||||
public bool Exist(Guid messageId)
|
public bool Exist(Guid messageId)
|
||||||
{
|
{
|
||||||
return _context.Messages.Any(m => m.Id == messageId);
|
return GetBaseQuery().Any(m => m.Id == messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user