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; using System.Linq; using System.Text.Json; namespace Kurs.Platform.Classrooms; [Authorize] public class ClassroomHub : Hub { private readonly IRepository _classSessionRepository; private readonly IRepository _participantRepository; private readonly IRepository _chatMessageRepository; private readonly IRepository _attendanceRepository; private readonly ILogger _logger; private readonly IGuidGenerator _guidGenerator; private readonly ICurrentUser _currentUser; public ClassroomHub( IRepository classSessionRepository, IRepository participantRepository, IRepository chatMessageRepository, IRepository attendanceRepository, ILogger logger, IGuidGenerator guidGenerator, ICurrentUser currentUser) { _classSessionRepository = classSessionRepository; _participantRepository = participantRepository; _chatMessageRepository = chatMessageRepository; _attendanceRepository = attendanceRepository; _logger = logger; _guidGenerator = guidGenerator; _currentUser = currentUser; } #region Helper Methods private async Task CreateAttendanceAsync(Guid sessionId, Guid userId, string userName) { var attendance = new ClassroomAttandance( _guidGenerator.Create(), sessionId, userId, userName, DateTime.UtcNow ); await _attendanceRepository.InsertAsync(attendance, autoSave: true); return attendance; } private async Task CloseAttendanceAsync(ClassroomAttandance attendance) { attendance.LeaveTime = DateTime.UtcNow; attendance.TotalDurationMinutes = (int)Math.Max( 1, (attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes ); await _attendanceRepository.UpdateAsync(attendance, autoSave: true); } private async Task UpdateParticipantConnectionAsync(ClassroomParticipant participant, string connectionId, bool isActive) { participant.UpdateConnectionId(connectionId); participant.IsActive = isActive; await _participantRepository.UpdateAsync(participant, autoSave: true); } private async Task DeactivateParticipantAsync(ClassroomParticipant participant) { participant.IsActive = false; participant.ConnectionId = null; await _participantRepository.UpdateAsync(participant, autoSave: true); } private async Task UpdateParticipantCountAsync(Guid sessionId, Classroom classroom) { var participantCount = await _participantRepository.CountAsync(x => x.SessionId == sessionId); classroom.ParticipantCount = participantCount; await _classSessionRepository.UpdateAsync(classroom, autoSave: true); } private async Task BroadcastChatMessageAsync(ClassroomChat chatMessage, Guid sessionId) { await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true); await Clients.Group(sessionId.ToString()).SendAsync("ChatMessage", new { Id = chatMessage.Id, chatMessage.SenderId, chatMessage.SenderName, chatMessage.Message, chatMessage.Timestamp, chatMessage.IsTeacher, chatMessage.MessageType, chatMessage.RecipientId, chatMessage.RecipientName }); } private async Task SetMuteStateAsync(ClassroomParticipant participant, bool isMuted) { if (isMuted) participant.MuteAudio(); else participant.UnmuteAudio(); await _participantRepository.UpdateAsync(participant, autoSave: true); } #endregion [HubMethodName("JoinClass")] public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive) { var classroom = await _classSessionRepository.GetAsync(sessionId); if (classroom == null) { await Clients.Caller.SendAsync("Error", "Classroom not found"); return; } var classroomSettings = string.IsNullOrWhiteSpace(classroom.SettingsJson) ? new ClassroomSettingsDto() : JsonSerializer.Deserialize(classroom.SettingsJson); var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == userId ); if (participant == null) { participant = new ClassroomParticipant( _guidGenerator.Create(), sessionId, userId, userName, isTeacher, classroomSettings.DefaultMicrophoneState == "muted", classroomSettings.DefaultCameraState == "off", false, isActive ); await _participantRepository.InsertAsync(participant, autoSave: true); await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive); await UpdateParticipantCountAsync(sessionId, classroom); } else { await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive); } await CreateAttendanceAsync(sessionId, userId, userName); await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); var existingParticipants = await _participantRepository.GetListAsync( x => x.SessionId == sessionId && x.IsActive ); var others = existingParticipants .Where(x => x.ConnectionId != Context.ConnectionId) .Select(x => new { UserId = x.UserId, UserName = x.UserName, IsTeacher = x.IsTeacher, IsActive = x.IsActive }) .ToList(); await Clients.Caller.SendAsync("ExistingParticipants", others); await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantJoined", userId, userName, isTeacher, isActive); } [HubMethodName("LeaveClass")] public async Task LeaveClassAsync(Guid sessionId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString()); var userId = _currentUser.Id; if (!userId.HasValue) return; var attendance = await _attendanceRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.StudentId == userId.Value && x.LeaveTime == null ); if (attendance != null) { await CloseAttendanceAsync(attendance); await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); } var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == userId ); if (participant != null) { await DeactivateParticipantAsync(participant); } await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantLeft", userId.Value); _logger.LogInformation("User {UserId} left class {SessionId}", userId, sessionId); } [HubMethodName("MuteParticipant")] public async Task MuteParticipantAsync(Guid sessionId, Guid userId, bool isMuted, bool isTeacher) { 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 == userId ); if (participant != null) { await SetMuteStateAsync(participant, isMuted); await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantMuted", userId, isMuted); } } [HubMethodName("SendChatMessage")] public async Task SendChatMessageAsync(Guid sessionId, Guid senderId, string senderName, string message, bool isTeacher, string messageType) { var chatMessage = new ClassroomChat( _guidGenerator.Create(), sessionId, senderId, senderName, message, null, null, isTeacher, messageType ); await BroadcastChatMessageAsync(chatMessage, sessionId); } [HubMethodName("SendPrivateMessage")] public async Task SendPrivateMessageAsync( Guid sessionId, Guid senderId, string senderName, string message, Guid recipientId, string recipientName, bool isTeacher, string messageType) { var chatMessage = new ClassroomChat( _guidGenerator.Create(), sessionId, senderId, senderName, message, recipientId, recipientName, isTeacher, "private" ); await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true); await Clients.User(recipientId.ToString()).SendAsync("ChatMessage", new { Id = Guid.NewGuid(), SenderId = senderId, SenderName = senderName, Message = message, Timestamp = DateTime.UtcNow, IsTeacher = isTeacher, RecipientId = recipientId, RecipientName = recipientName, MessageType = "private" }); await Clients.Caller.SendAsync("ChatMessage", new { Id = Guid.NewGuid(), SenderId = senderId, SenderName = senderName, Message = message, Timestamp = DateTime.UtcNow, IsTeacher = isTeacher, RecipientId = recipientId, RecipientName = recipientName, MessageType = "private" }); } [HubMethodName("SendAnnouncement")] public async Task SendAnnouncementAsync(Guid sessionId, Guid senderId, string senderName, string message, bool isTeacher) { var chatMessage = new ClassroomChat( _guidGenerator.Create(), sessionId, senderId, senderName, message, null, null, isTeacher, "announcement" ); await BroadcastChatMessageAsync(chatMessage, sessionId); } [HubMethodName("RaiseHand")] public async Task RaiseHandAsync(Guid sessionId, Guid studentId, string studentName) { // 🔑 Participant'ı bul var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == studentId ); if (participant != null) { participant.IsHandRaised = true; await _participantRepository.UpdateAsync(participant, autoSave: true); } await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseReceived", new { Id = Guid.NewGuid(), StudentId = studentId, StudentName = studentName, Timestamp = DateTime.UtcNow, IsActive = true }); } [HubMethodName("KickParticipant")] public async Task KickParticipantAsync(Guid sessionId, Guid participantId) { var attendance = await _attendanceRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null ); if (attendance != null) { await CloseAttendanceAsync(attendance); await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); } var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == participantId ); if (participant != null) { await DeactivateParticipantAsync(participant); } _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId); } [HubMethodName("ApproveHandRaise")] public async Task ApproveHandRaiseAsync(Guid sessionId, Guid studentId) { // 🔑 Öğrencinin parmak kaldırma durumunu sıfırla var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == studentId ); if (participant != null) { participant.IsHandRaised = false; await _participantRepository.UpdateAsync(participant, autoSave: true); } await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", new { studentId }); } [HubMethodName("DismissHandRaise")] public async Task DismissHandRaiseAsync(Guid sessionId, Guid studentId) { // 🔑 Participant'ı bul ve elini indir var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == studentId ); if (participant != null) { participant.IsHandRaised = false; await _participantRepository.UpdateAsync(participant, autoSave: true); } await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", new { studentId }); } [HubMethodName("SendOffer")] public async Task SendOfferAsync(Guid sessionId, Guid targetUserId, object offer) { _logger.LogInformation("➡️ SendOffer to {TargetUserId}, from {CurrentUser}", targetUserId, _currentUser.Id); await Clients.User(targetUserId.ToString()) .SendAsync("ReceiveOffer", _currentUser.Id?.ToString(), offer); } [HubMethodName("SendAnswer")] public async Task SendAnswerAsync(Guid sessionId, Guid targetUserId, object answer) { await Clients.User(targetUserId.ToString()) .SendAsync("ReceiveAnswer", _currentUser.Id?.ToString(), answer); } [HubMethodName("SendIceCandidate")] public async Task SendIceCandidateAsync(Guid sessionId, Guid targetUserId, object candidate) { await Clients.User(targetUserId.ToString()) .SendAsync("ReceiveIceCandidate", _currentUser.Id?.ToString(), candidate); } public override async Task OnDisconnectedAsync(Exception exception) { try { if (Context.ConnectionAborted.IsCancellationRequested) return; var userId = _currentUser.Id; if (userId.HasValue) { // 🔑 1. Katılımcı listesi var participants = await _participantRepository .GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId); foreach (var participant in participants) { // 🔑 2. Attendance kaydını kapat var attendance = await _attendanceRepository.FirstOrDefaultAsync( x => x.SessionId == participant.SessionId && x.StudentId == userId.Value && x.LeaveTime == null ); if (attendance != null) { attendance.LeaveTime = DateTime.UtcNow; attendance.TotalDurationMinutes = (int)Math.Max( 1, (attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes ); await _attendanceRepository.UpdateAsync(attendance, autoSave: true); // Frontend’e bildir await Clients.Group(participant.SessionId.ToString()) .SendAsync("AttendanceUpdated", new { attendance.Id, attendance.SessionId, attendance.StudentId, attendance.StudentName, attendance.JoinTime, attendance.LeaveTime, attendance.TotalDurationMinutes }); } participant.IsActive = false; participant.ConnectionId = null; await _participantRepository.UpdateAsync(participant, autoSave: true); // 🔑 3. ParticipantLeft event’i await Clients.Group(participant.SessionId.ToString()) .SendAsync("ParticipantLeft", userId.Value); } } } catch (TaskCanceledException) { _logger.LogDebug("OnDisconnectedAsync iptal edildi (connection aborted)."); } catch (Exception ex) { _logger.LogError(ex, "OnDisconnectedAsync hata"); } 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; } }