"""SocketIO service for real-time communication.""" import logging from flask import request from flask_socketio import disconnect, emit, join_room, leave_room from app import socketio from app.services.decorators import require_credits logger = logging.getLogger(__name__) class SocketIOService: """Service for managing SocketIO connections and user rooms.""" @staticmethod def get_user_room(user_id: int) -> str: """Get the room name for a specific user.""" return f"user_{user_id}" @staticmethod def emit_to_user(user_id: int, event: str, data: dict) -> None: """Emit an event to a specific user's room.""" room = SocketIOService.get_user_room(user_id) socketio.emit(event, data, room=room) logger.debug(f"Emitted {event} to user {user_id} in room {room}") @staticmethod def emit_to_all(event: str, data: dict) -> None: """Emit an event to all connected clients.""" try: socketio.emit(event, data) logger.info( f"Successfully emitted {event} to all clients with data keys: {list(data.keys())}" ) except Exception as e: logger.error(f"Failed to emit {event}: {e}") @staticmethod def emit_credits_changed(user_id: int, new_credits: int) -> None: """Emit credits_changed event to a user.""" SocketIOService.emit_to_user( user_id, "credits_changed", {"credits": new_credits}, ) @staticmethod def emit_sound_play_count_changed(sound_id: int, new_play_count: int) -> None: """Emit sound_play_count_changed event to all connected clients.""" SocketIOService.emit_to_all( "sound_play_count_changed", {"sound_id": sound_id, "play_count": new_play_count}, ) @staticmethod def emit_credits_required(user_id: int, credits_needed: int) -> None: """Emit an event when credits are required.""" SocketIOService.emit_to_user( user_id, "credits_required", {"credits_needed": credits_needed}, ) @staticmethod def get_user_from_socketio() -> dict | None: """Get user from SocketIO connection using cookies.""" try: from flask import current_app from flask_jwt_extended import decode_token # Check if we have the access_token cookie access_token = request.cookies.get("access_token_cookie") logger.debug( f"Access token from cookies: {access_token[:20] if access_token else None}..." ) if not access_token: logger.debug("No access token found in cookies") return None # Decode the JWT token manually with current_app.app_context(): try: decoded_token = decode_token(access_token) current_user_id = decoded_token["sub"] logger.debug(f"Decoded user ID: {current_user_id}") if not current_user_id: logger.debug("No user ID in token") return None except Exception as e: logger.debug(f"Token decode error: {e}") return None # Query database for user data from app.models.user import User user = User.query.get(int(current_user_id)) if not user or not user.is_active: logger.debug( f"User not found or inactive: {current_user_id}" ) return None logger.debug(f"Successfully found user: {user.email}") return { "id": str(user.id), "email": user.email, "name": user.name, "role": user.role, "credits": user.credits, } except Exception as e: logger.debug(f"Exception in get_user_from_socketio: {e}") return None @socketio.on("connect") def handle_connect(auth=None): """Handle client connection.""" try: logger.info( f"SocketIO connection established from {request.remote_addr}" ) logger.info(f"Session ID: {request.sid}") except Exception: logger.exception("Error handling SocketIO connection") disconnect() @socketio.on("authenticate") def handle_authenticate(data): """Handle authentication after connection.""" try: user = SocketIOService.get_user_from_socketio() if not user: logger.warning("SocketIO authentication failed - no user found") # emit("auth_error", {"error": "Authentication failed"}) disconnect() return user_id = int(user["id"]) user_room = SocketIOService.get_user_room(user_id) # Join user-specific room join_room(user_room) logger.info(f"User {user_id} authenticated and joined room {user_room}") # Send current credits on authentication SocketIOService.emit_to_user(user_id, "auth_success", {"user": user}) SocketIOService.emit_to_user( user_id, "credits_changed", {"credits": user["credits"]} ) except Exception: logger.exception("Error handling SocketIO authentication") # emit("auth_error", {"error": "Authentication failed"}) disconnect() # @socketio.on("play_sound") # @require_credits(1) # def handle_play_sound(data): # """Handle play_sound event from client.""" # try: # user = SocketIOService.get_user_from_socketio() # if not user: # logger.warning("SocketIO play_sound failed - no authenticated user") # # emit("error", {"message": "Authentication required"}) # return # user_id = int(user["id"]) # sound_id = data.get("soundId") # if not sound_id: # logger.warning("SocketIO play_sound failed - no soundId provided") # SocketIOService.emit_to_user( # user_id, "error", {"message": "Sound ID required"} # ) # return # # Import and use the VLC service to play the sound # from app.services.vlc_service import vlc_service # logger.info(f"User {user_id} playing sound {sound_id} via SocketIO") # # Play the sound using the VLC service # success = vlc_service.play_sound(sound_id, user_id) # if not success: # SocketIOService.emit_to_user( # user_id, # "error", # {"message": f"Failed to play sound {sound_id}"}, # ) # except Exception as e: # logger.exception(f"Error handling play_sound event: {e}") # # emit("error", {"message": "Failed to play sound"}) @socketio.on("disconnect") def handle_disconnect(): """Handle client disconnection.""" try: user = SocketIOService.get_user_from_socketio() if user: user_id = int(user["id"]) user_room = SocketIOService.get_user_room(user_id) leave_room(user_room) logger.info(f"User {user_id} disconnected from SocketIO") except Exception: logger.exception("Error handling SocketIO disconnection") # Export the service instance socketio_service = SocketIOService()