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.
 
 

742 lines
31 KiB

# import json
# import hmac
# import hashlib
# import logging
# from typing import Dict, Any
# from django.conf import settings
# from django.core.exceptions import ImproperlyConfigured
# from django.utils import timezone
# from django.utils.decorators import method_decorator
# from django.views.decorators.csrf import csrf_exempt
# from rest_framework import status
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework.permissions import AllowAny
# from drf_yasg.utils import swagger_auto_schema
# from drf_yasg import openapi
# from apps.course.models import CourseLiveSession, LiveSessionUser, Course, Participant
# from apps.account.models import User
# from utils.exceptions import AppAPIException
# logger = logging.getLogger(__name__)
# @method_decorator(csrf_exempt, name='dispatch')
# class PlugNMeetWebhookAPIView(APIView):
# """
# Webhook endpoint to receive events from PlugNMeet server.
# Events handled:
# - room_finished: Close the live session
# - participant_joined: Create LiveSessionUser entry
# - participant_left: Mark LiveSessionUser as offline/exited
# - end_recording: (Future implementation)
# """
# permission_classes = [AllowAny]
# @swagger_auto_schema(
# operation_description="Handle webhook events from PlugNMeet server for live sessions",
# tags=["Imam-Javad - Course"],
# responses={
# 200: openapi.Response(description="Webhook processed successfully"),
# 400: openapi.Response(description="Invalid webhook signature or data"),
# 500: openapi.Response(description="Internal server error")
# }
# )
# def post(self, request, *args, **kwargs):
# logger.info(f"[PlugNMeet Webhook] Received webhook request")
# # Verify webhook signature
# if not self._verify_webhook_signature(request):
# logger.warning(f"[PlugNMeet Webhook] Invalid signature")
# raise AppAPIException(
# {'message': 'Invalid webhook signature'},
# status_code=status.HTTP_403_FORBIDDEN
# )
# try:
# payload = json.loads(request.body.decode('utf-8'))
# except (json.JSONDecodeError, UnicodeDecodeError) as e:
# logger.error(f"[PlugNMeet Webhook] Invalid JSON payload - error={str(e)}")
# raise AppAPIException(
# {'message': 'Invalid JSON payload'},
# status_code=status.HTTP_400_BAD_REQUEST
# )
# event = payload.get('event')
# if not event:
# logger.warning(f"[PlugNMeet Webhook] Missing event field")
# raise AppAPIException(
# {'message': 'Missing event field'},
# status_code=status.HTTP_400_BAD_REQUEST
# )
# logger.info(f"[PlugNMeet Webhook] Processing event={event}")
# # Route to appropriate handler
# handler_map = {
# 'room_finished': self._handle_room_finished,
# 'participant_joined': self._handle_participant_joined,
# 'participant_left': self._handle_participant_left,
# 'end_recording': self._handle_end_recording,
# }
# handler = handler_map.get(event)
# if not handler:
# logger.info(f"[PlugNMeet Webhook] Unhandled event={event}, ignoring")
# return Response({'status': 'ok', 'message': f'Event {event} ignored'})
# try:
# result = handler(payload)
# logger.info(f"[PlugNMeet Webhook] Event processed successfully - event={event}")
# return Response({'status': 'ok', **result})
# except Exception as e:
# logger.error(f"[PlugNMeet Webhook] Error processing event={event} - error={str(e)}", exc_info=True)
# return Response(
# {'status': 'error', 'message': str(e)},
# status=status.HTTP_500_INTERNAL_SERVER_ERROR
# )
# # def _verify_webhook_signature(self, request) -> bool:
# # """
# # Verify webhook signature using SHA256 HMAC.
# # Expects Hash-Token header with SHA256 signature of request body.
# # """
# # hash_token = request.headers.get('Hash-Token')
# # if not hash_token:
# # logger.warning(f"[PlugNMeet Webhook] Missing Hash-Token header")
# # return False
# # # Get API secret from settings
# # api_secret = getattr(settings, 'PLUGNMEET_API_SECRET', None)
# # if not api_secret:
# # logger.error(f"[PlugNMeet Webhook] PLUGNMEET_API_SECRET not configured")
# # raise ImproperlyConfigured("PLUGNMEET_API_SECRET is not configured")
# # # Calculate expected signature
# # body = request.body
# # expected_signature = hmac.new(
# # api_secret.encode('utf-8'),
# # body,
# # hashlib.sha256
# # ).hexdigest()
# # # Compare signatures (constant time comparison)
# # is_valid = hmac.compare_digest(hash_token, expected_signature)
# # if not is_valid:
# # logger.warning(f"[PlugNMeet Webhook] Signature mismatch - expected={expected_signature[:10]}... got={hash_token[:10]}...")
# # return is_valid
# def _verify_webhook_signature(self, request) -> bool:
# """
# DEBUG VERSION: Prints details to find the mismatch.
# """
# hash_token = request.headers.get('Hash-Token')
# if not hash_token:
# logger.error("[PlugNMeet Webhook] MISSING Hash-Token header")
# return False
# # Get API secret from settings
# api_secret = getattr(settings, 'PLUGNMEET_API_SECRET', None)
# if not api_secret:
# logger.error("[PlugNMeet Webhook] PLUGNMEET_API_SECRET is missing in settings.py")
# raise ImproperlyConfigured("PLUGNMEET_API_SECRET is not configured")
# # DEBUG: Print the first/last few chars of the secret to ensure it's loaded correctly
# # DO NOT log the whole secret in production!
# logger.info(f"[DEBUG] Secret in Django: {api_secret[:4]}...{api_secret[-4:]}")
# # Calculate expected signature
# body = request.body
# # DEBUG: Check if body is empty (common middleware issue)
# if len(body) == 0:
# logger.error("[DEBUG] Request Body is EMPTY! Middleware might have consumed it.")
# expected_signature = hmac.new(
# api_secret.encode('utf-8'),
# body,
# hashlib.sha256
# ).hexdigest()
# # DEBUG: Compare them visually in logs
# logger.info(f"[DEBUG] Received Token: {hash_token}")
# logger.info(f"[DEBUG] Calculated: {expected_signature}")
# is_valid = hmac.compare_digest(hash_token, expected_signature)
# if not is_valid:
# logger.error("[PlugNMeet Webhook] Signature MISMATCH")
# return is_valid
# def _handle_room_finished(self, payload: Dict[str, Any]) -> Dict[str, Any]:
# """
# Handle room_finished event: Close the live session and all user sessions.
# Payload structure:
# {
# "event": "room_finished",
# "room": {
# "sid": "room-123456",
# "identity": "course-slug-20240101120000",
# "name": "کلاس جبر",
# "duration": 3600
# }
# }
# """
# room_data = payload.get('room', {})
# room_id = room_data.get('identity')
# if not room_id:
# logger.warning(f"[PlugNMeet Webhook] room_finished: Missing room identity")
# return {'message': 'Missing room identity'}
# logger.info(f"[PlugNMeet Webhook] room_finished - room_id={room_id}")
# try:
# session = CourseLiveSession.objects.get(room_id=room_id, ended_at__isnull=True)
# except CourseLiveSession.DoesNotExist:
# logger.warning(f"[PlugNMeet Webhook] room_finished: Session not found or already ended - room_id={room_id}")
# return {'message': 'Session not found or already ended'}
# # Close the session
# now = timezone.now()
# session.ended_at = now
# session.save(update_fields=['ended_at', 'updated_at'])
# logger.info(f"[PlugNMeet Webhook] Session closed - session_id={session.id} room_id={room_id}")
# # Close all active user sessions
# updated_count = LiveSessionUser.objects.filter(
# session=session,
# is_online=True,
# exited_at__isnull=True
# ).update(
# is_online=False,
# exited_at=now,
# updated_at=now
# )
# logger.info(f"[PlugNMeet Webhook] User sessions closed - session_id={session.id} count={updated_count}")
# return {
# 'message': 'Room finished',
# 'session_id': session.id,
# 'users_disconnected': updated_count
# }
# def _handle_participant_joined(self, payload: Dict[str, Any]) -> Dict[str, Any]:
# """
# Handle participant_joined event: Create or update LiveSessionUser entry.
# Payload structure:
# {
# "event": "participant_joined",
# "room": {
# "sid": "room-123456",
# "identity": "course-slug-20240101120000"
# },
# "participant": {
# "sid": "participant-user-27",
# "identity": "27",
# "name": "دانشجو نمونه",
# "metadata": "{\"is_admin\": false}",
# "joinedAt": 1697497300
# }
# }
# """
# room_data = payload.get('room', {})
# participant_data = payload.get('participant', {})
# room_id = room_data.get('identity')
# user_identity = participant_data.get('identity')
# joined_at_timestamp = participant_data.get('joinedAt')
# if not room_id or not user_identity:
# logger.warning(f"[PlugNMeet Webhook] participant_joined: Missing room_id or user_identity")
# return {'message': 'Missing required fields'}
# logger.info(f"[PlugNMeet Webhook] participant_joined - room_id={room_id} user_identity={user_identity}")
# # Get session
# try:
# session = CourseLiveSession.objects.get(room_id=room_id, ended_at__isnull=True)
# except CourseLiveSession.DoesNotExist:
# logger.warning(f"[PlugNMeet Webhook] participant_joined: Active session not found - room_id={room_id}")
# return {'message': 'Active session not found'}
# # Get user
# try:
# user = User.objects.get(id=int(user_identity))
# except (User.DoesNotExist, ValueError):
# logger.warning(f"[PlugNMeet Webhook] participant_joined: User not found - user_identity={user_identity}")
# return {'message': 'User not found'}
# # Determine user role
# is_admin = user.can_manage_course(session.course)
# role = 'moderator' if is_admin else 'participant'
# # Parse joined_at timestamp
# if joined_at_timestamp:
# entered_at = timezone.datetime.fromtimestamp(joined_at_timestamp, tz=timezone.utc)
# else:
# entered_at = timezone.now()
# # Check if user has an existing session entry that was marked as offline
# # If so, reactivate it instead of creating a new one
# existing_session = LiveSessionUser.objects.filter(
# session=session,
# user=user,
# is_online=False
# ).order_by('-entered_at').first()
# if existing_session:
# # Reactivate existing session
# existing_session.is_online = True
# existing_session.exited_at = None
# existing_session.save(update_fields=['is_online', 'exited_at', 'updated_at'])
# logger.info(f"[PlugNMeet Webhook] User rejoined (reactivated) - session_user_id={existing_session.id} user_id={user.id}")
# return {
# 'message': 'Participant rejoined',
# 'session_user_id': existing_session.id,
# 'created': False
# }
# # Create new LiveSessionUser entry
# try:
# session_user = LiveSessionUser.objects.create(
# session=session,
# user=user,
# role=role,
# entered_at=entered_at,
# is_online=True,
# )
# logger.info(f"[PlugNMeet Webhook] User joined - session_user_id={session_user.id} user_id={user.id} role={role}")
# except Exception as e:
# logger.error(f"[PlugNMeet Webhook] Failed to create session user - error={str(e)}")
# return {'message': f'Failed to create session user: {str(e)}'}
# return {
# 'message': 'Participant joined',
# 'session_user_id': session_user.id,
# 'created': created
# }
# def _handle_participant_left(self, payload: Dict[str, Any]) -> Dict[str, Any]:
# """
# Handle participant_left event: Mark LiveSessionUser as offline/exited.
# Payload structure:
# {
# "event": "participant_left",
# "room": {
# "sid": "room-123456",
# "identity": "course-slug-20240101120000"
# },
# "participant": {
# "sid": "participant-user-27",
# "identity": "27",
# "state": "DISCONNECTED",
# "joinedAt": 1697497300,
# "duration": 1800
# }
# }
# """
# room_data = payload.get('room', {})
# participant_data = payload.get('participant', {})
# room_id = room_data.get('identity')
# user_identity = participant_data.get('identity')
# if not room_id or not user_identity:
# logger.warning(f"[PlugNMeet Webhook] participant_left: Missing room_id or user_identity")
# return {'message': 'Missing required fields'}
# logger.info(f"[PlugNMeet Webhook] participant_left - room_id={room_id} user_identity={user_identity}")
# # Get session
# try:
# session = CourseLiveSession.objects.get(room_id=room_id)
# except CourseLiveSession.DoesNotExist:
# logger.warning(f"[PlugNMeet Webhook] participant_left: Session not found - room_id={room_id}")
# return {'message': 'Session not found'}
# # Get user
# try:
# user = User.objects.get(id=int(user_identity))
# except (User.DoesNotExist, ValueError):
# logger.warning(f"[PlugNMeet Webhook] participant_left: User not found - user_identity={user_identity}")
# return {'message': 'User not found'}
# # Update LiveSessionUser
# now = timezone.now()
# updated_count = LiveSessionUser.objects.filter(
# session=session,
# user=user,
# is_online=True,
# exited_at__isnull=True
# ).update(
# is_online=False,
# exited_at=now,
# updated_at=now
# )
# if updated_count > 0:
# logger.info(f"[PlugNMeet Webhook] User left - session_id={session.id} user_id={user.id}")
# else:
# logger.warning(f"[PlugNMeet Webhook] participant_left: No active session found for user - session_id={session.id} user_id={user.id}")
# return {
# 'message': 'Participant left',
# 'updated': updated_count > 0
# }
# def _handle_end_recording(self, payload: Dict[str, Any]) -> Dict[str, Any]:
# """
# Handle end_recording event: Download and save recording file.
# Payload structure:
# {
# "event": "end_recording",
# "room": {
# "sid": "room-123456",
# "identity": "course-slug-20240101120000"
# },
# "recording_info": {
# "recordingId": "rec-123456",
# "roomId": "course-slug-20240101120000",
# "recordingType": "COMPOSITE",
# "fileName": "algebra-1402-20231016.mp4",
# "duration": 3600,
# "status": "FINISHED"
# }
# }
# """
# from django.core.files.base import ContentFile
# from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
# from apps.course.models import LiveSessionRecording
# import os
# import tempfile
# import subprocess
# room_data = payload.get('room', {})
# recording_info = payload.get('recording_info', {})
# room_id = room_data.get('identity')
# recording_id = recording_info.get('recordingId')
# file_name = recording_info.get('fileName', 'recording.mp4')
# duration = recording_info.get('duration', 0)
# if not room_id or not recording_id:
# logger.warning(f"[PlugNMeet Webhook] end_recording: Missing room_id or recording_id")
# return {'message': 'Missing required fields'}
# logger.info(f"[PlugNMeet Webhook] end_recording - room_id={room_id} recording_id={recording_id}")
# # Get session
# try:
# session = CourseLiveSession.objects.get(room_id=room_id)
# except CourseLiveSession.DoesNotExist:
# logger.warning(f"[PlugNMeet Webhook] end_recording: Session not found - room_id={room_id}")
# return {'message': 'Session not found'}
# try:
# client = PlugNMeetClient()
# # Step 1: Get recording info
# logger.info(f"[PlugNMeet Webhook] Fetching recording info - recording_id={recording_id}")
# recording_data = client.get_recording_info(recording_id)
# if not recording_data.get('status'):
# logger.error(f"[PlugNMeet Webhook] Failed to get recording info - recording_id={recording_id}")
# return {'message': 'Failed to get recording info', 'error': recording_data.get('msg')}
# # Step 2: Get download token
# logger.info(f"[PlugNMeet Webhook] Getting download token - recording_id={recording_id}")
# token_response = client.get_recording_download_token(recording_id)
# if not token_response.get('status'):
# logger.error(f"[PlugNMeet Webhook] Failed to get download token - recording_id={recording_id}")
# return {'message': 'Failed to get download token', 'error': token_response.get('msg')}
# download_token = token_response.get('token')
# if not download_token:
# logger.error(f"[PlugNMeet Webhook] No download token in response - recording_id={recording_id}")
# return {'message': 'No download token received'}
# # Step 3: Download file
# download_path = f"/download/recording/{download_token}"
# # Create temporary file
# with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
# tmp_file_path = tmp_file.name
# try:
# logger.info(f"[PlugNMeet Webhook] Downloading recording file - recording_id={recording_id}")
# client.download_file(download_path, tmp_file_path)
# # Get file size
# file_size = os.path.getsize(tmp_file_path)
# logger.info(f"[PlugNMeet Webhook] File downloaded - size={file_size} bytes")
# # Determine recording type (video or audio)
# is_video = file_name.lower().endswith(('.mp4', '.webm', '.mkv'))
# recording_type = 'video' if is_video else 'voice'
# # Read file content
# with open(tmp_file_path, 'rb') as f:
# file_content = f.read()
# # Create LiveSessionRecording entry
# recording = LiveSessionRecording.objects.create(
# session=session,
# title=f"{session.subject} - Recording",
# file_time=timezone.timedelta(seconds=duration) if duration > 0 else None,
# recording_type=recording_type,
# )
# # Save file to Django FileField
# recording.file.save(file_name, ContentFile(file_content), save=True)
# logger.info(f"[PlugNMeet Webhook] Recording saved - recording_id={recording.id} file={file_name}")
# # Generate thumbnail for video recordings (if ffmpeg is available)
# thumbnail_generated = False
# if is_video and file_size > 0:
# try:
# thumbnail_generated = self._generate_video_thumbnail(tmp_file_path, recording)
# if thumbnail_generated:
# logger.info(f"[PlugNMeet Webhook] Thumbnail generated - recording_id={recording.id}")
# except Exception as e:
# logger.warning(f"[PlugNMeet Webhook] Thumbnail generation skipped - error={str(e)}")
# return {
# 'message': 'Recording downloaded and saved successfully',
# 'recording_id': recording.id,
# 'file_name': file_name,
# 'file_size': file_size,
# 'thumbnail_generated': thumbnail_generated
# }
# finally:
# # Clean up temporary file
# if os.path.exists(tmp_file_path):
# os.unlink(tmp_file_path)
# logger.debug(f"[PlugNMeet Webhook] Temporary file deleted - path={tmp_file_path}")
# except PlugNMeetError as e:
# logger.error(f"[PlugNMeet Webhook] PlugNMeet API error - recording_id={recording_id} error={str(e)}")
# return {'message': f'PlugNMeet API error: {str(e)}'}
# except Exception as e:
# logger.error(f"[PlugNMeet Webhook] Unexpected error - recording_id={recording_id} error={str(e)}", exc_info=True)
# return {'message': f'Unexpected error: {str(e)}'}
# def _generate_video_thumbnail(self, video_path: str, recording: 'LiveSessionRecording') -> bool:
# """
# Generate thumbnail from video file using ffmpeg.
# Args:
# video_path: Path to the video file
# recording: LiveSessionRecording instance
# Returns:
# True if thumbnail generated successfully, False otherwise
# """
# from django.core.files.base import ContentFile
# import subprocess
# import tempfile
# import os
# try:
# # Create temporary file for thumbnail
# with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_thumb:
# thumbnail_path = tmp_thumb.name
# # Extract frame at 1 second using ffmpeg
# # -ss 1: seek to 1 second
# # -i: input file
# # -frames:v 1: extract 1 frame
# # -q:v 2: quality (2 is high quality)
# cmd = [
# 'ffmpeg',
# '-ss', '1',
# '-i', video_path,
# '-frames:v', '1',
# '-q:v', '2',
# '-vf', 'scale=640:-1', # scale to width 640, maintain aspect ratio
# '-y', # overwrite output file
# thumbnail_path
# ]
# result = subprocess.run(
# cmd,
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
# timeout=30
# )
# if result.returncode != 0:
# logger.warning(f"[PlugNMeet Webhook] ffmpeg failed - return_code={result.returncode}")
# return False
# # Check if thumbnail was created
# if not os.path.exists(thumbnail_path) or os.path.getsize(thumbnail_path) == 0:
# logger.warning(f"[PlugNMeet Webhook] Thumbnail file not created or empty")
# return False
# # Save thumbnail to recording
# with open(thumbnail_path, 'rb') as f:
# thumbnail_content = f.read()
# thumbnail_filename = f"thumb_{recording.id}.jpg"
# recording.thumbnail.save(thumbnail_filename, ContentFile(thumbnail_content), save=True)
# # Clean up temporary file
# os.unlink(thumbnail_path)
# return True
# except subprocess.TimeoutExpired:
# logger.error(f"[PlugNMeet Webhook] ffmpeg timeout during thumbnail generation")
# return False
# except FileNotFoundError:
# logger.error(f"[PlugNMeet Webhook] ffmpeg not found - please install ffmpeg")
# return False
# except Exception as e:
# logger.error(f"[PlugNMeet Webhook] Thumbnail generation error - error={str(e)}", exc_info=True)
# return False
# finally:
# # Clean up temporary file if it still exists
# if 'thumbnail_path' in locals() and os.path.exists(thumbnail_path):
# try:
# os.unlink(thumbnail_path)
# except:
# pass
from rest_framework.parsers import BaseParser
import json
import hmac
import hashlib
import logging
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status
from utils.exceptions import AppAPIException
logger = logging.getLogger(__name__)
# 1. Custom Parser to preserve the raw body for HMAC verification
class RawJSONParser(BaseParser):
media_type = 'application/json'
def parse(self, stream, media_type=None, parser_context=None):
# We return the raw bytes so we can verify the signature first
return stream.read()
@method_decorator(csrf_exempt, name='dispatch')
class PlugNMeetWebhookAPIView(APIView):
# 2. CRITICAL: Disable Global Authentication logic for this specific endpoint
authentication_classes = []
permission_classes = [AllowAny]
# 3. Use the raw parser to prevent DRF from consuming the stream prematurely
parser_classes = [RawJSONParser]
def post(self, request, *args, **kwargs):
logger.info("⚡ [PlugNMeet Webhook] Request Hit the View!")
# --- 1. ROBUST HEADER EXTRACTION ---
# Try standard way
hash_token = request.headers.get('Hash-Token')
# Try Django internal way (Fall back to META)
# Django converts "Hash-Token" -> "HTTP_HASH_TOKEN"
if not hash_token:
hash_token = request.META.get('HTTP_HASH_TOKEN')
# --- 2. DEBUG IF MISSING ---
if not hash_token:
logger.error("❌ [PlugNMeet Webhook] MISSING Hash-Token.")
# Print ALL headers to see if it's there under a different name
# This is critical for debugging Nginx stripping issues
logger.info("--- DUMPING ALL HEADERS ---")
for key, value in request.headers.items():
logger.info(f"Header: {key} = {value}")
# Also dump META keys starting with HTTP
meta_headers = {k: v for k, v in request.META.items() if k.startswith('HTTP_')}
logger.info(f"META Headers: {meta_headers}")
logger.info("-----------------------------")
return Response({'message': 'Missing Hash-Token header'}, status=403)
# # --- SIGNATURE VERIFICATION ---
# hash_token = request.headers.get('Hash-Token')
# if not hash_token:
# logger.error("❌ [PlugNMeet Webhook] Missing Hash-Token")
# return Response({'message': 'Missing Hash-Token'}, status=403)
api_secret = getattr(settings, 'PLUGNMEET_API_SECRET', None)
# We use request.data because our RawJSONParser returned the bytes into it
body_bytes = request.data
# Debug Logs (Remove in production)
logger.info(f"🔍 [DEBUG] Secret: {api_secret[:5]}...")
logger.info(f"🔍 [DEBUG] Body Bytes Len: {len(body_bytes)}")
expected_signature = hmac.new(
api_secret.encode('utf-8'),
body_bytes,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(hash_token, expected_signature):
logger.error(f"❌ [PlugNMeet Webhook] Mismatch! \nExpected: {expected_signature}\nReceived: {hash_token}")
return Response({'message': 'Invalid webhook signature'}, status=403)
# --- PARSING ---
try:
# Now we manually parse the JSON since we used a RawParser
payload = json.loads(body_bytes.decode('utf-8'))
except Exception as e:
logger.error(f"❌ [PlugNMeet Webhook] JSON Error: {e}")
return Response({'message': 'Invalid JSON'}, status=400)
# --- EVENT ROUTING ---
event = payload.get('event')
logger.info(f"✅ [PlugNMeet Webhook] Processing Event: {event}")
handler_map = {
'room_finished': self._handle_room_finished,
'participant_joined': self._handle_participant_joined,
'participant_left': self._handle_participant_left,
'end_recording': self._handle_end_recording,
}
handler = handler_map.get(event)
if not handler:
return Response({'status': 'ok', 'message': f'Event {event} ignored'})
try:
result = handler(payload)
return Response({'status': 'ok', **result})
except Exception as e:
logger.error(f"❌ [PlugNMeet Webhook] Logic Error: {e}", exc_info=True)
return Response({'status': 'error', 'message': str(e)}, status=500)
# ... (Keep your existing _handle methods exactly as they were) ...
# Copy _handle_room_finished, _handle_participant_joined, etc. here
def _handle_room_finished(self, payload):
# ... your existing code ...
return {'status': 'ok'} # Placeholder