""" Meetings API Module Backend API endpoints for Jitsi Meet integration """ import os import uuid import httpx from datetime import datetime, timedelta from typing import Optional, List from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel, EmailStr router = APIRouter(prefix="/api/meetings", tags=["meetings"]) # ============================================ # Configuration # ============================================ JITSI_BASE_URL = os.getenv("JITSI_PUBLIC_URL", "http://localhost:8443") CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081") # ============================================ # Models # ============================================ class MeetingConfig(BaseModel): enable_lobby: bool = True enable_recording: bool = False start_with_audio_muted: bool = True start_with_video_muted: bool = False require_display_name: bool = True enable_breakout: bool = False class CreateMeetingRequest(BaseModel): type: str = "quick" # quick, scheduled, training, parent, class title: str = "Neues Meeting" duration: int = 60 scheduled_at: Optional[str] = None config: Optional[MeetingConfig] = None description: Optional[str] = None invites: Optional[List[str]] = None class ScheduleMeetingRequest(BaseModel): title: str scheduled_at: str duration: int = 60 description: Optional[str] = None invites: Optional[List[str]] = None class TrainingRequest(BaseModel): title: str description: Optional[str] = None scheduled_at: str duration: int = 120 max_participants: int = 20 trainer: str config: Optional[MeetingConfig] = None class ParentTeacherRequest(BaseModel): student_name: str parent_name: str parent_email: Optional[str] = None scheduled_at: str reason: Optional[str] = None send_invite: bool = True duration: int = 30 class MeetingResponse(BaseModel): room_name: str join_url: str moderator_url: Optional[str] = None password: Optional[str] = None expires_at: Optional[str] = None class MeetingStats(BaseModel): active: int = 0 scheduled: int = 0 recordings: int = 0 participants: int = 0 class ActiveMeeting(BaseModel): room_name: str title: str participants: int started_at: str # ============================================ # In-Memory Storage (for demo purposes) # In production, use database # ============================================ scheduled_meetings = [] active_meetings = [] trainings = [] recordings = [] # ============================================ # Helper Functions # ============================================ def generate_room_name(prefix: str = "meeting") -> str: """Generate a unique room name""" return f"{prefix}-{uuid.uuid4().hex[:8]}" def generate_password() -> str: """Generate a simple password""" return uuid.uuid4().hex[:8] def build_jitsi_url(room_name: str, config: Optional[MeetingConfig] = None) -> str: """Build Jitsi meeting URL with config parameters""" params = [] if config: if config.start_with_audio_muted: params.append("config.startWithAudioMuted=true") if config.start_with_video_muted: params.append("config.startWithVideoMuted=true") if config.require_display_name: params.append("config.requireDisplayName=true") # Common config params.extend([ "config.prejoinPageEnabled=false", "config.disableDeepLinking=true", "config.defaultLanguage=de", "interfaceConfig.SHOW_JITSI_WATERMARK=false", "interfaceConfig.SHOW_BRAND_WATERMARK=false" ]) url = f"{JITSI_BASE_URL}/{room_name}" if params: url += "#" + "&".join(params) return url async def call_consent_service(endpoint: str, method: str = "GET", data: dict = None) -> dict: """Call the consent service API""" async with httpx.AsyncClient() as client: url = f"{CONSENT_SERVICE_URL}{endpoint}" if method == "GET": response = await client.get(url) elif method == "POST": response = await client.post(url, json=data) else: raise ValueError(f"Unsupported method: {method}") if response.status_code >= 400: return None return response.json() # ============================================ # API Endpoints # ============================================ @router.get("/stats", response_model=MeetingStats) async def get_meeting_stats(): """Get meeting statistics""" return MeetingStats( active=len(active_meetings), scheduled=len(scheduled_meetings), recordings=len(recordings), participants=sum(m.get("participants", 0) for m in active_meetings) ) @router.get("/active", response_model=List[ActiveMeeting]) async def get_active_meetings(): """Get list of active meetings""" return [ ActiveMeeting( room_name=m["room_name"], title=m["title"], participants=m.get("participants", 0), started_at=m.get("started_at", datetime.now().isoformat()) ) for m in active_meetings ] @router.post("/create", response_model=MeetingResponse) async def create_meeting(request: CreateMeetingRequest): """Create a new meeting""" config = request.config or MeetingConfig() # Generate room name based on type if request.type == "quick": room_name = generate_room_name("quick") elif request.type == "training": room_name = generate_room_name("schulung") elif request.type == "parent": room_name = generate_room_name("elterngespraech") elif request.type == "class": room_name = generate_room_name("klasse") else: room_name = generate_room_name("meeting") join_url = build_jitsi_url(room_name, config) # Store meeting if scheduled if request.scheduled_at: scheduled_meetings.append({ "room_name": room_name, "title": request.title, "scheduled_at": request.scheduled_at, "duration": request.duration, "config": config.model_dump() if config else None }) return MeetingResponse( room_name=room_name, join_url=join_url ) @router.post("/schedule", response_model=MeetingResponse) async def schedule_meeting(request: ScheduleMeetingRequest): """Schedule a new meeting""" room_name = generate_room_name("meeting") meeting = { "room_name": room_name, "title": request.title, "scheduled_at": request.scheduled_at, "duration": request.duration, "description": request.description, "invites": request.invites or [] } scheduled_meetings.append(meeting) join_url = build_jitsi_url(room_name) # TODO: Send email invites if configured return MeetingResponse( room_name=room_name, join_url=join_url ) @router.post("/training", response_model=MeetingResponse) async def create_training(request: TrainingRequest): """Create a training session""" # Generate room name from title title_slug = request.title.lower().replace(" ", "-")[:20] room_name = f"schulung-{title_slug}-{uuid.uuid4().hex[:4]}" config = request.config or MeetingConfig( enable_lobby=True, enable_recording=True, start_with_audio_muted=True ) training = { "room_name": room_name, "title": request.title, "description": request.description, "scheduled_at": request.scheduled_at, "duration": request.duration, "max_participants": request.max_participants, "trainer": request.trainer, "config": config.model_dump() } trainings.append(training) scheduled_meetings.append(training) join_url = build_jitsi_url(room_name, config) return MeetingResponse( room_name=room_name, join_url=join_url ) @router.post("/parent-teacher", response_model=MeetingResponse) async def create_parent_teacher_meeting(request: ParentTeacherRequest): """Create a parent-teacher meeting""" # Generate room name with student name and date student_slug = request.student_name.lower().replace(" ", "-")[:15] date_str = datetime.fromisoformat(request.scheduled_at).strftime("%Y%m%d-%H%M") room_name = f"elterngespraech-{student_slug}-{date_str}" # Generate password for security password = generate_password() config = MeetingConfig( enable_lobby=True, enable_recording=False, start_with_audio_muted=False ) meeting = { "room_name": room_name, "title": f"Elterngespräch - {request.student_name}", "student_name": request.student_name, "parent_name": request.parent_name, "parent_email": request.parent_email, "scheduled_at": request.scheduled_at, "duration": request.duration, "reason": request.reason, "password": password, "config": config.model_dump() } scheduled_meetings.append(meeting) join_url = build_jitsi_url(room_name, config) # TODO: Send email invite to parents if configured return MeetingResponse( room_name=room_name, join_url=join_url, password=password ) @router.get("/scheduled") async def get_scheduled_meetings(): """Get all scheduled meetings""" return scheduled_meetings @router.get("/trainings") async def get_trainings(): """Get all training sessions""" return trainings @router.delete("/{room_name}") async def delete_meeting(room_name: str): """Delete a scheduled meeting""" # Find and remove the meeting (in-place modification) for i, m in enumerate(scheduled_meetings): if m["room_name"] == room_name: scheduled_meetings.pop(i) break return {"status": "deleted"} # ============================================ # Recording Endpoints # ============================================ @router.get("/recordings") async def get_recordings(): """Get list of recordings""" # Demo data return [ { "id": "docker-basics", "title": "Docker Grundlagen Schulung", "date": "2025-12-10T10:00:00", "duration": "1:30:00", "size_mb": 156, "participants": 15 }, { "id": "team-kw49", "title": "Team-Meeting KW 49", "date": "2025-12-06T14:00:00", "duration": "1:00:00", "size_mb": 98, "participants": 8 }, { "id": "parent-mueller", "title": "Elterngespräch - Max Müller", "date": "2025-12-02T16:00:00", "duration": "0:28:00", "size_mb": 42, "participants": 2 } ] @router.get("/recordings/{recording_id}") async def get_recording(recording_id: str): """Get recording details""" return { "id": recording_id, "title": "Recording " + recording_id, "date": "2025-12-10T10:00:00", "duration": "1:30:00", "size_mb": 156, "download_url": f"/api/recordings/{recording_id}/download" } @router.get("/recordings/{recording_id}/download") async def download_recording(recording_id: str): """Download a recording""" # In production, this would stream the actual file raise HTTPException(status_code=404, detail="Recording file not found (demo mode)") @router.delete("/recordings/{recording_id}") async def delete_recording(recording_id: str): """Delete a recording""" return {"status": "deleted", "id": recording_id} # ============================================ # Health Check # ============================================ @router.get("/health") async def health_check(): """Check meetings service health""" # Check Jitsi availability jitsi_healthy = False try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(JITSI_BASE_URL) jitsi_healthy = response.status_code == 200 except Exception: pass return { "status": "healthy" if jitsi_healthy else "degraded", "jitsi_url": JITSI_BASE_URL, "jitsi_available": jitsi_healthy, "scheduled_meetings": len(scheduled_meetings), "active_meetings": len(active_meetings) }