You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
439 lines
19 KiB
439 lines
19 KiB
import logging
|
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone
|
|
|
|
from rest_framework import status
|
|
from rest_framework.generics import GenericAPIView
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from rest_framework.response import Response
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from drf_yasg import openapi
|
|
import time
|
|
import jwt
|
|
from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording
|
|
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer
|
|
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
|
|
from utils.exceptions import AppAPIException
|
|
from django.conf import settings
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = LiveSessionRoomCreateSerializer
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Create a live session room for a course",
|
|
tags=["Imam-Javad - Course"],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'slug', openapi.IN_PATH,
|
|
description="Course slug",
|
|
type=openapi.TYPE_STRING,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
201: openapi.Response(
|
|
description="Live session room created successfully"
|
|
)
|
|
}
|
|
)
|
|
|
|
def post(self, request, slug, *args, **kwargs):
|
|
# 1. Standard Permissions Logic
|
|
course = get_object_or_404(Course, slug=slug)
|
|
if not request.user.can_manage_course(course):
|
|
raise AppAPIException({'message': 'Permission denied'}, status_code=403)
|
|
|
|
# 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:
|
|
|
|
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"
|
|
)
|
|
|
|
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()}
|
|
)
|
|
|
|
return Response({
|
|
'success': True,
|
|
'session': {'id': session.id, 'room_id': session.room_id},
|
|
'access_token': pnm_token # <--- REQUIRED for frontend
|
|
}, status=201)
|
|
|
|
|
|
# def post(self, request, slug, *args, **kwargs):
|
|
# logger.info(f"[LiveSession Create] Request from user_id={request.user.id} for course={slug}")
|
|
|
|
# data = dict(request.data or {})
|
|
# if 'metadata' in data:
|
|
# logger.warning("[LiveSession Create] 'metadata' provided by client will be ignored for security reasons.")
|
|
# data.pop('metadata', None)
|
|
|
|
# serializer = self.get_serializer(data=data)
|
|
# serializer.is_valid(raise_exception=True)
|
|
|
|
# course = get_object_or_404(Course, slug=slug)
|
|
|
|
# if not request.user.can_manage_course(course):
|
|
# logger.warning(f"[LiveSession Create] Permission denied - user_id={request.user.id} course={slug}")
|
|
# raise AppAPIException({'message': 'You do not have permission to create a live session for this course.'}, status_code=status.HTTP_403_FORBIDDEN)
|
|
|
|
# logger.info(f"[LiveSession Create] Permission granted for user_id={request.user.id} course={slug}")
|
|
|
|
# subject = serializer.validated_data.get('subject') or f"{course.title} Live Session"
|
|
# room_id = self._build_room_id(course)
|
|
# metadata = self._build_metadata(subject)
|
|
|
|
# payload = {
|
|
# 'room_id': room_id,
|
|
# 'metadata': metadata,
|
|
# }
|
|
|
|
# logger.info(f"[LiveSession Create] Calling PlugNMeet API - room_id={room_id} course={slug}")
|
|
|
|
# try:
|
|
# client = PlugNMeetClient()
|
|
# plugnmeet_response = client.create_room(payload)
|
|
# logger.info(f"[LiveSession Create] PlugNMeet room created successfully - room_id={room_id}")
|
|
# except ImproperlyConfigured as exc:
|
|
# logger.error(f"[LiveSession Create] Configuration error - {str(exc)}")
|
|
# raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
# except PlugNMeetError as exc:
|
|
# logger.error(f"[LiveSession Create] PlugNMeet API error - room_id={room_id} error={str(exc)}")
|
|
# detail = exc.response_data or {'message': str(exc)}
|
|
# status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY
|
|
# raise AppAPIException(detail, status_code=status_code)
|
|
|
|
# session, created = CourseLiveSession.objects.get_or_create(
|
|
# course=course,
|
|
# room_id=room_id,
|
|
# defaults={
|
|
# 'subject': subject,
|
|
# 'started_at': timezone.now(),
|
|
# },
|
|
# )
|
|
|
|
# if created:
|
|
# logger.info(f"[LiveSession Create] New session created - session_id={session.id} room_id={room_id} course={slug}")
|
|
# else:
|
|
# logger.info(f"[LiveSession Create] Existing session reactivated - session_id={session.id} room_id={room_id} course={slug}")
|
|
# updates = {}
|
|
# if session.subject != subject:
|
|
# session.subject = subject
|
|
# updates['subject'] = subject
|
|
# if session.room_id != room_id:
|
|
# session.room_id = room_id
|
|
# updates['room_id'] = room_id
|
|
# if session.started_at is None:
|
|
# session.started_at = timezone.now()
|
|
# updates['started_at'] = session.started_at
|
|
# if updates:
|
|
# session.save(update_fields=list(updates.keys()))
|
|
# logger.info(f"[LiveSession Create] Session updated - session_id={session.id} fields={list(updates.keys())}")
|
|
|
|
# logger.info(f"[LiveSession Create] Success - session_id={session.id} room_id={room_id} course={slug} user_id={request.user.id}")
|
|
|
|
# return Response({
|
|
# 'session': {
|
|
# 'id': session.id,
|
|
# 'room_id': session.room_id,
|
|
# 'subject': session.subject,
|
|
# 'started_at': session.started_at,
|
|
# },
|
|
# 'plugnmeet': plugnmeet_response,
|
|
# }, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
|
|
|
|
@staticmethod
|
|
def _build_room_id(course: Course) -> str:
|
|
return f"room-{course.id}-imamjavad"
|
|
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
|
|
def _deep_update(self, base: dict, overrides: dict) -> dict:
|
|
for key, value in overrides.items():
|
|
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
base[key] = self._deep_update(base.get(key, {}), value)
|
|
else:
|
|
base[key] = value
|
|
return base
|
|
|
|
|
|
class CourseLiveSessionTokenAPIView(GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = LiveSessionTokenSerializer
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Generate access token for live session",
|
|
tags=["Imam-Javad - Course"],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Live session token generated successfully"
|
|
)
|
|
}
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
course_slug = serializer.validated_data['course_slug']
|
|
user = request.user
|
|
|
|
logger.info(f"[LiveSession Token] Request from user_id={user.id} for course={course_slug}")
|
|
|
|
try:
|
|
course = Course.objects.get(slug=course_slug)
|
|
except Course.DoesNotExist:
|
|
logger.warning(f"[LiveSession Token] Course not found - course={course_slug} user_id={user.id}")
|
|
raise AppAPIException({'message': 'Course not found.'}, status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
if not course.is_online:
|
|
logger.warning(f"[LiveSession Token] Course not configured for online - course={course_slug} user_id={user.id}")
|
|
raise AppAPIException({'message': 'Course is not configured for online sessions.'}, status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
session = CourseLiveSession.objects.select_related('course').get(
|
|
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}")
|
|
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
|
|
|
|
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}")
|
|
|
|
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)
|
|
|
|
user_info = {
|
|
'user_id': str(user.id),
|
|
'name': user.get_full_name() or user.email or user.username or f"user-{user.id}",
|
|
'is_admin': is_admin,
|
|
}
|
|
|
|
user_metadata = {}
|
|
profile_pic = self._build_profile_url(request, user)
|
|
logger.info(f"[LiveSession Token] Profile pic URL - user_id={user.id} url={profile_pic}")
|
|
if profile_pic:
|
|
user_metadata['profilePic'] = profile_pic
|
|
|
|
if not is_admin:
|
|
user_metadata['lock_settings'] = {
|
|
'lock_microphone': True,
|
|
'lock_screen_sharing': True,
|
|
'lock_webcam': 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,
|
|
}
|
|
else:
|
|
user_metadata['is_hidden'] = False
|
|
|
|
if user_metadata:
|
|
user_info['user_metadata'] = user_metadata
|
|
|
|
payload = {
|
|
'room_id': room_id,
|
|
'user_info': user_info,
|
|
}
|
|
|
|
logger.info(f"[LiveSession Token] Requesting token from PlugNMeet - room_id={room_id} user_id={user.id} role={user_role}")
|
|
|
|
try:
|
|
client = PlugNMeetClient()
|
|
plugnmeet_response = client.get_join_token(payload)
|
|
logger.info(f"[LiveSession Token] Token generated successfully - room_id={room_id} user_id={user.id}")
|
|
except ImproperlyConfigured as exc:
|
|
logger.error(f"[LiveSession Token] Configuration error - {str(exc)}")
|
|
raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
except PlugNMeetError as exc:
|
|
logger.error(f"[LiveSession Token] PlugNMeet API error - room_id={room_id} user_id={user.id} error={str(exc)}")
|
|
detail = exc.response_data or {'message': str(exc)}
|
|
status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY
|
|
raise AppAPIException(detail, status_code=status_code)
|
|
|
|
logger.info(f"[LiveSession Token] Success - room_id={room_id} user_id={user.id} role={user_role} course={course_slug}")
|
|
|
|
return Response({
|
|
'room_id': room_id,
|
|
'token': plugnmeet_response.get('token'),
|
|
'plugnmeet': plugnmeet_response,
|
|
})
|
|
|
|
@staticmethod
|
|
def _build_profile_url(request, user):
|
|
avatar = getattr(user, 'avatar', None)
|
|
if avatar and getattr(avatar, 'url', None):
|
|
return request.build_absolute_uri(avatar.url)
|
|
return None
|
|
|
|
|
|
class CourseLiveSessionRecordedFileAPIView(GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = LiveSessionRecordingSerializer
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Update recorded file for live session",
|
|
tags=["Imam-Javad - Course"],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'course_id', openapi.IN_PATH,
|
|
description="Course ID",
|
|
type=openapi.TYPE_INTEGER,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Recorded file updated successfully"
|
|
)
|
|
}
|
|
)
|
|
def patch(self, request, course_id, *args, **kwargs):
|
|
logger.info(f"[LiveSession Recorded File] Request from user_id={request.user.id} for course_id={course_id}")
|
|
|
|
course = get_object_or_404(Course, id=course_id)
|
|
|
|
if not request.user.can_manage_course(course):
|
|
logger.warning(f"[LiveSession Recorded File] Permission denied - user_id={request.user.id} course_id={course_id}")
|
|
raise AppAPIException({'message': 'You do not have permission to update this course.'}, status_code=status.HTTP_403_FORBIDDEN)
|
|
|
|
try:
|
|
session = course.live_sessions.latest('-started_at')
|
|
except CourseLiveSession.DoesNotExist:
|
|
logger.warning(f"[LiveSession Recorded File] No active session found - course_id={course_id} user_id={request.user.id}")
|
|
raise AppAPIException({'message': 'No live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
logger.info(f"[LiveSession Recorded File] Latest session found - session_id={session.id} course_id={course_id}")
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
recording = LiveSessionRecording.objects.create(
|
|
session=session,
|
|
file=serializer.validated_data['file'],
|
|
title=serializer.validated_data.get('title') or f"{session.subject} Recording",
|
|
recording_type=serializer.validated_data.get('recording_type', 'video'),
|
|
file_time=serializer.validated_data.get('file_time'),
|
|
)
|
|
|
|
logger.info(f"[LiveSession Recorded File] Recording created successfully - recording_id={recording.id} session_id={session.id} user_id={request.user.id}")
|
|
|
|
return Response({
|
|
'id': recording.id,
|
|
'session_id': session.id,
|
|
'title': recording.title,
|
|
'file': request.build_absolute_uri(recording.file.url),
|
|
'recording_type': recording.recording_type,
|
|
'file_time': recording.file_time,
|
|
'is_active': recording.is_active,
|
|
}, status=status.HTTP_201_CREATED)
|