From 839acf11f192fdcd1649fbca96d689ff6f7af462 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Tue, 14 Oct 2025 15:46:37 +0330 Subject: [PATCH] feat(course): sync live session status with PlugNMeet on validate Poll PlugNMeet for room activity during online class token validation and close inactive sessions, updating related LiveSessionUser entries. Add PlugNMeetClient.is_room_active and improve error handling to raise PlugNMeetError when response status is false. Includes logging and TODO for future webhook-based sync. --- apps/course/services/plugnmeet.py | 12 ++++++ apps/course/views/course.py | 63 ++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/apps/course/services/plugnmeet.py b/apps/course/services/plugnmeet.py index d5859d5..5a3180b 100644 --- a/apps/course/services/plugnmeet.py +++ b/apps/course/services/plugnmeet.py @@ -32,6 +32,9 @@ class PlugNMeetClient: def get_join_token(self, payload: Dict[str, Any]) -> Dict[str, Any]: return self._post("/auth/room/getJoinToken", payload) + def is_room_active(self, room_id: str) -> Dict[str, Any]: + return self._post("/auth/room/isRoomActive", {"roomId": room_id}) + def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: url = urljoin(f"{self.base_url}/", path.lstrip("/")) body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) @@ -57,6 +60,15 @@ class PlugNMeetClient: data = self._safe_json(response) if data is None: raise PlugNMeetError("PlugNMeet server returned an invalid response format.") + + if isinstance(data, dict) and data.get('status') is False: + error_message = data.get('msg') or data.get('message') or "PlugNMeet operation failed." + raise PlugNMeetError( + error_message, + status_code=response.status_code, + response_data=data, + ) + return data def _build_signature(self, body: str) -> str: diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 35dd16d..3e7f322 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -30,9 +30,11 @@ from apps.course.models import ( CourseCategory, CourseGlossary, CourseLiveSession, + LiveSessionUser, Participant, ) from apps.course.doc import * +from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.account.serializers import UserProfileSerializer from utils.exceptions import AppAPIException from utils.redis import OnlineClassTokenManager @@ -398,6 +400,10 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): course = get_object_or_404(queryset, slug=slug) user = request.user + # TODO: This room activity check should be replaced by webhook integration in the future + # Webhook will automatically notify when room becomes inactive, eliminating polling + self._sync_room_status_with_plugnmeet(course) + course_data = CourseDetailSerializer(course, context={'request': request}).data user_data = UserProfileSerializer(user, context={'request': request}).data metadata = self._build_metadata( @@ -558,4 +564,59 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): return value if timezone.is_naive(value): value = timezone.make_aware(value, timezone.get_current_timezone()) - return timezone.localtime(value).isoformat() \ No newline at end of file + return timezone.localtime(value).isoformat() + + def _sync_room_status_with_plugnmeet(self, course: Course): + """ + Check if active live session's room is still active in PlugNMeet. + If room is inactive, close the session and all related user entries. + + TODO: This should be replaced by webhook integration in the future. + PlugNMeet should send webhooks when rooms end, eliminating the need for polling. + """ + active_session = CourseLiveSession.objects.filter( + course=course, + ended_at__isnull=True + ).first() + + if not active_session or not active_session.room_id: + return + + try: + client = PlugNMeetClient() + response = client.is_room_active(active_session.room_id) + is_active = response.get('isActive', False) + + if not is_active: + logger.info(f"[Room Sync] Room inactive in PlugNMeet - room_id={active_session.room_id} session_id={active_session.id}") + self._close_live_session(active_session) + else: + logger.debug(f"[Room Sync] Room still active - room_id={active_session.room_id} session_id={active_session.id}") + + except (PlugNMeetError, Exception) as e: + logger.warning(f"[Room Sync] Failed to check room status - room_id={active_session.room_id} error={str(e)}") + + @staticmethod + def _close_live_session(session: CourseLiveSession): + """ + Close a live session and all related user entries. + Sets ended_at for session and exited_at/is_online for users. + """ + now = timezone.now() + + session.ended_at = now + session.save(update_fields=['ended_at', 'updated_at']) + logger.info(f"[Room Sync] Session closed - session_id={session.id} room_id={session.room_id} ended_at={now}") + + updated_count = LiveSessionUser.objects.filter( + session=session, + is_online=True, + exited_at__isnull=True + ).update( + is_online=False, + exited_at=now, + updated_at=now + ) + + if updated_count > 0: + logger.info(f"[Room Sync] User sessions closed - session_id={session.id} count={updated_count}") \ No newline at end of file