@ -11,7 +11,8 @@ 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
from datetime 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
@ -42,91 +43,156 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
)
}
)
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 )
def post ( self , request , slug , * args , * * kwargs ) :
# 1. Validation and Permission Logic
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} " )
raise AppAPIException ( { ' message ' : ' Permission denied ' } , status_code = status . HTTP_403_FORBIDDEN )
subject = serializer . validated_data . get ( ' subject ' ) or f " {course.title} Live Session "
room_id = self . _build_room_id ( course )
# 2. Build Safe Room ID (Prefix with 'room-' to satisfy NATS rules)
# OLD: f"{course.id}-imamjavad" -> "9-imamjavad" (Risk of NATS errors)
# NEW: f"room-{course.id}-imamjavad" -> "room-9-imamjavad" (Safe)
room_id = f " room-{course.id}-imamjavad "
subject = f " {course.title} Live Session "
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} " )
# 3. Create Room via PlugNMeet API
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 )
# Note: client.create_room only CREATES the room.
# It does not give you a token to join it.
plugnmeet_response = client . create_room ( {
' room_id ' : room_id ,
' metadata ' : metadata ,
} )
except Exception as exc :
logger . error ( f " PlugNMeet Error: {exc} " )
raise AppAPIException ( { ' message ' : str ( exc ) } , status_code = 500 )
# 4. Save to Database
session , created = CourseLiveSession . objects . get_or_create (
course = course ,
room_id = room_id ,
defaults = {
' subject ' : subject ,
' started_at ' : timezone . now ( ) ,
} ,
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} " )
# 5. GENERATE JOIN TOKEN (Critical for User Access)
# You must fetch these exactly as they are in your config.yaml
PNM_API_KEY = " habibmeet_api_key_2024 "
PNM_SECRET = " habibmeet_secret_zumyyYWqv7KR2kUqvYdq4z4sXg7XTBD2ljT6_2024 "
token_payload = {
" iss " : PNM_API_KEY , # Issuer must be the API Key
" exp " : int ( time . time ( ) ) + 3600 , # 1 hour expiry
" sub " : str ( request . user . id ) ,
" room_id " : room_id ,
" name " : f " {request.user.first_name} {request.user.last_name} " ,
" is_admin " : True , # Gives moderator privileges
" user_id " : str ( request . user . id ) ,
}
# SIGN with the SECRET, not the key
token = jwt . encode ( token_payload , PNM_SECRET , algorithm = " HS256 " )
return Response ( {
' success ' : True ,
' 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 )
# Frontend uses this to connect:
# https://meet.newhorizonco.uk/?access_token={access_token}
' access_token ' : token
} , status = status . HTTP_201_CREATED )
# 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 " {course.id}-imamjavad "
return f " room- {course.id}-imamjavad"
def _build_metadata ( self , subject : str ) - > dict :
# Build secured, centralized metadata. Client overrides are NOT allowed.