Neda/Back/domains/realtime/ws.py

171 lines
5.3 KiB
Python

import uuid
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Request, Header, status, HTTPException
from livekit import api
from core.config import settings
from core.websocket import get_ws_current_user
from db.session import AsyncSessionLocal
from domains.groups.repo import get_group_member
from domains.realtime.ws_manager import manager
from domains.realtime.presence_service import (
user_join_group,
user_leave_group,
list_online_users
)
from domains.realtime.speaker_service import (
request_speak,
stop_speaking,
current_speaker
)
from integrations.livekit.token_service import generate_join_token
router = APIRouter()
@router.post("/lk-webhook")
async def livekit_webhook(
request: Request,
authorization: str = Header(None)
):
"""
LiveKit Webhook to sync presence and handle participant events.
"""
receiver = api.WebhookReceiver(
api.TokenVerifier(
settings.LIVEKIT_API_KEY,
settings.LIVEKIT_API_SECRET
)
)
try:
body = await request.body()
event = receiver.receive(body.decode("utf-8"), authorization)
except Exception:
raise HTTPException(status_code=401, detail="Invalid webhook signature")
room_name = event.room.name
identity = event.participant.identity
if event.event == "participant_joined":
await user_join_group(room_name, identity)
await manager.broadcast(room_name, {
"type": "presence",
"users": await list_online_users(room_name, use_livekit=True)
})
elif event.event == "participant_left":
await user_leave_group(room_name, identity)
await manager.broadcast(room_name, {
"type": "presence",
"users": await list_online_users(room_name, use_livekit=True)
})
return {"status": "ok"}
@router.websocket("/ws/groups/{group_id}")
async def group_ws(websocket: WebSocket, group_id: str):
user = await get_ws_current_user(websocket)
try:
group_id_uuid = uuid.UUID(group_id)
except ValueError:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
# check if user is member of group
async with AsyncSessionLocal() as db:
membership = await get_group_member(db, group_id_uuid, user.id)
if not membership:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return
from domains.groups.repo import get_group_by_id
group = await get_group_by_id(db, group_id_uuid)
group_type = str(group.type) if group else "public"
user_id = str(user.id)
# connect websocket
await manager.connect(group_id, websocket)
# add presence
await user_join_group(group_id, user_id)
# give listener token
listener_token = generate_join_token(
user_id=user_id,
group_id=group_id,
can_publish=False
)
await websocket.send_json({
"type": "livekit_token",
"token": listener_token
})
# broadcast updated presence
await manager.broadcast(
group_id,
{
"type": "presence",
"users": await list_online_users(group_id)
}
)
try:
while True:
data = await websocket.receive_json()
event = data.get("type")
# user wants to speak
if event == "request_speak":
success = await request_speak(group_id, user_id, group_type)
if success:
# Broadcast globally that someone is speaking
await manager.broadcast(
group_id,
{
"type": "speaker",
"user_id": user_id
}
)
# Signal the specific client to unmute
await websocket.send_json({
"type": "speaker_granted"
})
else:
# someone else is speaking
async with AsyncSessionLocal() as temp_db:
speaker = await current_speaker(temp_db, group_id)
await websocket.send_json(
{
"type": "speaker_busy",
"speaker": speaker
}
)
# user stops speaking
elif event == "stop_speak":
released = await stop_speaking(group_id, user_id, group_type)
if released:
await manager.broadcast(
group_id,
{
"type": "speaker_released"
}
)
elif event == "keep_alive_speaker":
from db.redis import extend_speaker_lock
await extend_speaker_lock(group_id, user_id)
except WebSocketDisconnect:
manager.disconnect(group_id, websocket)
await user_leave_group(group_id, user_id)
await stop_speaking(group_id, user_id, group_type)
await manager.broadcast(
group_id,
{
"type": "presence",
"users": await list_online_users(group_id)
}
)