Files
Govor/Govor.API/Hubs/ChatsHub.cs
T
2025-07-11 15:52:10 +07:00

241 lines
10 KiB
C#

using Govor.API.Services;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.Messages.Parameters;
using Govor.Contracts.Requests.SignalR;
using Govor.Contracts.Responses.SignalR;
using Govor.Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
[Authorize] // api/chats
public class ChatsHub : Hub
{
private readonly ILogger<ChatsHub> _logger;
private readonly IMessageCommandService _messageCommandService;
private readonly IUserGroupsService _userService;
public ChatsHub(ILogger<ChatsHub> logger, IMessageCommandService messageCommandService, IUserGroupsService userService)
{
_logger = logger;
_messageCommandService = messageCommandService;
_userService = userService;
}
public override async Task OnConnectedAsync()
{
var userId = GetUserId();
if (userId == Guid.Empty)
{
_logger.LogWarning("User connected with invalid UserID claim.");
Context.Abort(); // Abort connection if userID is invalid
return;
}
// Add user to their own group (for private messages and notifications)
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group",
userId, Context.ConnectionId);
var userGroups = await _userService.GetUserGroupsAsync(userId);
foreach (var group in userGroups)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}");
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId =
GetUserId(suppressException: true); // Suppress exception if userID is not found (e.g. connection aborted early)
if (userId != Guid.Empty)
{
// Remove user from their own group
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation(
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
Context.ConnectionId);
var userGroups =
await _userService
.GetUserGroupsAsync(
userId); // This might be problematic if the service relies on the user being connected
foreach (var group in userGroups)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}");
}
}
else if (exception != null)
{
_logger.LogWarning(exception,
"User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
else
{
_logger.LogInformation(
"User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception);
}
public async Task Send(MessageRequest request)
{
var senderId = GetUserId();
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
{
_logger.LogWarning("Empty message (no content and no attachments) received from user {UserId}", senderId);
throw new ArgumentException("Message cannot be empty (must have content or attachments).");
}
_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)) ??
Array.Empty<SendMedia>());
var result = await _messageCommandService.SendMessageAsync(sendMessageParams);
if (!result.IsSuccess || result.Message.Id == 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.Message.Id,
SenderId = result.Message.SenderId,
RecipientId = result.Message.RecipientId,
RecipientType = result.Message.RecipientType,
EncryptedContent = result.Message.EncryptedContent,
SentAt = result.Message.SentAt,
IsEdited = false,
MediaAttachments = result.Message.MediaAttachments.Select(m => m.MediaFile).ToList(),
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.Message.Id, senderId, request.RecipientId, request.RecipientType);
}
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 _messageCommandService.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);
}
}
public async Task Edit(EditMessageRequest request)
{
}
private Guid GetUserId(bool suppressException = false)
{
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
if (!suppressException)
{
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
}
return Guid.Empty;
}
return userId;
}
}