diff --git a/app/router/auth.py b/app/router/auth.py index 0a28af7..9708233 100644 --- a/app/router/auth.py +++ b/app/router/auth.py @@ -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", diff --git a/app/router/lio.py b/app/router/lio.py index c007a06..edfc0c7 100644 --- a/app/router/lio.py +++ b/app/router/lio.py @@ -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)}" - ) + ) \ No newline at end of file