erp-platform/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs
2025-08-30 22:57:47 +03:00

512 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Classroom, Guid> _classSessionRepository;
private readonly IRepository<ClassroomParticipant, Guid> _participantRepository;
private readonly IRepository<ClassroomChat, Guid> _chatMessageRepository;
private readonly IRepository<ClassroomAttandance, Guid> _attendanceRepository;
private readonly ILogger<ClassroomHub> _logger;
private readonly IGuidGenerator _guidGenerator;
private readonly ICurrentUser _currentUser;
public ClassroomHub(
IRepository<Classroom, Guid> classSessionRepository,
IRepository<ClassroomParticipant, Guid> participantRepository,
IRepository<ClassroomChat, Guid> chatMessageRepository,
IRepository<ClassroomAttandance, Guid> attendanceRepository,
ILogger<ClassroomHub> 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<ClassroomAttandance> 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<ClassroomSettingsDto>(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);
// Frontende 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 eventi
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; }
}