mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 11:44:56 +00:00
the basis for group and private messages
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Govor.API.Services;
|
using Govor.Application.Interfaces;
|
||||||
|
using Govor.Application.Interfaces.Infrastructure.Extensions;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -9,24 +10,36 @@ namespace Govor.API.Controllers;
|
|||||||
public class InviteController : ControllerBase
|
public class InviteController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IGroupService _groupService;
|
private readonly IGroupService _groupService;
|
||||||
|
private readonly ILogger<InviteController> _logger;
|
||||||
public InviteController(IGroupService groupService)
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
public InviteController(IGroupService groupService,
|
||||||
|
ILogger<InviteController> logger)
|
||||||
{
|
{
|
||||||
_groupService = groupService;
|
_groupService = groupService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{code}")]
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[HttpGet("{code}")]
|
||||||
public IActionResult JoinGroup(string code)
|
public IActionResult JoinGroup(string code)
|
||||||
{
|
{
|
||||||
var group = _groupService.GetGroupByInvite(code);
|
try
|
||||||
if (group == null) return NotFound();
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
{
|
||||||
groupId = group.Id,
|
_groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code);
|
||||||
name = group.Name,
|
|
||||||
isChannel = group.IsChannel
|
var group = _groupService.GetGroupByInviteCode(code);
|
||||||
});
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
groupId = group.Id,
|
||||||
|
name = group.Name,
|
||||||
|
isChannel = group.IsChannel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
return StatusCode(500, ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-83
@@ -13,42 +13,138 @@ 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;
|
||||||
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 Send(MessageRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.EncryptedContent) && (request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}", GetUserId());
|
||||||
|
throw new ArgumentException("Message cannot be empty (must have content or attachments).");
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderId = GetUserId();
|
||||||
|
_logger.LogInformation("Message send initiated by {SenderId} to {RecipientId} of type {RecipientType}", senderId, request.RecipientId, request.RecipientType);
|
||||||
|
|
||||||
|
var sendMessageParams = new SendMessage(
|
||||||
|
EncryptContent: request.EncryptedContent,
|
||||||
|
ReplyToMessageId: request.ReplyToMessageId,
|
||||||
|
FromUserId: senderId,
|
||||||
|
RecipientId: request.RecipientId,
|
||||||
|
RecipientType: request.RecipientType,
|
||||||
|
SendAt: DateTime.UtcNow,
|
||||||
|
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
||||||
|
f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty<SendMedia>()
|
||||||
|
);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _messageService.SendMessageAsync(sendMessageParams);
|
||||||
|
|
||||||
|
if (!result.IsSuccess || result.MessageId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogError(result.Exception, "Failed to send message from {SenderId} to {RecipientId}. Error: {ErrorMessage}", senderId, request.RecipientId, result.Exception?.Message ?? "Unknown error");
|
||||||
|
if (result.Exception != null) throw result.Exception;
|
||||||
|
throw new HubException("Failed to send message due to an internal error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageResponse = new UserMessageResponse // Assuming a response DTO
|
||||||
|
{
|
||||||
|
MessageId = result.MessageId,
|
||||||
|
SenderId = senderId,
|
||||||
|
RecipientId = request.RecipientId,
|
||||||
|
RecipientType = request.RecipientType,
|
||||||
|
EncryptedContent = request.EncryptedContent,
|
||||||
|
SentAt = sendMessageParams.SendAt,
|
||||||
|
IsEdited = false,
|
||||||
|
MediaAttachments = request.MediaAttachments,
|
||||||
|
ReplyToMessageId = request.ReplyToMessageId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify recipient (user or group)
|
||||||
|
if (request.RecipientType == RecipientType.User)
|
||||||
|
{
|
||||||
|
// Send to the recipient's personal group
|
||||||
|
await Clients.Group(request.RecipientId.ToString()).SendAsync("ReceiveMessage", messageResponse);
|
||||||
|
}
|
||||||
|
else if (request.RecipientType == RecipientType.Group)
|
||||||
|
{
|
||||||
|
// Send to all members of the group, including the sender if they are part of the group via a different connection
|
||||||
|
await Clients.Group($"group_{request.RecipientId}").SendAsync("ReceiveMessage", messageResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify sender (confirmation) on their connection
|
||||||
|
await Clients.Caller.SendAsync("MessageSent", messageResponse); // Or use "ReceiveMessage" if the sender should also just get it like anyone else
|
||||||
|
|
||||||
|
_logger.LogInformation("Message {MessageId} sent successfully from {SenderId} to {RecipientId} ({RecipientType})", result.MessageId, senderId, request.RecipientId, request.RecipientType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId);
|
||||||
|
// Consider sending a specific error message to the caller instead of a generic HubException or rethrowing.
|
||||||
|
// For example: await Clients.Caller.SendAsync("SendMessageFailed", new { Error = ex.Message });
|
||||||
|
throw new HubException("An error occurred while sending the message.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Remove(Guid recipientId, Guid messageId)
|
public async Task Remove(Guid recipientId, Guid messageId)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -59,79 +155,19 @@ public class ChatsHub : Hub
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendGroup(GroupMessageRequest request)
|
|
||||||
{
|
|
||||||
var senderId = GetUserId();
|
|
||||||
|
|
||||||
// Создание сообщения
|
|
||||||
var message = new SendMessage(
|
|
||||||
EncryptContent: request.EncryptedContent,
|
|
||||||
ReplyToMessageId: request.ReplyToMessageId,
|
|
||||||
FromUserId: senderId,
|
|
||||||
RecipientId: request.GroupId,
|
|
||||||
SendAt: DateTime.UtcNow,
|
|
||||||
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
|
||||||
f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty<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)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(request.EncryptedContent))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Empty message received from user {UserId}", GetUserId());
|
|
||||||
throw new ArgumentException("Message cannot be empty", nameof(request.EncryptedContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
var senderId = GetUserId();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Message sent from {SenderId} to {RecipientId} at {UtcNow}", senderId, request.RecipientId, DateTime.UtcNow);
|
|
||||||
|
|
||||||
var message = new SendMessage(
|
|
||||||
EncryptContent: request.EncryptedContent,
|
|
||||||
ReplyToMessageId: request.ReplyToMessageId,
|
|
||||||
FromUserId: senderId,
|
|
||||||
RecipientId: request.RecipientId,
|
|
||||||
SendAt: DateTime.UtcNow,
|
|
||||||
Media: request.MediaAttachments?.Select(f => new SendMedia(
|
|
||||||
f.MediaId, f.EncryptedKey, f.Type, f.MimeType)) ?? Array.Empty<SendMedia>());
|
|
||||||
|
|
||||||
Result result = await _chatService.SendMessageAsync(message);
|
|
||||||
if(result.IsSuccess == false)
|
|
||||||
throw result.Exception;
|
|
||||||
|
|
||||||
// Sending a message to the sender and recipient
|
|
||||||
await Clients.Group(message.RecipientId.ToString()).SendAsync("Receive", message);
|
|
||||||
await Clients.Group(message.FromUserId.ToString()).SendAsync("Receive", message);
|
|
||||||
// TODO: Send to Group
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, request.RecipientId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Guid GetUserId()
|
private Guid GetUserId(bool suppressException = false)
|
||||||
{
|
{
|
||||||
var userIdClaim = Context.User?.FindFirst("userID")?.Value;
|
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,16 @@
|
|||||||
using Govor.Application.Interfaces.Messages;
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
using Govor.Core.Models;
|
using Govor.Core.Models;
|
||||||
|
|
||||||
namespace Govor.API.Services;
|
namespace Govor.Application.Interfaces;
|
||||||
|
|
||||||
public interface IGroupService : IMessageSendingService, IMessageManagementService
|
public interface IGroupService
|
||||||
{
|
{
|
||||||
ChatGroup GetGroupByInvite(string code);
|
Task<ChatGroup> GetGroupByIdAsync(Guid groupId);
|
||||||
|
Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds);
|
||||||
|
Task<Result> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode);
|
||||||
|
Task<Result> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId);
|
||||||
|
Task<Result> DeleteGroupAsync(Guid groupId, Guid userId);
|
||||||
|
Task<List<User>> GetGroupMembersAsync(Guid groupId);
|
||||||
|
Task<List<ChatGroup>>GetUserGroupsAsync(Guid userId);
|
||||||
|
ChatGroup GetGroupByInviteCode(string code);
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Govor.Application.Interfaces.Messages;
|
|
||||||
|
|
||||||
public interface IChatService : IMessageSendingService, IMessageManagementService
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
|
||||||
|
|
||||||
namespace Govor.Application.Interfaces.Messages;
|
|
||||||
|
|
||||||
public interface IMessageManagementService
|
|
||||||
{
|
|
||||||
Task<Result> EditMessageAsync(Guid editorId, Guid messageId, string newContent);
|
|
||||||
Task<Result> DeleteMessageAsync(Guid editorId, Guid messageId);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Govor.Application.Interfaces.Messages;
|
|
||||||
|
|
||||||
public interface IMessageReactionService
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
|
||||||
|
|
||||||
namespace Govor.Application.Interfaces.Messages;
|
|
||||||
|
|
||||||
public interface IMessageSendingService
|
|
||||||
{
|
|
||||||
Task<Result> SendMessageAsync(SendMessage newMessage);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
|
||||||
|
namespace Govor.Application.Interfaces.Messages;
|
||||||
|
|
||||||
|
// Combining IChatService and IGroupService functionalities relevant to messages
|
||||||
|
public interface IMessageService
|
||||||
|
{
|
||||||
|
Task<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);
|
||||||
@@ -2,7 +2,4 @@ using Govor.Core.Models;
|
|||||||
|
|
||||||
namespace Govor.Application.Interfaces.Messages.Parameters;
|
namespace Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
|
||||||
public record SendMedia(Guid Id,
|
public record SendMedia(Guid MediaId, string EncryptedKey, MediaType Type, string MimeType);
|
||||||
string EncryptedKey,
|
|
||||||
MediaType Type,
|
|
||||||
string MimeType);
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
using Govor.Core.Models;
|
||||||
|
|
||||||
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,
|
||||||
|
RecipientType RecipientType,
|
||||||
Guid RecipientId,
|
Guid RecipientId,
|
||||||
Guid FromUserId,
|
Guid FromUserId,
|
||||||
DateTime SendAt,
|
DateTime SendAt,
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
using Govor.Application.Interfaces;
|
||||||
|
using Govor.Application.Interfaces.Messages;
|
||||||
|
using Govor.Application.Interfaces.Messages.Parameters;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
using Govor.Core.Repositories.Groups;
|
||||||
|
using Govor.Core.Repositories.Messages;
|
||||||
|
using Govor.Core.Repositories.Users;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Govor.Application.Services;
|
||||||
|
|
||||||
|
public class MessageService : IMessageService
|
||||||
|
{
|
||||||
|
private readonly IMessagesRepository _messagesRepository;
|
||||||
|
private readonly IUsersRepository _usersRepository; // For validating user recipients
|
||||||
|
private readonly IGroupsRepository _groupsRepository; // For validating group recipients and fetching members
|
||||||
|
private readonly IVerifyFriendship _verifyFriendship; // For private messages
|
||||||
|
private readonly ILogger<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 (!_groupsRepository.Exists(sendParams.RecipientId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attempt to send message to non-existent group {GroupId}", sendParams.RecipientId);
|
||||||
|
return new SendMessageResult(false, new KeyNotFoundException($"Recipient group {sendParams.RecipientId} not found."), Guid.Empty);
|
||||||
|
}
|
||||||
|
// TODO: Optionally, verify if sender is a member of the group
|
||||||
|
// bool isMember = await _groupsRepository.IsUserMemberOfGroupAsync(sendParams.FromUserId, sendParams.RecipientId);
|
||||||
|
// if (!isMember)
|
||||||
|
// {
|
||||||
|
// _logger.LogWarning("User {UserId} attempted to send message to group {GroupId} but is not a member", sendParams.FromUserId, sendParams.RecipientId);
|
||||||
|
// return new SendMessageResult(false, new UnauthorizedAccessException("Sender is not a member of the group."), Guid.Empty);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("Invalid recipient type specified: {RecipientType}", sendParams.RecipientType);
|
||||||
|
return new SendMessageResult(false, new ArgumentException("Invalid recipient type."), Guid.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageId = Guid.NewGuid();
|
||||||
|
var message = new Message
|
||||||
|
{
|
||||||
|
Id = messageId,
|
||||||
|
SenderId = sendParams.FromUserId,
|
||||||
|
RecipientId = sendParams.RecipientId,
|
||||||
|
RecipientType = sendParams.RecipientType,
|
||||||
|
EncryptedContent = sendParams.EncryptContent,
|
||||||
|
SentAt = sendParams.SendAt,
|
||||||
|
IsEdited = false,
|
||||||
|
ReplyToMessageId = sendParams.ReplyToMessageId,
|
||||||
|
MediaAttachments = sendParams.Media?.Select(m => new MediaAttachments
|
||||||
|
{
|
||||||
|
Id = m.MediaId, // Assuming SendMedia provides Id or it's generated here/by repo
|
||||||
|
MessageId = messageId,
|
||||||
|
Type = m.Type,
|
||||||
|
MimeType = m.MimeType,
|
||||||
|
EncryptedKey = m.EncryptedKey,
|
||||||
|
// FilePath will be set by storage service or handled by repository if media ID is pre-generated
|
||||||
|
}).ToList() ?? new List<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.FindByIdAsync(editParams.MessageId);
|
||||||
|
|
||||||
|
if (message.SenderId != editParams.EditorId)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User {EditorId} attempted to edit message {MessageId} not sent by them (sender was {SenderId})", editParams.EditorId, editParams.MessageId, message.SenderId);
|
||||||
|
return new EditMessageResult(false, new UnauthorizedAccessException("User is not authorized to edit this message."), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add a time limit for editing messages? e.g., if (message.SentAt < DateTime.UtcNow.AddMinutes(-15)) throw new Exception("Edit time limit exceeded");
|
||||||
|
|
||||||
|
// Keep a copy of the original message state for the result, if needed by Hub for notifications
|
||||||
|
var originalMessageForNotification = new Message
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
SenderId = message.SenderId,
|
||||||
|
RecipientId = message.RecipientId,
|
||||||
|
RecipientType = message.RecipientType,
|
||||||
|
// Populate other fields if necessary for the notification
|
||||||
|
};
|
||||||
|
|
||||||
|
message.EncryptedContent = editParams.NewContent;
|
||||||
|
message.IsEdited = true;
|
||||||
|
message.EditedAt = editParams.EditedAt;
|
||||||
|
|
||||||
|
await _messagesRepository.UpdateAsync(message);
|
||||||
|
_logger.LogInformation("Message {MessageId} edited successfully by user {EditorId}", editParams.MessageId, editParams.EditorId);
|
||||||
|
return new EditMessageResult(true, null, originalMessageForNotification);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error editing message {MessageId} by user {EditorId}", editParams.MessageId, editParams.EditorId);
|
||||||
|
return new EditMessageResult(false, ex, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteMessageResult> DeleteMessageAsync(DeleteMessage deleteParams)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = await _messagesRepository.FindByIdAsync(deleteParams.MessageId);
|
||||||
|
|
||||||
|
if (message.SenderId != deleteParams.DeleterId)
|
||||||
|
{
|
||||||
|
// TODO: Allow group admins to delete messages in their groups?
|
||||||
|
// if (message.RecipientType == RecipientType.Group) {
|
||||||
|
// bool isAdmin = await _groupsRepository.IsUserAdminOfGroupAsync(deleteParams.DeleterId, message.RecipientId);
|
||||||
|
// if (!isAdmin) {
|
||||||
|
// _logger.LogWarning("User {DeleterId} (not sender or admin) attempted to delete group message {MessageId}", deleteParams.DeleterId, deleteParams.MessageId);
|
||||||
|
// return new DeleteMessageResult(false, new UnauthorizedAccessException("User is not authorized to delete this message."), null);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
_logger.LogWarning("User {DeleterId} attempted to delete message {MessageId} not sent by them (sender was {SenderId})", deleteParams.DeleterId, deleteParams.MessageId, message.SenderId);
|
||||||
|
return new DeleteMessageResult(false, new UnauthorizedAccessException("User is not authorized to delete this message."), null);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a copy of the original message state for the result, if needed by Hub for notifications
|
||||||
|
var originalMessageForNotification = new Message
|
||||||
|
{
|
||||||
|
Id = message.Id,
|
||||||
|
SenderId = message.SenderId,
|
||||||
|
RecipientId = message.RecipientId,
|
||||||
|
RecipientType = message.RecipientType,
|
||||||
|
// Populate other fields if necessary for the notification
|
||||||
|
};
|
||||||
|
|
||||||
|
await _messagesRepository.RemoveAsync(deleteParams.MessageId);
|
||||||
|
|
||||||
|
// TODO: Delete associated media attachments from storage if they are no longer referenced.
|
||||||
|
|
||||||
|
_logger.LogInformation("Message {MessageId} deleted successfully by user {DeleterId}", deleteParams.MessageId, deleteParams.DeleterId);
|
||||||
|
return new DeleteMessageResult(true, null, originalMessageForNotification);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting message {MessageId} by user {DeleterId}", deleteParams.MessageId, deleteParams.DeleterId);
|
||||||
|
return new DeleteMessageResult(false, ex, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using Govor.Application.Interfaces;
|
|
||||||
using Govor.Application.Interfaces.Messages;
|
|
||||||
using Govor.Application.Interfaces.Messages.Parameters;
|
|
||||||
using Govor.Core.Models;
|
|
||||||
using Govor.Core.Repositories.Messages;
|
|
||||||
using Govor.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Govor.Application.Services;
|
|
||||||
|
|
||||||
public class PrivateChatService : IChatService
|
|
||||||
{
|
|
||||||
private readonly IVerifyFriendship _verifyFriendship;
|
|
||||||
private readonly IMessagesRepository _messages;
|
|
||||||
|
|
||||||
public PrivateChatService(IMessagesRepository messages, IVerifyFriendship verifyFriendship)
|
|
||||||
{
|
|
||||||
_messages = messages;
|
|
||||||
_verifyFriendship = verifyFriendship;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Result> EditMessageAsync(Guid editorId, Guid messageId, string newContent)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Result> DeleteMessageAsync(Guid editorId, Guid messageId)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
||||||
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();
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
using Govor.Contracts.Requests.SignalR;
|
using Govor.Contracts.Requests.SignalR;
|
||||||
|
using Govor.Core.Models;
|
||||||
|
|
||||||
namespace Govor.Contracts.Responses.SignalR;
|
namespace Govor.Contracts.Responses.SignalR;
|
||||||
|
|
||||||
public record UserMessageResponse
|
public record UserMessageResponse
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid MessageId { get; init; }
|
||||||
public Guid SenderId { get; init; }
|
public Guid SenderId { get; init; }
|
||||||
|
public Guid RecipientId { get; init; }
|
||||||
|
public RecipientType RecipientType{get; init; }
|
||||||
public string EncryptedContent { get; init; } = string.Empty;
|
public string EncryptedContent { get; init; } = string.Empty;
|
||||||
public Guid? ReplyToMessageId { get; set; }
|
public Guid? ReplyToMessageId { get; init; }
|
||||||
public List<MediaReference> MediaReferences { get; init; } = new List<MediaReference>();
|
public DateTime SentAt { get; init; }
|
||||||
|
public bool IsEdited { get; init; } = false;
|
||||||
|
public List<MediaReference> MediaAttachments { get; init; } = new List<MediaReference>();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Govor.Core.Models;
|
||||||
|
|
||||||
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
|
public interface IGroupsExist
|
||||||
|
{
|
||||||
|
public bool Exists(Guid groupId);
|
||||||
|
public bool Exists(ChatGroup chatGroup);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Govor.Core.Repositories.Groups;
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
public class IGroupsReader
|
public interface IGroupsReader
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
|
public interface IGroupsRepository : IGroupsReader, IGroupsExist, IGroupsWriter
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Govor.Core.Repositories.Groups;
|
||||||
|
|
||||||
|
public interface IGroupsWriter
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user