Refactor multiplayer room endpoints and add logging

Refactored the multiplayer room creation and user management endpoints in lio.py for improved modularity and validation. Added helper functions for user, room, and playlist validation, and enhanced error handling. In auth.py, added logging to output generated JWT tokens for better traceability.
This commit is contained in:
咕谷酱
2025-08-23 21:07:15 +08:00
committed by MingxuanGame
parent e0aae80f4b
commit 8e6b462645
2 changed files with 310 additions and 177 deletions

View File

@@ -212,7 +212,7 @@ async def oauth_token(
geoip: GeoIPHelper = Depends(get_geoip_helper),
):
scopes = scope.split(" ")
client = (
await db.exec(
select(OAuthClient).where(
@@ -364,7 +364,8 @@ async def oauth_token(
refresh_token_str,
settings.access_token_expire_minutes * 60,
)
# 打印jwt
logger.info(f"[Auth] Generated JWT for user {user_id}: {access_token}")
return TokenResponse(
access_token=access_token,
token_type="Bearer",
@@ -416,7 +417,8 @@ async def oauth_token(
new_refresh_token,
settings.access_token_expire_minutes * 60,
)
# 打印jwt
logger.info(f"[Auth] Generated JWT for user {token_record.user_id}: {access_token}")
return TokenResponse(
access_token=access_token,
token_type="Bearer",
@@ -482,6 +484,9 @@ async def oauth_token(
settings.access_token_expire_minutes * 60,
)
# 打印jwt
logger.info(f"[Auth] Generated JWT for user {user_id}: {access_token}")
return TokenResponse(
access_token=access_token,
token_type="Bearer",
@@ -525,6 +530,9 @@ async def oauth_token(
settings.access_token_expire_minutes * 60,
)
# 打印生成的令牌
logger.info(f"[Auth] Generated access token for client {client_id}: {access_token}")
return TokenResponse(
access_token=access_token,
token_type="Bearer",

View File

@@ -1,17 +1,20 @@
"""LIO (Legacy IO) router for osu-server-spectator compatibility."""
from __future__ import annotations
import hashlib
import hmac
import json
import time
from typing import Any
from typing import Any, Dict, List
from fastapi import APIRouter, HTTPException, Request, status
from pydantic import BaseModel
from sqlmodel import col
from sqlmodel import col, select
from app.database.lazer_user import User
from app.database.playlists import Playlist as DBPlaylist
from app.database.room import Room
from app.database.room_participated_user import RoomParticipatedUser
from app.dependencies.database import Database
from app.models.multiplayer_hub import PlaylistItem as HubPlaylistItem
from app.models.room import MatchType, QueueMode, RoomStatus
from app.utils import utcnow
router = APIRouter(prefix="/_lio", tags=["LIO"])
@@ -24,23 +27,270 @@ class RoomCreateRequest(BaseModel):
password: str | None = None
match_type: str = "HeadToHead"
queue_mode: str = "HostOnly"
initial_playlist: List[Dict[str, Any]] = []
playlist: List[Dict[str, Any]] = []
def verify_request_signature(request: Request, timestamp: str, body: bytes) -> bool:
"""Verify HMAC signature for shared interop requests."""
# For now, skip signature verification in development
# In production, you should implement proper HMAC verification
"""
Verify HMAC signature for shared interop requests.
Args:
request: FastAPI request object
timestamp: Request timestamp
body: Request body bytes
Returns:
bool: True if signature is valid
Note:
Currently skips verification in development.
In production, implement proper HMAC verification.
"""
# TODO: Implement proper HMAC verification for production
return True
async def _validate_user_exists(db: Database, user_id: int) -> User:
"""Validate that a user exists in the database."""
user_result = await db.execute(select(User).where(User.id == user_id))
user = user_result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
return user
def _parse_room_enums(match_type: str, queue_mode: str) -> tuple[MatchType, QueueMode]:
"""Parse and validate room type enums."""
try:
match_type_enum = MatchType(match_type.lower())
except ValueError:
match_type_enum = MatchType.HEAD_TO_HEAD
try:
queue_mode_enum = QueueMode(queue_mode.lower())
except ValueError:
queue_mode_enum = QueueMode.HOST_ONLY
return match_type_enum, queue_mode_enum
def _coerce_playlist_item(item_data: Dict[str, Any], default_order: int, host_user_id: int) -> Dict[str, Any]:
"""
Normalize playlist item data with default values.
Args:
item_data: Raw playlist item data
default_order: Default playlist order
host_user_id: Host user ID for default owner
Returns:
Dict with normalized playlist item data
"""
# Use host_user_id if owner_id is 0 or not provided
owner_id = item_data.get("owner_id", host_user_id)
if owner_id == 0:
owner_id = host_user_id
return {
"owner_id": owner_id,
"ruleset_id": item_data.get("ruleset_id", 0),
"beatmap_id": item_data.get("beatmap_id"),
"required_mods": item_data.get("required_mods", []),
"allowed_mods": item_data.get("allowed_mods", []),
"expired": bool(item_data.get("expired", False)),
"playlist_order": item_data.get("playlist_order", default_order),
"played_at": item_data.get("played_at", None),
"freestyle": bool(item_data.get("freestyle", True)),
"beatmap_checksum": item_data.get("beatmap_checksum", ""),
"star_rating": item_data.get("star_rating", 0.0),
}
def _validate_playlist_items(items: List[Dict[str, Any]]) -> None:
"""Validate playlist items data."""
if not items:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="At least one playlist item is required to create a room"
)
for idx, item in enumerate(items):
if item["beatmap_id"] is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Playlist item at index {idx} missing beatmap_id"
)
ruleset_id = item["ruleset_id"]
if not isinstance(ruleset_id, int) or not (0 <= ruleset_id <= 3):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Playlist item at index {idx} has invalid ruleset_id {ruleset_id}"
)
async def _create_room(db: Database, room_data: Dict[str, Any]) -> tuple[Room, int]:
"""Create a new multiplayer room."""
host_user_id = room_data.get("user_id")
room_name = room_data.get("name", "Unnamed Room")
password = room_data.get("password")
match_type = room_data.get("match_type", "HeadToHead")
queue_mode = room_data.get("queue_mode", "HostOnly")
if not host_user_id or not isinstance(host_user_id, int):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing or invalid user_id"
)
# Validate host user exists
await _validate_user_exists(db, host_user_id)
# Parse room type enums
match_type_enum, queue_mode_enum = _parse_room_enums(match_type, queue_mode)
# Create room
room = Room(
name=room_name,
host_id=host_user_id,
password=password if password else None,
type=match_type_enum,
queue_mode=queue_mode_enum,
status=RoomStatus.IDLE,
participant_count=1,
auto_skip=False,
auto_start_duration=0,
)
db.add(room)
await db.commit()
await db.refresh(room)
return room, host_user_id
async def _add_playlist_items(db: Database, room_id: int, room_data: Dict[str, Any], host_user_id: int) -> None:
"""Add playlist items to the room."""
initial_playlist = room_data.get("initial_playlist", [])
legacy_playlist = room_data.get("playlist", [])
items_raw: List[Dict[str, Any]] = []
# Process initial playlist
for i, item in enumerate(initial_playlist):
if hasattr(item, "dict"):
item = item.dict()
items_raw.append(_coerce_playlist_item(item, i, host_user_id))
# Process legacy playlist
start_index = len(items_raw)
for j, item in enumerate(legacy_playlist, start=start_index):
items_raw.append(_coerce_playlist_item(item, j, host_user_id))
# Validate playlist items
_validate_playlist_items(items_raw)
# Insert playlist items
for item_data in items_raw:
hub_item = HubPlaylistItem(
id=-1, # Placeholder, will be assigned by add_to_db
owner_id=item_data["owner_id"],
ruleset_id=item_data["ruleset_id"],
expired=item_data["expired"],
playlist_order=item_data["playlist_order"],
played_at=item_data["played_at"],
allowed_mods=item_data["allowed_mods"],
required_mods=item_data["required_mods"],
beatmap_id=item_data["beatmap_id"],
freestyle=item_data["freestyle"],
beatmap_checksum=item_data["beatmap_checksum"],
star_rating=item_data["star_rating"],
)
await DBPlaylist.add_to_db(hub_item, room_id=room_id, session=db)
async def _add_host_as_participant(db: Database, room_id: int, host_user_id: int) -> None:
"""Add the host as a room participant and update participant count."""
participant = RoomParticipatedUser(room_id=room_id, user_id=host_user_id)
db.add(participant)
await _update_room_participant_count(db, room_id)
async def _update_room_participant_count(db: Database, room_id: int) -> None:
"""Update the participant count for a room."""
# Count active participants
active_participants = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
col(RoomParticipatedUser.left_at).is_(None)
)
)
count = len(active_participants.all())
# Update room participant count
room_result = await db.execute(select(Room).where(Room.id == room_id))
room_obj = room_result.scalar_one_or_none()
if room_obj:
room_obj.participant_count = count
async def _verify_room_password(db: Database, room_id: int, provided_password: str | None) -> None:
"""Verify room password if required."""
room_result = await db.execute(
select(Room.password).where(Room.id == room_id)
)
room_password = room_result.scalar_one_or_none()
if room_password is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Room not found"
)
if room_password and provided_password != room_password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid password"
)
async def _add_or_update_participant(db: Database, room_id: int, user_id: int) -> None:
"""Add a user as participant or update existing participation."""
existing_result = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
RoomParticipatedUser.user_id == user_id
)
)
existing = existing_result.scalar_one_or_none()
if existing:
# Rejoin room
existing.left_at = None
existing.joined_at = utcnow()
else:
# New participant
participant = RoomParticipatedUser(room_id=room_id, user_id=user_id)
db.add(participant)
# ===== API ENDPOINTS =====
@router.post("/multiplayer/rooms")
async def create_multiplayer_room(
request: Request,
room_data: dict[str, Any],
room_data: Dict[str, Any],
db: Database,
timestamp: str = "",
) -> dict[str, Any]:
"""Create a new multiplayer room."""
) -> int:
"""Create a new multiplayer room with initial playlist."""
try:
# Verify request signature
body = await request.body()
@@ -50,90 +300,39 @@ async def create_multiplayer_room(
detail="Invalid request signature"
)
# Parse room data
# Parse room data if string
if isinstance(room_data, str):
room_data = json.loads(room_data)
# Extract required fields
host_user_id = room_data.get("user_id")
room_name = room_data.get("name", "Unnamed Room")
password = room_data.get("password")
match_type = room_data.get("match_type", "HeadToHead")
queue_mode = room_data.get("queue_mode", "HostOnly")
print(f"Creating room with data: {room_data}")
if not host_user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing user_id"
)
# Verify that the host user exists
from app.database.lazer_user import User
from sqlmodel import select
user_result = await db.execute(
select(User).where(User.id == host_user_id)
)
host_user = user_result.first()
if not host_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {host_user_id} not found"
)
# Create room in database using SQLModel
from app.database.room import Room
from app.models.room import MatchType, QueueMode, RoomStatus
# Convert string values to enums
try:
match_type_enum = MatchType(match_type.lower())
except ValueError:
match_type_enum = MatchType.HEAD_TO_HEAD
try:
queue_mode_enum = QueueMode(queue_mode.lower())
except ValueError:
queue_mode_enum = QueueMode.HOST_ONLY
# Create new room
room = Room(
name=room_name,
host_id=host_user_id,
password=password if password else None,
type=match_type_enum,
queue_mode=queue_mode_enum,
status=RoomStatus.IDLE,
participant_count=1,
auto_skip=False,
auto_start_duration=0,
)
db.add(room)
await db.commit()
await db.refresh(room)
# Create room
room, host_user_id = await _create_room(db, room_data)
room_id = room.id
# Add host as participant
from app.database.room_participated_user import RoomParticipatedUser
participant = RoomParticipatedUser(
room_id=room_id,
user_id=host_user_id,
)
db.add(participant)
await db.commit()
return {"room_id": str(room_id)}
try:
# Add playlist items
await _add_playlist_items(db, room_id, room_data, host_user_id)
# Add host as participant
await _add_host_as_participant(db, room_id, host_user_id)
await db.commit()
return room_id
except HTTPException:
# Clean up room if playlist creation fails
await db.delete(room)
await db.commit()
raise
except json.JSONDecodeError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON: {str(e)}"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -147,9 +346,9 @@ async def add_user_to_room(
room_id: int,
user_id: int,
db: Database,
user_data: dict[str, Any] | None = None,
user_data: Dict[str, Any] | None = None,
timestamp: str = "",
) -> dict[str, Any]:
) -> Dict[str, Any]:
"""Add a user to a multiplayer room."""
try:
# Verify request signature
@@ -160,78 +359,21 @@ async def add_user_to_room(
detail="Invalid request signature"
)
from app.database.room import Room
from sqlmodel import select
# Verify room password if provided
provided_password = user_data.get("password") if user_data else None
await _verify_room_password(db, room_id, provided_password)
# Check if room exists
result = await db.execute(
select(Room.password, Room.participant_count).where(Room.id == room_id)
)
room_data = result.first()
if not room_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Room not found"
)
password, participant_count = room_data
# Check password if room is password protected
if password and user_data:
provided_password = user_data.get("password")
if provided_password != password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid password"
)
# Add user to room (or update existing participation)
from app.database.room_participated_user import RoomParticipatedUser
from sqlmodel import select
# Add or update participant
await _add_or_update_participant(db, room_id, user_id)
# Check if user already participated
existing_participation = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
RoomParticipatedUser.user_id == user_id
)
)
existing = existing_participation.first()
if existing:
# Update existing participation
existing.left_at = None
existing.joined_at = utcnow()
else:
# Create new participation
participant = RoomParticipatedUser(
room_id=room_id,
user_id=user_id,
)
db.add(participant)
# Update participant count
active_count = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
col(RoomParticipatedUser.left_at).is_(None)
)
)
count = len(active_count.all())
await _update_room_participant_count(db, room_id)
# Update room participant count
room_update = await db.execute(
select(Room).where(Room.id == room_id)
)
room_obj = room_update.first()
if room_obj:
room_obj.participant_count = count
await db.commit()
return {"success": True}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -246,7 +388,7 @@ async def remove_user_from_room(
user_id: int,
db: Database,
timestamp: str = "",
) -> dict[str, Any]:
) -> Dict[str, Any]:
"""Remove a user from a multiplayer room."""
try:
# Verify request signature
@@ -257,11 +399,7 @@ async def remove_user_from_room(
detail="Invalid request signature"
)
from app.database.room import Room
from app.database.room_participated_user import RoomParticipatedUser
from sqlmodel import select
# Remove user from room by setting left_at timestamp
# Mark user as left
result = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
@@ -269,34 +407,21 @@ async def remove_user_from_room(
col(RoomParticipatedUser.left_at).is_(None)
)
)
participation = result.first()
participation = result.scalar_one_or_none()
if participation:
participation.left_at = utcnow()
# Update participant count
active_count = await db.execute(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room_id,
col(RoomParticipatedUser.left_at).is_(None)
)
)
count = len(active_count.all())
await _update_room_participant_count(db, room_id)
# Update room participant count
room_result = await db.execute(
select(Room).where(Room.id == room_id)
)
room_obj = room_result.first()
if room_obj:
room_obj.participant_count = count
await db.commit()
return {"success": True}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to remove user from room: {str(e)}"
)
)