You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

231 lines
10 KiB

import logging
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.course.models import Course, CourseLiveSession, Participant
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from utils.exceptions import AppAPIException
logger = logging.getLogger(__name__)
class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = LiveSessionRoomCreateSerializer
def post(self, request, slug, *args, **kwargs):
logger.info(f"[LiveSession Create] Request from user_id={request.user.id} for course={slug}")
serializer = self.get_serializer(data=request.data or {})
serializer.is_valid(raise_exception=True)
course = get_object_or_404(Course, slug=slug)
if not request.user.can_manage_course(course):
logger.warning(f"[LiveSession Create] Permission denied - user_id={request.user.id} course={slug}")
raise AppAPIException({'message': 'You do not have permission to create a live session for this course.'}, status_code=status.HTTP_403_FORBIDDEN)
logger.info(f"[LiveSession Create] Permission granted for user_id={request.user.id} course={slug}")
subject = serializer.validated_data.get('subject') or f"{course.title} Live Session"
room_id = serializer.validated_data.get('room_id') or self._build_room_id(course)
metadata = self._merge_metadata(subject, serializer.validated_data.get('metadata') or {})
payload = {
'room_id': room_id,
'metadata': metadata,
}
logger.info(f"[LiveSession Create] Calling PlugNMeet API - room_id={room_id} course={slug}")
try:
client = PlugNMeetClient()
plugnmeet_response = client.create_room(payload)
logger.info(f"[LiveSession Create] PlugNMeet room created successfully - room_id={room_id}")
except ImproperlyConfigured as exc:
logger.error(f"[LiveSession Create] Configuration error - {str(exc)}")
raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
except PlugNMeetError as exc:
logger.error(f"[LiveSession Create] PlugNMeet API error - room_id={room_id} error={str(exc)}")
detail = exc.response_data or {'message': str(exc)}
status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY
raise AppAPIException(detail, status_code=status_code)
session, created = CourseLiveSession.objects.get_or_create(
course=course,
room_id=room_id,
defaults={
'subject': subject,
'started_at': timezone.now(),
},
)
if created:
logger.info(f"[LiveSession Create] New session created - session_id={session.id} room_id={room_id} course={slug}")
else:
logger.info(f"[LiveSession Create] Existing session reactivated - session_id={session.id} room_id={room_id} course={slug}")
updates = {}
if session.subject != subject:
session.subject = subject
updates['subject'] = subject
if session.room_id != room_id:
session.room_id = room_id
updates['room_id'] = room_id
if session.started_at is None:
session.started_at = timezone.now()
updates['started_at'] = session.started_at
if updates:
session.save(update_fields=list(updates.keys()))
logger.info(f"[LiveSession Create] Session updated - session_id={session.id} fields={list(updates.keys())}")
logger.info(f"[LiveSession Create] Success - session_id={session.id} room_id={room_id} course={slug} user_id={request.user.id}")
return Response({
'session': {
'id': session.id,
'room_id': session.room_id,
'subject': session.subject,
'started_at': session.started_at,
},
'plugnmeet': plugnmeet_response,
}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
@staticmethod
def _build_room_id(course: Course) -> str:
timestamp = timezone.now().strftime('%Y%m%d%H%M%S')
return f"{course.slug}-{timestamp}"
def _merge_metadata(self, subject: str, overrides: dict) -> dict:
base = {
'room_title': subject,
'default_lock_settings': {
'lock_microphone': True,
'lock_webcam': True,
'lock_screen_sharing': True,
},
'room_features': {
'mute_on_start': True,
'waiting_room_features': {
'is_active': False,
},
},
}
return self._deep_update(base, overrides)
def _deep_update(self, base: dict, overrides: dict) -> dict:
for key, value in overrides.items():
if isinstance(value, dict) and isinstance(base.get(key), dict):
base[key] = self._deep_update(base.get(key, {}), value)
else:
base[key] = value
return base
class CourseLiveSessionTokenAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = LiveSessionTokenSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
course_slug = serializer.validated_data['course_slug']
user = request.user
logger.info(f"[LiveSession Token] Request from user_id={user.id} for course={course_slug}")
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
logger.warning(f"[LiveSession Token] Course not found - course={course_slug} user_id={user.id}")
raise AppAPIException({'message': 'Course not found.'}, status_code=status.HTTP_404_NOT_FOUND)
if not course.is_online:
logger.warning(f"[LiveSession Token] Course not configured for online - course={course_slug} user_id={user.id}")
raise AppAPIException({'message': 'Course is not configured for online sessions.'}, status_code=status.HTTP_400_BAD_REQUEST)
try:
session = CourseLiveSession.objects.select_related('course').get(
course=course,
ended_at__isnull=True
)
logger.info(f"[LiveSession Token] Active session found - session_id={session.id} room_id={session.room_id} course={course_slug}")
except CourseLiveSession.DoesNotExist:
logger.warning(f"[LiveSession Token] No active session found - course={course_slug} user_id={user.id}")
raise AppAPIException({'message': 'No active live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND)
room_id = session.room_id
is_admin = user.can_manage_course(course)
user_role = "professor" if is_admin else "student"
logger.info(f"[LiveSession Token] User role determined - user_id={user.id} role={user_role} course={course_slug}")
if not is_admin and not Participant.objects.filter(course=course, student_id=user.id, is_active=True).exists():
logger.warning(f"[LiveSession Token] Access denied - user_id={user.id} not enrolled in course={course_slug}")
raise AppAPIException({'message': 'You do not have access to this live session.'}, status_code=status.HTTP_403_FORBIDDEN)
user_info = {
'user_id': str(user.id),
'name': user.get_full_name() or user.email or user.username or f"user-{user.id}",
'is_admin': is_admin,
}
user_metadata = {}
profile_pic = self._build_profile_url(request, user)
if profile_pic:
user_metadata['profilePic'] = profile_pic
if not is_admin:
user_metadata['lock_settings'] = {
'lock_microphone': True,
'lock_screen_sharing': True,
'lock_webcam': True,
}
else:
user_metadata['is_hidden'] = False
if user_metadata:
user_info['user_metadata'] = user_metadata
payload = {
'room_id': room_id,
'user_info': user_info,
}
logger.info(f"[LiveSession Token] Requesting token from PlugNMeet - room_id={room_id} user_id={user.id} role={user_role}")
try:
client = PlugNMeetClient()
plugnmeet_response = client.get_join_token(payload)
logger.info(f"[LiveSession Token] Token generated successfully - room_id={room_id} user_id={user.id}")
except ImproperlyConfigured as exc:
logger.error(f"[LiveSession Token] Configuration error - {str(exc)}")
raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
except PlugNMeetError as exc:
logger.error(f"[LiveSession Token] PlugNMeet API error - room_id={room_id} user_id={user.id} error={str(exc)}")
detail = exc.response_data or {'message': str(exc)}
status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY
raise AppAPIException(detail, status_code=status_code)
logger.info(f"[LiveSession Token] Success - room_id={room_id} user_id={user.id} role={user_role} course={course_slug}")
return Response({
'room_id': room_id,
'token': plugnmeet_response.get('token'),
'plugnmeet': plugnmeet_response,
})
@staticmethod
def _build_profile_url(request, user):
avatar = getattr(user, 'avatar', None)
if avatar and getattr(avatar, 'url', None):
return request.build_absolute_uri(avatar.url)
return None