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}") data = dict(request.data or {}) if 'metadata' in data: logger.warning("[LiveSession Create] 'metadata' provided by client will be ignored for security reasons.") data.pop('metadata', None) serializer = self.get_serializer(data=data) 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._build_metadata(subject) 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 _build_metadata(self, subject: str) -> dict: # Build secured, centralized metadata. Client overrides are NOT allowed. return { 'room_title': subject, 'default_lock_settings': { 'lock_microphone': True, 'lock_webcam': True, 'lock_screen_sharing': True, 'lock_whiteboard': False, 'lock_shared_notepad': False, 'lock_chat': False, 'lock_chat_send_message': False, 'lock_chat_file_share': False, 'lock_private_chat': False, }, 'room_features': { 'allow_webcams': True, 'mute_on_start': True, 'allow_screen_sharing': True, 'allow_recording': True, 'allow_rtmp': False, 'allow_view_other_webcams': True, 'allow_view_other_participants_list': True, 'admin_only_webcams': False, 'allow_polls': True, 'room_duration': 0, 'chat_features': { 'allow_chat': True, 'allow_file_upload': True, }, 'shared_note_pad_features': { 'allowed_shared_note_pad': True, }, 'whiteboard_features': { 'allowed_whiteboard': True, }, 'breakout_room_features': { 'is_allow': True, 'allowed_number_rooms': 6, }, 'waiting_room_features': { 'is_active': False, }, 'recording_features': { 'is_allow': True, 'is_allow_cloud': True, 'enable_auto_cloud_recording': False, }, }, } 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, 'lock_whiteboard': False, 'lock_shared_notepad': False, 'lock_chat': False, 'lock_chat_send_message': False, 'lock_chat_file_share': False, 'lock_private_chat': False, } 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