the basis for group and private messages

This commit is contained in:
Artemy
2025-07-04 14:33:54 +07:00
parent fab3d6b67b
commit bdcfd32b11
20 changed files with 418 additions and 207 deletions
+25 -12
View File
@@ -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.Mvc;
@@ -9,24 +10,36 @@ namespace Govor.API.Controllers;
public class InviteController : ControllerBase
{
private readonly IGroupService _groupService;
public InviteController(IGroupService groupService)
private readonly ILogger<InviteController> _logger;
private readonly ICurrentUserService _currentUser;
public InviteController(IGroupService groupService,
ILogger<InviteController> logger)
{
_groupService = groupService;
_logger = logger;
}
[HttpGet("{code}")]
[Authorize]
[HttpGet("{code}")]
public IActionResult JoinGroup(string code)
{
var group = _groupService.GetGroupByInvite(code);
if (group == null) return NotFound();
return Ok(new
try
{
groupId = group.Id,
name = group.Name,
isChannel = group.IsChannel
});
_groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code);
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
View File
@@ -13,42 +13,138 @@ namespace Govor.API.Hubs;
public class ChatsHub : Hub
{
private readonly ILogger<ChatsHub> _logger;
private readonly IChatService _chatService;
private readonly IGroupService _groupService;
private readonly IMessageService _messageService;
public ChatsHub(ILogger<ChatsHub> logger,
IChatService chatService
)
public ChatsHub(ILogger<ChatsHub> logger, IMessageService messageService)
{
_logger = logger;
_chatService = chatService;
_messageService = messageService;
}
public override async Task OnConnectedAsync()
{
var userId = GetUserId();
if (userId != Guid.Empty)
if (userId == Guid.Empty)
{
// Binding ConnectionId to UserId
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId);
_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);
// 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();
}
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)
{
// Remove user from their own group
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);
}
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)
{
@@ -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;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
_logger.LogError("Could not retrieve sender userId");
throw new UnauthorizedAccessException("userID claim is missing or invalid");
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;
}