mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 11:44:56 +00:00
be0edb0f94
Introduces IHubUserAccessor and its implementation to centralize user ID retrieval from SignalR HubCallerContext, replacing duplicated logic in ChatsHub, FriendsHub, and PresenceHub. Moves extension and mapping files to a Common directory, adds UserToUserDtoMappingAction for online status mapping, and implements OnlineUserStore with tests for tracking online users. Updates dependency injection and test code to use the new abstractions.
308 lines
12 KiB
C#
308 lines
12 KiB
C#
using Govor.API.Common.SignalR.Helpers;
|
|
using Govor.Application.Exceptions.VerifyFriendship;
|
|
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.Messages;
|
|
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;
|
|
private readonly IHubUserAccessor _userAccessor;
|
|
|
|
public ChatsHub(ILogger<ChatsHub> logger, IMessageCommandService messageCommandService, IUserGroupsService userService, IHubUserAccessor userAccessor)
|
|
{
|
|
_logger = logger;
|
|
_messageCommandService = messageCommandService;
|
|
_userService = userService;
|
|
_userAccessor = userAccessor;
|
|
}
|
|
|
|
public override async Task OnConnectedAsync()
|
|
{
|
|
var userId = _userAccessor.GetUserId(Context);
|
|
if (userId == Guid.Empty)
|
|
{
|
|
_logger.LogWarning("User connected with invalid UserID claim.");
|
|
Context.Abort();
|
|
return;
|
|
}
|
|
|
|
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 = _userAccessor.GetUserId(Context, true);
|
|
if (userId != Guid.Empty)
|
|
{
|
|
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);
|
|
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<HubResult<UserMessageResponse>> Send(MessageRequest request)
|
|
{
|
|
var senderId= _userAccessor.GetUserId(Context);
|
|
|
|
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
|
|
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
|
|
{
|
|
_logger.LogWarning("Empty message received from user {UserId}", senderId);
|
|
return HubResult<UserMessageResponse>.BadRequest("Message must contain content or media.");
|
|
}
|
|
|
|
_logger.LogInformation("Sending message from {SenderId} to {RecipientId} ({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>()
|
|
);
|
|
|
|
try
|
|
{
|
|
var result = await _messageCommandService.SendMessageAsync(sendMessageParams);
|
|
|
|
if (!result.IsSuccess || result.Message.Id == Guid.Empty)
|
|
return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Failed to send message", result.Exception);
|
|
|
|
var response = BuildUserMessageResponse(result.Message, request.ReplyToMessageId);
|
|
|
|
await NotifyClientsAboutMessage(response);
|
|
|
|
return HubResult<UserMessageResponse>.Ok(response);
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
return LogAndUnauthorized<UserMessageResponse>(ex, "Unauthorized sending attempt", senderId,
|
|
request.RecipientId);
|
|
}
|
|
catch (FriendshipException)
|
|
{
|
|
return HubResult<UserMessageResponse>.Unauthorized(
|
|
"You cannot send this message because you are not friends.");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return HubResult<UserMessageResponse>.BadRequest("Invalid message content.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Unhandled exception", ex);
|
|
}
|
|
}
|
|
|
|
|
|
public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
|
|
{
|
|
var removerId = _userAccessor.GetUserId(Context);
|
|
_logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId);
|
|
|
|
try
|
|
{
|
|
var result = await _messageCommandService.DeleteMessageAsync(new DeleteMessage(removerId, request.MessageId));
|
|
|
|
if (!result.IsSuccess || result.OriginalMessage == null)
|
|
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Message deletion failed", result.Exception);
|
|
|
|
var notification = new MessageRemovedResponse
|
|
{
|
|
MessageId = request.MessageId,
|
|
SenderId = result.OriginalMessage.SenderId,
|
|
RecipientId = result.OriginalMessage.RecipientId,
|
|
RecipientType = result.OriginalMessage.RecipientType
|
|
};
|
|
|
|
await NotifyClientsAboutRemoval(notification);
|
|
|
|
_logger.LogInformation("Message {MessageId} removed successfully by {RemoverId}", request.MessageId, removerId);
|
|
return HubResult<MessageRemovedResponse>.Ok(notification);
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
return LogAndUnauthorized<MessageRemovedResponse>(ex, "Unauthorized removal", removerId, request.MessageId);
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return LogAndNotFound<MessageRemovedResponse>(ex, "Message not found", removerId, request.MessageId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Unhandled deletion error", ex);
|
|
}
|
|
}
|
|
|
|
|
|
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
|
|
{
|
|
var editor = _userAccessor.GetUserId(Context);
|
|
_logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor);
|
|
|
|
var editMessageParam = new EditMessage(editor,
|
|
request.MessageId,
|
|
request.NewEncryptedContent,
|
|
DateTime.UtcNow);
|
|
|
|
try
|
|
{
|
|
var result = await _messageCommandService.EditMessageAsync(editMessageParam);
|
|
|
|
if (!result.IsSuccess || result.OriginalMessage == null)
|
|
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Edit message error",
|
|
result.Exception);
|
|
|
|
var response = new MessageEditResponse()
|
|
{
|
|
MessageId = result.messageId,
|
|
EditorId = editor,
|
|
RecipientId = result.OriginalMessage.RecipientId,
|
|
RecipientType = result.OriginalMessage.RecipientType,
|
|
NewEncryptedContent = request.NewEncryptedContent,
|
|
EditedAt = editMessageParam.EditedAt,
|
|
};
|
|
|
|
await NotifyClientsAboutEdit(response);
|
|
|
|
_logger.LogInformation("Message {MessageId} edited successfully by {editor}", request.MessageId, editor);
|
|
|
|
return HubResult<MessageEditResponse>.Ok(response);
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
return LogAndUnauthorized<MessageEditResponse>(ex, "Unauthorized editing", editor, request.MessageId);
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return LogAndNotFound<MessageEditResponse>(ex, "Message not found", editor, request.MessageId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Unhandled exception error", ex);
|
|
}
|
|
}
|
|
|
|
|
|
private UserMessageResponse BuildUserMessageResponse(Message message, Guid? replyToId)
|
|
{
|
|
return new UserMessageResponse
|
|
{
|
|
MessageId = message.Id,
|
|
SenderId = message.SenderId,
|
|
RecipientId = message.RecipientId,
|
|
RecipientType = message.RecipientType,
|
|
EncryptedContent = message.EncryptedContent,
|
|
SentAt = message.SentAt,
|
|
IsEdited = false,
|
|
MediaAttachments = message.MediaAttachments.Select(m => m.MediaFile).ToList(),
|
|
ReplyToMessageId = replyToId
|
|
};
|
|
}
|
|
|
|
private async Task NotifyClientsAboutMessage(UserMessageResponse response)
|
|
{
|
|
string group = response.RecipientType == RecipientType.User
|
|
? response.RecipientId.ToString()
|
|
: $"group_{response.RecipientId}";
|
|
|
|
await Clients.Group(group).SendAsync("ReceiveMessage", response);
|
|
await Clients.Caller.SendAsync("MessageSent", response);
|
|
}
|
|
|
|
private async Task NotifyClientsAboutRemoval(MessageRemovedResponse response)
|
|
{
|
|
if (response.RecipientType == RecipientType.User)
|
|
{
|
|
await Clients.Group(response.SenderId.ToString()).SendAsync("MessageRemoved", response);
|
|
if (response.SenderId != response.RecipientId)
|
|
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageRemoved", response);
|
|
}
|
|
else
|
|
{
|
|
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageRemoved", response);
|
|
}
|
|
}
|
|
|
|
private async Task NotifyClientsAboutEdit(MessageEditResponse response)
|
|
{
|
|
if (response.RecipientType == RecipientType.User)
|
|
{
|
|
await Clients.Group(response.EditorId.ToString()).SendAsync("MessageEdit", response);
|
|
if (response.EditorId != response.RecipientId)
|
|
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageEdit", response);
|
|
}
|
|
else
|
|
{
|
|
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageEdit", response);
|
|
}
|
|
}
|
|
|
|
// Logging helpers
|
|
private HubResult<T> LogAndError<T>(Guid userId, Guid targetId, string message, Exception ex)
|
|
{
|
|
_logger.LogError(ex, "{Message} from {UserId} to {TargetId}", message, userId, targetId);
|
|
return HubResult<T>.Error(ex?.Message ?? "Internal server error");
|
|
}
|
|
|
|
private HubResult<T> LogAndUnauthorized<T>(Exception ex, string msg, Guid userId, Guid targetId)
|
|
{
|
|
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
|
return HubResult<T>.Unauthorized("You are not authorized to perform this action.");
|
|
}
|
|
|
|
private HubResult<T> LogAndNotFound<T>(Exception ex, string msg, Guid userId, Guid targetId)
|
|
{
|
|
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
|
|
return HubResult<T>.NotFound("Message not found.");
|
|
}
|
|
} |