|
|
|
@ -602,6 +602,15 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
return metadata |
|
|
|
|
|
|
|
def _build_live_session_context(self, course: Course) -> dict: |
|
|
|
""" |
|
|
|
Build live session context with real-time PlugNMeet verification. |
|
|
|
|
|
|
|
This method: |
|
|
|
1. Finds the latest session for the course |
|
|
|
2. Verifies with PlugNMeet if the room is actually active |
|
|
|
3. Auto-closes sessions if PlugNMeet reports room is inactive |
|
|
|
4. Returns accurate session state independent of webhook delays |
|
|
|
""" |
|
|
|
latest_session = ( |
|
|
|
CourseLiveSession.objects.filter(course=course) |
|
|
|
.order_by('-started_at', '-id') |
|
|
|
@ -609,6 +618,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
) |
|
|
|
|
|
|
|
if not latest_session: |
|
|
|
logger.debug(f"[Live Session Context] No session found for course={course.slug}") |
|
|
|
return { |
|
|
|
'is_online': False, |
|
|
|
'live_session': None, |
|
|
|
@ -620,6 +630,13 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
started_at = latest_session.started_at |
|
|
|
ended_at = latest_session.ended_at |
|
|
|
is_online = bool(started_at and not ended_at) |
|
|
|
|
|
|
|
# CRITICAL: Verify room status with PlugNMeet if session appears online |
|
|
|
# This ensures we don't rely solely on webhooks which may fail or be delayed |
|
|
|
if is_online and latest_session.room_id: |
|
|
|
is_online = self._verify_and_sync_room_status(latest_session) |
|
|
|
# Refresh ended_at in case session was closed |
|
|
|
ended_at = latest_session.ended_at |
|
|
|
|
|
|
|
live_session_data = { |
|
|
|
'id': latest_session.id, |
|
|
|
@ -629,13 +646,78 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
'ended_at': self._format_datetime(ended_at), |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
context = { |
|
|
|
'is_online': is_online, |
|
|
|
'live_session': live_session_data, |
|
|
|
'active_room_id': live_session_data['room_id'] if is_online and live_session_data['room_id'] else None, |
|
|
|
'livesession_started_at': live_session_data['started_at'], |
|
|
|
'livesession_ended_at': live_session_data['ended_at'], |
|
|
|
} |
|
|
|
|
|
|
|
logger.debug(f"[Live Session Context] course={course.slug} is_online={is_online} room_id={live_session_data['room_id']}") |
|
|
|
return context |
|
|
|
|
|
|
|
def _verify_and_sync_room_status(self, session: CourseLiveSession) -> bool: |
|
|
|
""" |
|
|
|
Verify room status with PlugNMeet and sync local database. |
|
|
|
|
|
|
|
Args: |
|
|
|
session: The CourseLiveSession to verify |
|
|
|
|
|
|
|
Returns: |
|
|
|
bool: True if room is active, False if inactive or verification failed |
|
|
|
|
|
|
|
Side effects: |
|
|
|
- Closes session in database if PlugNMeet reports room is inactive |
|
|
|
- Updates LiveSessionUser records accordingly |
|
|
|
""" |
|
|
|
if not session.room_id: |
|
|
|
logger.warning(f"[Room Sync] Session has no room_id - session_id={session.id}") |
|
|
|
return False |
|
|
|
|
|
|
|
try: |
|
|
|
client = PlugNMeetClient() |
|
|
|
response = client.is_room_active(session.room_id) |
|
|
|
|
|
|
|
# PlugNMeet returns: {"status": true, "msg": "...", "isActive": true/false} |
|
|
|
is_active = response.get('isActive', False) |
|
|
|
response_msg = response.get('msg', 'unknown') |
|
|
|
|
|
|
|
if is_active: |
|
|
|
logger.debug(f"[Room Sync] ✓ Room verified active - room_id={session.room_id} session_id={session.id} msg={response_msg}") |
|
|
|
return True |
|
|
|
else: |
|
|
|
# Room is not active in PlugNMeet but active in our database |
|
|
|
# This happens when: |
|
|
|
# 1. Webhook failed to fire |
|
|
|
# 2. Room was ended externally |
|
|
|
# 3. Room crashed or timed out |
|
|
|
logger.warning(f"[Room Sync] ✗ Room inactive in PlugNMeet - auto-closing session_id={session.id} room_id={session.room_id} msg={response_msg}") |
|
|
|
self._close_live_session(session) |
|
|
|
return False |
|
|
|
|
|
|
|
except PlugNMeetError as e: |
|
|
|
# PlugNMeet API returned an error |
|
|
|
error_msg = str(e) |
|
|
|
logger.error(f"[Room Sync] PlugNMeet API error - room_id={session.room_id} session_id={session.id} error={error_msg}") |
|
|
|
|
|
|
|
# Check if error message indicates room doesn't exist |
|
|
|
if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): |
|
|
|
logger.warning(f"[Room Sync] Room not found in PlugNMeet - closing session_id={session.id}") |
|
|
|
self._close_live_session(session) |
|
|
|
return False |
|
|
|
|
|
|
|
# For other API errors, assume room might still be active (fail-safe) |
|
|
|
logger.warning(f"[Room Sync] Cannot verify room status, assuming inactive for safety - room_id={session.room_id}") |
|
|
|
return False |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
# Network error or unexpected exception |
|
|
|
logger.error(f"[Room Sync] Unexpected error verifying room - room_id={session.room_id} session_id={session.id} error={type(e).__name__}: {str(e)}") |
|
|
|
# For network errors, fail-safe: assume room might still be active |
|
|
|
# but log a warning for monitoring |
|
|
|
logger.warning(f"[Room Sync] Network/system error, assuming room inactive for safety") |
|
|
|
return False |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _user_can_join_live_session(user, course: Course) -> bool: |
|
|
|
@ -686,8 +768,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
# 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): |
|
|
|
def _close_live_session(self, session: CourseLiveSession): |
|
|
|
""" |
|
|
|
Close a live session and all related user entries. |
|
|
|
Sets ended_at for session and exited_at/is_online for users. |
|
|
|
|