.net10 сервер с рабочими звонками и файлами
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MediatR;
|
||||
using Vortex.Modules.Chats.Application.Messages.Send;
|
||||
using Vortex.Modules.Chats.Domain;
|
||||
using Vortex.Shared.Kernel;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Vortex.Modules.Chats.Infrastructure.SignalR;
|
||||
|
||||
/// <summary>
|
||||
/// Хаб SignalR для обработки сообщений и WebRTC сигналинга в реальном времени.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public sealed class ChatHub : Hub
|
||||
{
|
||||
// Маппинг userId → список connectionId
|
||||
private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
|
||||
|
||||
private readonly ISender _sender;
|
||||
private readonly IUserContext _userContext;
|
||||
private readonly IChatRepository _chatRepository;
|
||||
|
||||
public ChatHub(ISender sender, IUserContext userContext, IChatRepository chatRepository)
|
||||
{
|
||||
_sender = sender;
|
||||
_userContext = userContext;
|
||||
_chatRepository = chatRepository;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
if (_userContext.IsAuthenticated)
|
||||
{
|
||||
var userId = _userContext.UserId.ToString();
|
||||
_userConnections.AddOrUpdate(
|
||||
userId,
|
||||
_ => new HashSet<string> { Context.ConnectionId },
|
||||
(_, set) => { lock (set) { set.Add(Context.ConnectionId); } return set; }
|
||||
);
|
||||
|
||||
var userChats = await _chatRepository.GetUserChatsAsync(_userContext.UserId, Context.ConnectionAborted);
|
||||
foreach (var chat in userChats)
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, chat.Id.ToString());
|
||||
}
|
||||
|
||||
await Clients.Others.SendAsync("user_online", new { userId });
|
||||
}
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (_userContext.IsAuthenticated)
|
||||
{
|
||||
var userId = _userContext.UserId.ToString();
|
||||
if (_userConnections.TryGetValue(userId, out var set))
|
||||
{
|
||||
lock (set) { set.Remove(Context.ConnectionId); }
|
||||
if (set.Count == 0)
|
||||
{
|
||||
_userConnections.TryRemove(userId, out _);
|
||||
await Clients.Others.SendAsync("user_offline", new { userId, lastSeen = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
}
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Chat methods
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
[HubMethodName("send_message")]
|
||||
public async Task SendMessage(SendMessageHubRequest request)
|
||||
{
|
||||
var command = new SendMessageCommand(
|
||||
request.ChatId,
|
||||
_userContext.UserId,
|
||||
request.Content,
|
||||
request.Type,
|
||||
request.MediaUrl,
|
||||
request.MediaType,
|
||||
request.FileName,
|
||||
request.FileSize);
|
||||
|
||||
await _sender.Send(command);
|
||||
}
|
||||
|
||||
[HubMethodName("read_messages")]
|
||||
public async Task ReadMessages(ReadMessagesRequest request)
|
||||
{
|
||||
if (request.MessageIds != null && request.MessageIds.Any())
|
||||
{
|
||||
var parsedIds = request.MessageIds
|
||||
.Select(id => Guid.TryParse(id, out var parsed) ? parsed : Guid.Empty)
|
||||
.Where(id => id != Guid.Empty)
|
||||
.ToList();
|
||||
|
||||
if (parsedIds.Any())
|
||||
{
|
||||
var command = new Vortex.Modules.Chats.Application.Messages.Read.ReadMessagesCommand(
|
||||
request.ChatId, _userContext.UserId, parsedIds);
|
||||
await _sender.Send(command);
|
||||
}
|
||||
}
|
||||
|
||||
await Clients.Group(request.ChatId.ToString()).SendAsync("messages_read", new
|
||||
{
|
||||
ChatId = request.ChatId.ToString(),
|
||||
UserId = _userContext.UserId,
|
||||
MessageIds = request.MessageIds ?? new List<string>()
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("typing_start")]
|
||||
public async Task TypingStart(string chatId)
|
||||
{
|
||||
await Clients.Group(chatId).SendAsync("user_typing", new { ChatId = chatId, UserId = _userContext.UserId });
|
||||
}
|
||||
|
||||
[HubMethodName("typing_stop")]
|
||||
public async Task TypingStop(string chatId)
|
||||
{
|
||||
await Clients.Group(chatId).SendAsync("user_stopped_typing", new { ChatId = chatId, UserId = _userContext.UserId });
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// WebRTC signaling
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task SendToUserAsync(string targetUserId, string method, object payload)
|
||||
{
|
||||
if (_userConnections.TryGetValue(targetUserId, out var connectionIds))
|
||||
{
|
||||
string[] ids;
|
||||
lock (connectionIds) { ids = connectionIds.ToArray(); }
|
||||
foreach (var connId in ids)
|
||||
{
|
||||
await Clients.Client(connId).SendAsync(method, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HubMethodName("call_offer")]
|
||||
public async Task CallOffer(CallOfferRequest request)
|
||||
{
|
||||
// Try to get caller info from current user's claims
|
||||
var displayName = Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User";
|
||||
var avatar = Context.User?.FindFirstValue("avatar");
|
||||
var username = Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name;
|
||||
|
||||
await SendToUserAsync(request.TargetUserId, "call_incoming", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
offer = request.Offer,
|
||||
callType = request.CallType,
|
||||
chatId = request.ChatId,
|
||||
callerInfo = new
|
||||
{
|
||||
id = _userContext.UserId.ToString(),
|
||||
displayName = displayName,
|
||||
avatar = avatar,
|
||||
username = username
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("call_answer")]
|
||||
public async Task CallAnswer(CallAnswerRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "call_answered", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
answer = request.Answer,
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("call_decline")]
|
||||
public async Task CallDecline(TargetUserRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "call_declined", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("call_end")]
|
||||
public async Task CallEnd(TargetUserRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "call_ended", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("ice_candidate")]
|
||||
public async Task IceCandidate(IceCandidateRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "ice_candidate", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
candidate = request.Candidate,
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("renegotiate")]
|
||||
public async Task Renegotiate(RenegotiateRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "renegotiate", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
offer = request.Offer,
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("renegotiate_answer")]
|
||||
public async Task RenegotiateAnswer(RenegotiateAnswerRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "renegotiate_answer", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
answer = request.Answer,
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("call_type_changed")]
|
||||
public async Task CallTypeChanged(CallTypeChangedRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "call_type_changed", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
callType = request.CallType,
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Records
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
public record SendMessageHubRequest(Guid ChatId, string? Content, string Type, string? MediaUrl, string? MediaType, string? FileName, long? FileSize);
|
||||
public record ReadMessagesRequest(Guid ChatId, List<string>? MessageIds);
|
||||
public record CallOfferRequest(string TargetUserId, object Offer, string CallType, string? ChatId);
|
||||
public record CallAnswerRequest(string TargetUserId, object Answer);
|
||||
public record TargetUserRequest(string TargetUserId);
|
||||
public record IceCandidateRequest(string TargetUserId, object Candidate);
|
||||
public record RenegotiateRequest(string TargetUserId, object Offer);
|
||||
public record RenegotiateAnswerRequest(string TargetUserId, object Answer);
|
||||
public record CallTypeChangedRequest(string TargetUserId, string CallType);
|
||||
}
|
||||
Reference in New Issue
Block a user