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.authentication import TokenAuthentication from rest_framework.response import Response from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from datetime import time import jwt from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from utils.exceptions import AppAPIException logger = logging.getLogger(__name__) class CourseLiveSessionRoomCreateAPIView(GenericAPIView): permission_classes = [IsAuthenticated] authentication_classes = [TokenAuthentication] serializer_class = LiveSessionRoomCreateSerializer @swagger_auto_schema( operation_description="Create a live session room for a course", tags=["Imam-Javad - Course"], manual_parameters=[ openapi.Parameter( 'slug', openapi.IN_PATH, description="Course slug", type=openapi.TYPE_STRING, required=True ) ], responses={ 201: openapi.Response( description="Live session room created successfully" ) } ) def post(self, request, slug, *args, **kwargs): # 1. Validation and Permission Logic course = get_object_or_404(Course, slug=slug) if not request.user.can_manage_course(course): raise AppAPIException({'message': 'Permission denied'}, status_code=status.HTTP_403_FORBIDDEN) # 2. Build Safe Room ID (Prefix with 'room-' to satisfy NATS rules) # OLD: f"{course.id}-imamjavad" -> "9-imamjavad" (Risk of NATS errors) # NEW: f"room-{course.id}-imamjavad" -> "room-9-imamjavad" (Safe) room_id = f"room-{course.id}-imamjavad" subject = f"{course.title} Live Session" metadata = self._build_metadata(subject) # 3. Create Room via PlugNMeet API try: client = PlugNMeetClient() # Note: client.create_room only CREATES the room. # It does not give you a token to join it. plugnmeet_response = client.create_room({ 'room_id': room_id, 'metadata': metadata, }) except Exception as exc: logger.error(f"PlugNMeet Error: {exc}") raise AppAPIException({'message': str(exc)}, status_code=500) # 4. Save to Database session, created = CourseLiveSession.objects.get_or_create( course=course, room_id=room_id, defaults={'subject': subject, 'started_at': timezone.now()}, ) # 5. GENERATE JOIN TOKEN (Critical for User Access) # You must fetch these exactly as they are in your config.yaml PNM_API_KEY = "habibmeet_api_key_2024" PNM_SECRET = "habibmeet_secret_zumyyYWqv7KR2kUqvYdq4z4sXg7XTBD2ljT6_2024" token_payload = { "iss": PNM_API_KEY, # Issuer must be the API Key "exp": int(time.time()) + 3600, # 1 hour expiry "sub": str(request.user.id), "room_id": room_id, "name": f"{request.user.first_name} {request.user.last_name}", "is_admin": True, # Gives moderator privileges "user_id": str(request.user.id), } # SIGN with the SECRET, not the key token = jwt.encode(token_payload, PNM_SECRET, algorithm="HS256") return Response({ 'success': True, 'session': { 'id': session.id, 'room_id': session.room_id, 'started_at': session.started_at, }, # Frontend uses this to connect: # https://meet.newhorizonco.uk/?access_token={access_token} 'access_token': token }, status=status.HTTP_201_CREATED) # 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 = 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: return f"room-{course.id}-imamjavad" 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, 'is_allow_local': True, 'enable_auto_cloud_recording': False, 'only_record_admin_webcams': 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] authentication_classes = [TokenAuthentication] serializer_class = LiveSessionTokenSerializer @swagger_auto_schema( operation_description="Generate access token for live session", tags=["Imam-Javad - Course"], responses={ 200: openapi.Response( description="Live session token generated successfully" ) } ) 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) logger.info(f"[LiveSession Token] Profile pic URL - user_id={user.id} url={profile_pic}") 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 class CourseLiveSessionRecordedFileAPIView(GenericAPIView): permission_classes = [IsAuthenticated] authentication_classes = [TokenAuthentication] serializer_class = LiveSessionRecordingSerializer @swagger_auto_schema( operation_description="Update recorded file for live session", tags=["Imam-Javad - Course"], manual_parameters=[ openapi.Parameter( 'course_id', openapi.IN_PATH, description="Course ID", type=openapi.TYPE_INTEGER, required=True ) ], responses={ 200: openapi.Response( description="Recorded file updated successfully" ) } ) def patch(self, request, course_id, *args, **kwargs): logger.info(f"[LiveSession Recorded File] Request from user_id={request.user.id} for course_id={course_id}") course = get_object_or_404(Course, id=course_id) if not request.user.can_manage_course(course): logger.warning(f"[LiveSession Recorded File] Permission denied - user_id={request.user.id} course_id={course_id}") raise AppAPIException({'message': 'You do not have permission to update this course.'}, status_code=status.HTTP_403_FORBIDDEN) try: session = course.live_sessions.latest('-started_at') except CourseLiveSession.DoesNotExist: logger.warning(f"[LiveSession Recorded File] No active session found - course_id={course_id} user_id={request.user.id}") raise AppAPIException({'message': 'No live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND) logger.info(f"[LiveSession Recorded File] Latest session found - session_id={session.id} course_id={course_id}") serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) recording = LiveSessionRecording.objects.create( session=session, file=serializer.validated_data['file'], title=serializer.validated_data.get('title') or f"{session.subject} Recording", recording_type=serializer.validated_data.get('recording_type', 'video'), file_time=serializer.validated_data.get('file_time'), ) logger.info(f"[LiveSession Recorded File] Recording created successfully - recording_id={recording.id} session_id={session.id} user_id={request.user.id}") return Response({ 'id': recording.id, 'session_id': session.id, 'title': recording.title, 'file': request.build_absolute_uri(recording.file.url), 'recording_type': recording.recording_type, 'file_time': recording.file_time, 'is_active': recording.is_active, }, status=status.HTTP_201_CREATED)