diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 4fa5e1c..e036cff 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -679,9 +679,19 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): client = PlugNMeetClient() response = client.is_room_active(session.room_id) + # Debug: Log full response to understand structure + logger.debug(f"[Room Sync] PlugNMeet response - room_id={session.room_id} response={response}") + # PlugNMeet returns: {"status": true, "msg": "...", "isActive": true/false} - is_active = response.get('isActive', False) + # Note: isActive might be boolean or string, handle both + is_active_raw = response.get('isActive', False) + is_active = is_active_raw if isinstance(is_active_raw, bool) else str(is_active_raw).lower() == 'true' response_msg = response.get('msg', 'unknown') + response_status = response.get('status', False) + + # Additional check: if status is true and msg says "active", trust that + if response_status and 'active' in response_msg.lower() and 'not' not in response_msg.lower(): + is_active = True if is_active: logger.debug(f"[Room Sync] ✓ Room verified active - room_id={session.room_id} session_id={session.id} msg={response_msg}") diff --git a/apps/course/views/live_session.py b/apps/course/views/live_session.py index 732a303..5dc5c46 100644 --- a/apps/course/views/live_session.py +++ b/apps/course/views/live_session.py @@ -300,20 +300,39 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): 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) - # CRITICAL: Verify the room is actually active in PlugNMeet before issuing token - # This prevents issuing tokens for rooms that have crashed or ended without webhook notification - room_id = session.room_id - if not self._verify_room_is_active(session): - logger.error(f"[LiveSession Token] Room not active in PlugNMeet - refusing token - room_id={room_id} session_id={session.id}") - raise AppAPIException({ - 'status': 'False', - 'message': 'room is not active. create room first', - 'msg': 'room is not active. create room first' - }, status_code=status.HTTP_400_BAD_REQUEST) - + # Check user role first to determine permissions 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}") + + # CRITICAL: Verify the room is actually active in PlugNMeet before issuing token + # This prevents issuing tokens for rooms that have crashed or ended without webhook notification + room_id = session.room_id + room_is_active = self._verify_room_is_active(session) + + if not room_is_active: + # Room is not active in PlugNMeet but we have a session record + if is_admin: + # For professors: Auto-recreate the room in PlugNMeet + logger.info(f"[LiveSession Token] Room inactive but professor requesting - recreating room - room_id={room_id} session_id={session.id}") + try: + self._recreate_room_in_plugnmeet(course, session) + logger.info(f"[LiveSession Token] Room recreated successfully - room_id={room_id}") + except Exception as e: + logger.error(f"[LiveSession Token] Failed to recreate room - room_id={room_id} error={str(e)}") + raise AppAPIException({ + 'status': 'False', + 'message': f'Failed to recreate room: {str(e)}', + 'msg': f'Failed to recreate room: {str(e)}' + }, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + # For students: Refuse token - they cannot create rooms + logger.error(f"[LiveSession Token] Room not active and user is student - refusing token - room_id={room_id} user_id={user.id}") + raise AppAPIException({ + 'status': 'False', + 'message': 'room is not active. Please wait for the professor to start the class.', + 'msg': 'room is not active. Please wait for the professor to start the class.' + }, status_code=status.HTTP_400_BAD_REQUEST) 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}") @@ -399,8 +418,18 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): client = PlugNMeetClient() response = client.is_room_active(session.room_id) - is_active = response.get('isActive', False) + # Debug: Log full response + logger.debug(f"[Room Verify] PlugNMeet response - room_id={session.room_id} response={response}") + + # Handle isActive as boolean or string + is_active_raw = response.get('isActive', False) + is_active = is_active_raw if isinstance(is_active_raw, bool) else str(is_active_raw).lower() == 'true' response_msg = response.get('msg', 'unknown') + response_status = response.get('status', False) + + # Trust status and msg if they indicate active room + if response_status and 'active' in response_msg.lower() and 'not' not in response_msg.lower(): + is_active = True if is_active: logger.debug(f"[Room Verify] ✓ Room is active - room_id={session.room_id} session_id={session.id}") @@ -431,6 +460,47 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): logger.error(f"[Room Verify] Unexpected error - room_id={session.room_id} error={type(e).__name__}: {str(e)}") return False + def _recreate_room_in_plugnmeet(self, course: Course, session: CourseLiveSession) -> None: + """ + Recreate a room in PlugNMeet when session exists but room is inactive. + + This happens when: + - Webhook failed to notify us of room closure + - PlugNMeet server restarted + - Room was manually ended + + Args: + course: The course for which to recreate the room + session: The existing session to reactivate + + Raises: + PlugNMeetError: If room creation fails + """ + subject = session.subject or f"{course.title} Live Session" + room_id = session.room_id + metadata = self._build_metadata(subject) + + payload = { + 'room_id': room_id, + 'metadata': metadata, + } + + logger.info(f"[Room Recreate] Recreating room in PlugNMeet - room_id={room_id} session_id={session.id}") + + try: + client = PlugNMeetClient() + plugnmeet_response = client.create_room(payload) + logger.info(f"[Room Recreate] Room recreated successfully - room_id={room_id} response={plugnmeet_response}") + + # Reset session ended_at to mark it as active again + session.ended_at = None + session.save(update_fields=['ended_at', 'updated_at']) + logger.info(f"[Room Recreate] Session reactivated - session_id={session.id}") + + except PlugNMeetError as exc: + logger.error(f"[Room Recreate] Failed to recreate room - room_id={room_id} error={str(exc)}") + raise + @staticmethod def _build_profile_url(request, user): avatar = getattr(user, 'avatar', None)