using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Volo.Abp.Domain.Repositories; using Kurs.Platform.Entities; using Microsoft.Extensions.Logging; using Volo.Abp.Guids; using Volo.Abp.Users; namespace Kurs.Platform.SignalR.Hubs; [Authorize] public class ClassroomHub : Hub { private readonly IRepository _classSessionRepository; private readonly IRepository _participantRepository; private readonly IRepository _chatMessageRepository; private readonly ILogger _logger; private readonly IGuidGenerator _guidGenerator; private readonly ICurrentUser _currentUser; public ClassroomHub( IRepository classSessionRepository, IRepository participantRepository, IRepository chatMessageRepository, ILogger logger, IGuidGenerator guidGenerator, ICurrentUser currentUser) { _classSessionRepository = classSessionRepository; _participantRepository = participantRepository; _chatMessageRepository = chatMessageRepository; _logger = logger; _guidGenerator = guidGenerator; _currentUser = currentUser; } [HubMethodName("JoinClass")] public async Task JoinClassAsync(Guid sessionId, string userName) { var classSession = await _classSessionRepository.GetAsync(sessionId); if (!classSession.CanJoin()) { await Clients.Caller.SendAsync("Error", "Cannot join this class at this time"); return; } // Add to SignalR group await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); // Update participant connection var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == _currentUser.Id ); if (participant != null) { participant.UpdateConnectionId(Context.ConnectionId); await _participantRepository.UpdateAsync(participant); } // Notify others await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantJoined", _currentUser.Id, userName); _logger.LogInformation($"User {userName} joined class {sessionId}"); } [HubMethodName("LeaveClass")] public async Task LeaveClassAsync(Guid sessionId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString()); await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantLeft", _currentUser); _logger.LogInformation($"User {_currentUser} left class {sessionId}"); } public async Task SendSignalingMessageAsync(SignalingMessageDto message) { // Forward WebRTC signaling messages await Clients.User(message.ToUserId) .SendAsync("ReceiveSignalingMessage", message); _logger.LogInformation($"Signaling message sent from {message.FromUserId} to {message.ToUserId}"); } public async Task SendChatMessageAsync(Guid sessionId, string message) { var userName = _currentUser.UserName; var userId = _currentUser.Id; // Check if user is teacher var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == userId ); var isTeacher = participant?.IsTeacher ?? false; // Save message to database var chatMessage = new ClassChat( _guidGenerator.Create(), sessionId, userId, userName, message, isTeacher ); await _chatMessageRepository.InsertAsync(chatMessage); // Send to all participants await Clients.Group(sessionId.ToString()) .SendAsync("ChatMessage", new { Id = chatMessage.Id, SenderId = chatMessage.SenderId, SenderName = chatMessage.SenderName, Message = chatMessage.Message, Timestamp = chatMessage.Timestamp, IsTeacher = chatMessage.IsTeacher }); } public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted) { var teacherParticipant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == _currentUser.Id ); if (teacherParticipant?.IsTeacher != true) { await Clients.Caller.SendAsync("Error", "Only teachers can mute participants"); return; } var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == participantId ); if (participant != null) { if (isMuted) participant.MuteAudio(); else participant.UnmuteAudio(); await _participantRepository.UpdateAsync(participant); // Notify the participant and others await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantMuted", participantId, isMuted); } } public override async Task OnDisconnectedAsync(Exception exception) { // Handle cleanup when user disconnects var userId = _currentUser.Id; if (userId.HasValue) { var participants = await _participantRepository.GetListAsync( x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId ); foreach (var participant in participants) { await Clients.Group(participant.SessionId.ToString()) .SendAsync("ParticipantLeft", userId.Value); } } await base.OnDisconnectedAsync(exception); } } public class SignalingMessageDto { public string Type { get; set; } // offer, answer, ice-candidate public string FromUserId { get; set; } public string ToUserId { get; set; } public object Data { get; set; } }