|
|
|
@ -53,59 +53,94 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView): |
|
|
|
# 2. Setup ID and Metadata |
|
|
|
room_id = f"room-{course.id}-imamjavad" |
|
|
|
subject = f"{course.title} Live Session" |
|
|
|
metadata = self._build_metadata(subject) |
|
|
|
|
|
|
|
# 3. Use your CLEAN PlugNMeetClient |
|
|
|
try: |
|
|
|
client = PlugNMeetClient() # Loads keys from settings automatically |
|
|
|
|
|
|
|
# This uses the keys internally to talk to the server |
|
|
|
plugnmeet_response = client.create_room({ |
|
|
|
'room_id': room_id, |
|
|
|
'metadata': metadata, |
|
|
|
}) |
|
|
|
|
|
|
|
# 4. Generate the JOIN TOKEN (The Entry Ticket) |
|
|
|
# Users CANNOT enter without this. |
|
|
|
token_payload = { |
|
|
|
"room_id": room_id, |
|
|
|
"user_info": { |
|
|
|
"name": f"{request.user.first_name} {request.user.last_name}", |
|
|
|
"user_id": str(request.user.id), |
|
|
|
"is_admin": True, |
|
|
|
"is_hidden": False |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
# Check if your client has a method for this, otherwise use manual: |
|
|
|
# access_token = client.get_join_token(token_payload) |
|
|
|
# If not, we do it manually here using the SAME secret from settings: |
|
|
|
# 3. Database Logic - Check FIRST before calling PlugNMeet |
|
|
|
# Strategy: |
|
|
|
# 1. Try to find active session (ended_at is NULL) |
|
|
|
# 2. If not found, try to find ended session with same room_id and reactivate it |
|
|
|
# 3. If not found, create new session |
|
|
|
session = None |
|
|
|
needs_room_creation = False |
|
|
|
|
|
|
|
pnm_token = jwt.encode( |
|
|
|
{ |
|
|
|
"iss": settings.PLUGNMEET_API_KEY, |
|
|
|
"exp": int(time.time()) + 3600, |
|
|
|
"sub": str(request.user.id), |
|
|
|
**token_payload |
|
|
|
}, |
|
|
|
settings.PLUGNMEET_API_SECRET, |
|
|
|
algorithm="HS256" |
|
|
|
try: |
|
|
|
# Try to get active session first |
|
|
|
session = CourseLiveSession.objects.get( |
|
|
|
course=course, room_id=room_id, ended_at__isnull=True |
|
|
|
) |
|
|
|
needs_room_creation = False |
|
|
|
logger.info(f"[LiveSession Create] Found active session - session_id={session.id} room_id={room_id}") |
|
|
|
except CourseLiveSession.DoesNotExist: |
|
|
|
# No active session, check if there's an old one with same room_id |
|
|
|
try: |
|
|
|
session = CourseLiveSession.objects.get( |
|
|
|
course=course, room_id=room_id |
|
|
|
) |
|
|
|
# Reactivate the old session and mark for room recreation |
|
|
|
session.ended_at = None |
|
|
|
session.started_at = timezone.now() |
|
|
|
session.subject = subject |
|
|
|
session.save(update_fields=['ended_at', 'started_at', 'subject', 'updated_at']) |
|
|
|
needs_room_creation = True |
|
|
|
logger.info(f"[LiveSession Create] Reactivated ended session - session_id={session.id} room_id={room_id}") |
|
|
|
except CourseLiveSession.DoesNotExist: |
|
|
|
# No session exists at all, create new one and mark for room creation |
|
|
|
session = CourseLiveSession.objects.create( |
|
|
|
course=course, |
|
|
|
room_id=room_id, |
|
|
|
subject=subject, |
|
|
|
started_at=timezone.now() |
|
|
|
) |
|
|
|
needs_room_creation = True |
|
|
|
logger.info(f"[LiveSession Create] Created new session - session_id={session.id} room_id={room_id}") |
|
|
|
|
|
|
|
# 4. Create room in PlugNMeet ONLY if needed |
|
|
|
if needs_room_creation: |
|
|
|
metadata = self._build_metadata(subject) |
|
|
|
try: |
|
|
|
client = PlugNMeetClient() |
|
|
|
plugnmeet_response = client.create_room({ |
|
|
|
'room_id': room_id, |
|
|
|
'metadata': metadata, |
|
|
|
}) |
|
|
|
logger.info(f"[LiveSession Create] Room created in PlugNMeet - room_id={room_id}") |
|
|
|
except Exception as exc: |
|
|
|
logger.error(f"[LiveSession Create] PlugNMeet Error: {exc}") |
|
|
|
# If room creation fails, revert the session changes |
|
|
|
if session.ended_at is None: |
|
|
|
session.ended_at = timezone.now() |
|
|
|
session.save(update_fields=['ended_at', 'updated_at']) |
|
|
|
raise AppAPIException({'message': f'Failed to create room: {str(exc)}'}, status_code=500) |
|
|
|
else: |
|
|
|
logger.info(f"[LiveSession Create] Skipping room creation - room already exists - room_id={room_id}") |
|
|
|
|
|
|
|
# 5. Generate the JOIN TOKEN (The Entry Ticket) |
|
|
|
token_payload = { |
|
|
|
"room_id": room_id, |
|
|
|
"user_info": { |
|
|
|
"name": f"{request.user.first_name} {request.user.last_name}", |
|
|
|
"user_id": str(request.user.id), |
|
|
|
"is_admin": True, |
|
|
|
"is_hidden": False |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
except Exception as exc: |
|
|
|
logger.error(f"PlugNMeet Error: {exc}") |
|
|
|
raise AppAPIException({'message': str(exc)}, status_code=500) |
|
|
|
|
|
|
|
# 5. Database Logic |
|
|
|
session, created = CourseLiveSession.objects.get_or_create( |
|
|
|
course=course, room_id=room_id, |
|
|
|
defaults={'subject': subject, 'started_at': timezone.now()} |
|
|
|
pnm_token = jwt.encode( |
|
|
|
{ |
|
|
|
"iss": settings.PLUGNMEET_API_KEY, |
|
|
|
"exp": int(time.time()) + 3600, |
|
|
|
"sub": str(request.user.id), |
|
|
|
**token_payload |
|
|
|
}, |
|
|
|
settings.PLUGNMEET_API_SECRET, |
|
|
|
algorithm="HS256" |
|
|
|
) |
|
|
|
|
|
|
|
logger.info(f"[LiveSession Create] Success - session_id={session.id} room_id={room_id} user_id={request.user.id}") |
|
|
|
|
|
|
|
return Response({ |
|
|
|
'success': True, |
|
|
|
'session': {'id': session.id, 'room_id': session.room_id}, |
|
|
|
'access_token': pnm_token # <--- REQUIRED for frontend |
|
|
|
'access_token': pnm_token |
|
|
|
}, status=201) |
|
|
|
|
|
|
|
|
|
|
|
@ -295,17 +330,45 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): |
|
|
|
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}") |
|
|
|
logger.info(f"[LiveSession Token] Active session found in DB - 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 |
|
|
|
|
|
|
|
# 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}") |
|
|
|
raise AppAPIException({'message': 'You do not have access to this live session.'}, status_code=status.HTTP_403_FORBIDDEN) |
|
|
|
@ -368,6 +431,164 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): |
|
|
|
'plugnmeet': plugnmeet_response, |
|
|
|
}) |
|
|
|
|
|
|
|
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, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _verify_room_is_active(session: CourseLiveSession) -> bool: |
|
|
|
""" |
|
|
|
Verify that the room is actually active in PlugNMeet. |
|
|
|
|
|
|
|
Args: |
|
|
|
session: The CourseLiveSession to verify |
|
|
|
|
|
|
|
Returns: |
|
|
|
bool: True if room is active in PlugNMeet, False otherwise |
|
|
|
|
|
|
|
Side effects: |
|
|
|
- Closes session in database if PlugNMeet reports room is inactive |
|
|
|
""" |
|
|
|
if not session.room_id: |
|
|
|
logger.warning(f"[Room Verify] Session has no room_id - session_id={session.id}") |
|
|
|
return False |
|
|
|
|
|
|
|
try: |
|
|
|
client = PlugNMeetClient() |
|
|
|
response = client.is_room_active(session.room_id) |
|
|
|
|
|
|
|
# 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}") |
|
|
|
return True |
|
|
|
else: |
|
|
|
logger.warning(f"[Room Verify] ✗ Room is NOT active - room_id={session.room_id} session_id={session.id} msg={response_msg}") |
|
|
|
# Auto-close the session since room is not active |
|
|
|
now = timezone.now() |
|
|
|
session.ended_at = now |
|
|
|
session.save(update_fields=['ended_at', 'updated_at']) |
|
|
|
logger.info(f"[Room Verify] Session auto-closed - session_id={session.id} room_id={session.room_id}") |
|
|
|
return False |
|
|
|
|
|
|
|
except PlugNMeetError as e: |
|
|
|
error_msg = str(e) |
|
|
|
logger.error(f"[Room Verify] PlugNMeet API error - room_id={session.room_id} error={error_msg}") |
|
|
|
|
|
|
|
# If room not found, close the session |
|
|
|
if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): |
|
|
|
now = timezone.now() |
|
|
|
session.ended_at = now |
|
|
|
session.save(update_fields=['ended_at', 'updated_at']) |
|
|
|
logger.warning(f"[Room Verify] Room not found - session closed - session_id={session.id}") |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
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) |
|
|
|
|