This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/meetings_api.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

444 lines
12 KiB
Python

"""
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)
}