|
|
@ -9,6 +9,7 @@ from django.utils import timezone |
|
|
from drf_yasg import openapi |
|
|
from drf_yasg import openapi |
|
|
from drf_yasg.utils import swagger_auto_schema |
|
|
from drf_yasg.utils import swagger_auto_schema |
|
|
from rest_framework import status |
|
|
from rest_framework import status |
|
|
|
|
|
from rest_framework.authentication import TokenAuthentication |
|
|
from rest_framework.authtoken.models import Token |
|
|
from rest_framework.authtoken.models import Token |
|
|
from rest_framework.exceptions import NotFound |
|
|
from rest_framework.exceptions import NotFound |
|
|
from rest_framework.filters import SearchFilter |
|
|
from rest_framework.filters import SearchFilter |
|
|
@ -415,14 +416,12 @@ class CourseOnlineClassTokenAPIView(GenericAPIView): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
|
|
|
# Changed from AllowAny to enable DRF authentication |
|
|
|
|
|
# Users can still access without auth, but if token is provided, it will be authenticated |
|
|
|
|
|
authentication_classes = [TokenAuthentication] |
|
|
permission_classes = [AllowAny] |
|
|
permission_classes = [AllowAny] |
|
|
serializer_class = OnlineClassTokenVerifySerializer |
|
|
serializer_class = OnlineClassTokenVerifySerializer |
|
|
|
|
|
|
|
|
def get_permissions(self): |
|
|
|
|
|
if self.request.method == 'GET': |
|
|
|
|
|
return [IsAuthenticated()] |
|
|
|
|
|
return [AllowAny()] |
|
|
|
|
|
|
|
|
|
|
|
@swagger_auto_schema( |
|
|
@swagger_auto_schema( |
|
|
tags=['Imam-Javad - Course'], |
|
|
tags=['Imam-Javad - Course'], |
|
|
operation_description="Get course and user data for authenticated user.", |
|
|
operation_description="Get course and user data for authenticated user.", |
|
|
@ -453,11 +452,24 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
} |
|
|
} |
|
|
) |
|
|
) |
|
|
def get(self, request, slug, *args, **kwargs): |
|
|
def get(self, request, slug, *args, **kwargs): |
|
|
|
|
|
print("=" * 80) |
|
|
|
|
|
print(f"[Online Validate GET] REQUEST RECEIVED {request.data}") |
|
|
|
|
|
print(f"[Online Validate GET] slug={slug}") |
|
|
|
|
|
print(f"[Online Validate GET] user={request.user}") |
|
|
|
|
|
print(f"[Online Validate GET] user.is_authenticated={request.user.is_authenticated}") |
|
|
|
|
|
print(f"[Online Validate GET] user.id={request.user.id if request.user.is_authenticated else 'N/A'}") |
|
|
|
|
|
print("=" * 80) |
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[Online Validate GET] Request received - slug={slug} user_id={request.user.id if request.user.is_authenticated else 'anonymous'}") |
|
|
|
|
|
|
|
|
detail_view = CourseDetailAPIView() |
|
|
detail_view = CourseDetailAPIView() |
|
|
queryset = detail_view.get_queryset() |
|
|
queryset = detail_view.get_queryset() |
|
|
course = get_object_or_404(queryset, slug=slug) |
|
|
course = get_object_or_404(queryset, slug=slug) |
|
|
user = request.user |
|
|
user = request.user |
|
|
|
|
|
|
|
|
|
|
|
print(f"[Online Validate GET] Course found - course_id={course.id} slug={slug} is_online={course.is_online}") |
|
|
|
|
|
logger.info(f"[Online Validate GET] Course found - course_id={course.id} slug={slug} is_online={course.is_online}") |
|
|
|
|
|
|
|
|
# DEPRECATED: Polling approach replaced by webhook integration |
|
|
# DEPRECATED: Polling approach replaced by webhook integration |
|
|
# Room status is now updated automatically via PlugNMeet webhooks |
|
|
# Room status is now updated automatically via PlugNMeet webhooks |
|
|
# self._sync_room_status_with_plugnmeet(course) |
|
|
# self._sync_room_status_with_plugnmeet(course) |
|
|
@ -466,10 +478,13 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data |
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data |
|
|
metadata = self._build_metadata( |
|
|
metadata = self._build_metadata( |
|
|
course, |
|
|
course, |
|
|
{'user_id': user.id, 'extra': {}, 'generated_at': timezone.now().isoformat()}, |
|
|
|
|
|
|
|
|
{'user_id': user.id if user.is_authenticated else None, 'extra': {}, 'generated_at': timezone.now().isoformat()}, |
|
|
user=user, |
|
|
user=user, |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
print(f"[Online Validate GET] Success - metadata={metadata}") |
|
|
|
|
|
logger.info(f"[Online Validate GET] Success - user_id={user.id if user.is_authenticated else 'anonymous'} course={slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") |
|
|
|
|
|
|
|
|
return Response({ |
|
|
return Response({ |
|
|
'course': course_data, |
|
|
'course': course_data, |
|
|
'user': user_data, |
|
|
'user': user_data, |
|
|
@ -499,41 +514,56 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
} |
|
|
} |
|
|
) |
|
|
) |
|
|
def post(self, request, *args, **kwargs): |
|
|
def post(self, request, *args, **kwargs): |
|
|
logger.info(f"[Online Validate] Request received") |
|
|
|
|
|
|
|
|
print("=" * 80) |
|
|
|
|
|
print(f"[Online Validate POST] REQUEST RECEIVED") |
|
|
|
|
|
print(f"[Online Validate POST] request.data={request.data}") |
|
|
|
|
|
print(f"[Online Validate POST] has_token={'token' in request.data}") |
|
|
|
|
|
print("=" * 80) |
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"[Online Validate POST] Request received - has_token={'token' in request.data}") |
|
|
|
|
|
|
|
|
serializer = self.get_serializer(data=request.data) |
|
|
serializer = self.get_serializer(data=request.data) |
|
|
serializer.is_valid(raise_exception=True) |
|
|
serializer.is_valid(raise_exception=True) |
|
|
|
|
|
|
|
|
token_value = serializer.validated_data['token'] |
|
|
token_value = serializer.validated_data['token'] |
|
|
|
|
|
print(f"[Online Validate POST] Token extracted - token={token_value[:16]}...") |
|
|
|
|
|
logger.info(f"[Online Validate POST] Token extracted - token={token_value[:16]}...") |
|
|
|
|
|
|
|
|
manager = OnlineClassTokenManager() |
|
|
manager = OnlineClassTokenManager() |
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
payload = manager.get_payload(token_value) |
|
|
payload = manager.get_payload(token_value) |
|
|
logger.info(f"[Online Validate] Token decoded successfully") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Token decoded successfully - payload={payload}") |
|
|
|
|
|
logger.info(f"[Online Validate POST] Token decoded successfully - payload={payload}") |
|
|
except Exception as e: |
|
|
except Exception as e: |
|
|
logger.error(f"[Online Validate] Token decode failed - error={str(e)}") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Token decode FAILED - error={str(e)} type={type(e).__name__}") |
|
|
|
|
|
logger.error(f"[Online Validate POST] Token decode failed - error={str(e)} type={type(e).__name__}") |
|
|
raise |
|
|
raise |
|
|
|
|
|
|
|
|
course_id = payload.get('course_id') |
|
|
course_id = payload.get('course_id') |
|
|
user_id = payload.get('user_id') |
|
|
user_id = payload.get('user_id') |
|
|
if not course_id or not user_id: |
|
|
if not course_id or not user_id: |
|
|
logger.warning(f"[Online Validate] Invalid token payload - course_id={course_id} user_id={user_id}") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Invalid token payload - course_id={course_id} user_id={user_id}") |
|
|
|
|
|
logger.warning(f"[Online Validate POST] Invalid token payload - course_id={course_id} user_id={user_id}") |
|
|
raise AppAPIException({'message': 'Token payload is invalid.'}, status_code=status.HTTP_400_BAD_REQUEST) |
|
|
raise AppAPIException({'message': 'Token payload is invalid.'}, status_code=status.HTTP_400_BAD_REQUEST) |
|
|
|
|
|
|
|
|
logger.info(f"[Online Validate] Processing for user_id={user_id} course_id={course_id}") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Processing for user_id={user_id} course_id={course_id}") |
|
|
|
|
|
logger.info(f"[Online Validate POST] Processing for user_id={user_id} course_id={course_id}") |
|
|
|
|
|
|
|
|
detail_view = CourseDetailAPIView() |
|
|
detail_view = CourseDetailAPIView() |
|
|
queryset = detail_view.get_queryset() |
|
|
queryset = detail_view.get_queryset() |
|
|
course = get_object_or_404(queryset, pk=course_id) |
|
|
course = get_object_or_404(queryset, pk=course_id) |
|
|
user = get_object_or_404(UserModel.objects.all(), pk=user_id) |
|
|
user = get_object_or_404(UserModel.objects.all(), pk=user_id) |
|
|
|
|
|
|
|
|
logger.info(f"[Online Validate] Course found - slug={course.slug} is_online={course.is_online}") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Course found - slug={course.slug} is_online={course.is_online}") |
|
|
|
|
|
logger.info(f"[Online Validate POST] Course found - slug={course.slug} is_online={course.is_online}") |
|
|
|
|
|
|
|
|
course_data = CourseDetailSerializer(course, context={'request': request}).data |
|
|
course_data = CourseDetailSerializer(course, context={'request': request}).data |
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data |
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data |
|
|
metadata = self._build_metadata(course, payload, user=user) |
|
|
metadata = self._build_metadata(course, payload, user=user) |
|
|
|
|
|
|
|
|
logger.info(f"[Online Validate] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") |
|
|
|
|
|
|
|
|
print(f"[Online Validate POST] Success - metadata={metadata}") |
|
|
|
|
|
logger.info(f"[Online Validate POST] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") |
|
|
|
|
|
|
|
|
return Response({ |
|
|
return Response({ |
|
|
'course': course_data, |
|
|
'course': course_data, |
|
|
@ -572,6 +602,15 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
return metadata |
|
|
return metadata |
|
|
|
|
|
|
|
|
def _build_live_session_context(self, course: Course) -> dict: |
|
|
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 = ( |
|
|
latest_session = ( |
|
|
CourseLiveSession.objects.filter(course=course) |
|
|
CourseLiveSession.objects.filter(course=course) |
|
|
.order_by('-started_at', '-id') |
|
|
.order_by('-started_at', '-id') |
|
|
@ -579,6 +618,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
if not latest_session: |
|
|
if not latest_session: |
|
|
|
|
|
logger.debug(f"[Live Session Context] No session found for course={course.slug}") |
|
|
return { |
|
|
return { |
|
|
'is_online': False, |
|
|
'is_online': False, |
|
|
'live_session': None, |
|
|
'live_session': None, |
|
|
@ -590,6 +630,13 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
started_at = latest_session.started_at |
|
|
started_at = latest_session.started_at |
|
|
ended_at = latest_session.ended_at |
|
|
ended_at = latest_session.ended_at |
|
|
is_online = bool(started_at and not 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 = { |
|
|
live_session_data = { |
|
|
'id': latest_session.id, |
|
|
'id': latest_session.id, |
|
|
@ -599,13 +646,88 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
'ended_at': self._format_datetime(ended_at), |
|
|
'ended_at': self._format_datetime(ended_at), |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
|
context = { |
|
|
'is_online': is_online, |
|
|
'is_online': is_online, |
|
|
'live_session': live_session_data, |
|
|
'live_session': live_session_data, |
|
|
'active_room_id': live_session_data['room_id'] if is_online and live_session_data['room_id'] else None, |
|
|
'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_started_at': live_session_data['started_at'], |
|
|
'livesession_ended_at': live_session_data['ended_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) |
|
|
|
|
|
|
|
|
|
|
|
# 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} |
|
|
|
|
|
# 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}") |
|
|
|
|
|
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 |
|
|
@staticmethod |
|
|
def _user_can_join_live_session(user, course: Course) -> bool: |
|
|
def _user_can_join_live_session(user, course: Course) -> bool: |
|
|
@ -656,8 +778,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): |
|
|
# except (PlugNMeetError, Exception) as e: |
|
|
# except (PlugNMeetError, Exception) as e: |
|
|
# logger.warning(f"[Room Sync] Failed to check room status - room_id={active_session.room_id} error={str(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. |
|
|
Close a live session and all related user entries. |
|
|
Sets ended_at for session and exited_at/is_online for users. |
|
|
Sets ended_at for session and exited_at/is_online for users. |
|
|
|