diff --git a/app/api/voice_rooms.py b/app/api/voice_rooms.py new file mode 100644 index 0000000..18043ab --- /dev/null +++ b/app/api/voice_rooms.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, Body +from app.services.voice_rooms import VoiceRoomService +from app.realtime.voice_hub import voice_hub + +router = APIRouter(prefix="/api/voice") +service = VoiceRoomService() + + +@router.get("/rooms") +async def list_rooms(): + rooms = await service.list_public_rooms() + for r in rooms: + r["users"] = len(voice_hub.rooms.get(r["id"], {})) + return rooms + + +@router.post("/rooms") +async def create_room( + name: str = Body(...), + public: bool = Body(True), + owner: str = Body(...), +): + return await service.create_room(name, owner, public) + + +@router.post("/rooms/join") +async def join_private(code: str = Body(...)): + return await service.join_by_code(code) diff --git a/app/api/voice_ws.py b/app/api/voice_ws.py index f164be6..8c6cc3a 100644 --- a/app/api/voice_ws.py +++ b/app/api/voice_ws.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query from app.realtime.voice_hub import voice_hub +from app.services.voice_rooms import voice_rooms_collection router = APIRouter() @@ -9,9 +10,17 @@ async def voice_ws( room_id: str = Query(...), username: str = Query(...) ): + room = await voice_rooms_collection.find_one({"id": room_id}) + if not room: + await ws.close(code=4004) + return if username in voice_hub.rooms.get(room_id, {}): await ws.close(code=4001) return + + if len(voice_hub.rooms.get(room_id, {})) >= room.get("max_users", 5): + await ws.close(code=4003) + return await voice_hub.connect(room_id, username, ws) diff --git a/app/services/voice_rooms.py b/app/services/voice_rooms.py new file mode 100644 index 0000000..310ce47 --- /dev/null +++ b/app/services/voice_rooms.py @@ -0,0 +1,67 @@ +from datetime import datetime, timedelta +from uuid import uuid4 +from fastapi import HTTPException +from app.db.database import db + +voice_rooms_collection = db.voice_rooms + + +def _serialize(doc): + if not doc: + return None + doc["_id"] = str(doc["_id"]) + if "created_at" in doc: + doc["created_at"] = doc["created_at"].isoformat() + if "expires_at" in doc and doc["expires_at"]: + doc["expires_at"] = doc["expires_at"].isoformat() + return doc + + +class VoiceRoomService: + + async def list_public_rooms(self): + rooms = await voice_rooms_collection.find( + {"public": True} + ).sort("created_at", -1).to_list(100) + + return [_serialize(r) for r in rooms] + + async def create_room( + self, + name: str, + owner: str, + public: bool, + max_users: int = 5, + ttl_minutes: int | None = None, + ): + room_id = str(uuid4()) + invite_code = None if public else uuid4().hex[:6] + + room = { + "id": room_id, + "name": name, + "public": public, + "invite_code": invite_code, + "owner": owner, + "max_users": max_users, + "created_at": datetime.utcnow(), + "expires_at": ( + datetime.utcnow() + timedelta(minutes=ttl_minutes) + if ttl_minutes else None + ), + } + + await voice_rooms_collection.insert_one(room) + return _serialize(room) + + async def get_room(self, room_id: str): + room = await voice_rooms_collection.find_one({"id": room_id}) + if not room: + raise HTTPException(404, "Room not found") + return room + + async def join_by_code(self, code: str): + room = await voice_rooms_collection.find_one({"invite_code": code}) + if not room: + raise HTTPException(404, "Invalid invite code") + return _serialize(room)