"""Callback management API endpoints.""" import logging import re from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from ..auth import verify_token from ..config import CallbackConfig, settings from ..config_manager import config_manager router = APIRouter(prefix="/api/callbacks", tags=["callbacks"]) logger = logging.getLogger(__name__) class CallbackInfo(BaseModel): """Information about a configured callback.""" name: str command: str timeout: int working_dir: str | None = None shell: bool class CallbackCreateRequest(BaseModel): """Request model for creating or updating a callback.""" command: str = Field(..., description="Command to execute", min_length=1) timeout: int = Field(default=30, description="Execution timeout in seconds", ge=1, le=300) working_dir: str | None = Field(default=None, description="Working directory") shell: bool = Field(default=True, description="Run command in shell") def _validate_callback_name(name: str) -> None: """Validate callback name. Args: name: Callback name to validate. Raises: HTTPException: If name is invalid. """ # All available callback events valid_names = { "on_play", "on_pause", "on_stop", "on_next", "on_previous", "on_volume", "on_mute", "on_seek", "on_turn_on", "on_turn_off", "on_toggle", } if name not in valid_names: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Callback name must be one of: {', '.join(sorted(valid_names))}", ) @router.get("/list") async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]: """List all configured callbacks. Returns: List of configured callbacks. """ return [ CallbackInfo( name=name, command=config.command, timeout=config.timeout, working_dir=config.working_dir, shell=config.shell, ) for name, config in settings.callbacks.items() ] @router.post("/create/{callback_name}") async def create_callback( callback_name: str, request: CallbackCreateRequest, _: str = Depends(verify_token), ) -> dict[str, Any]: """Create a new callback. Args: callback_name: Callback event name (on_turn_on, on_turn_off, on_toggle). request: Callback configuration. Returns: Success response with callback name. Raises: HTTPException: If callback already exists or name is invalid. """ # Validate name _validate_callback_name(callback_name) # Check if callback already exists if callback_name in settings.callbacks: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Callback '{callback_name}' already exists. Use PUT /api/callbacks/update/{callback_name} to update it.", ) # Create callback config callback_config = CallbackConfig(**request.model_dump()) # Add to config file and in-memory try: config_manager.add_callback(callback_name, callback_config) except Exception as e: logger.error(f"Failed to add callback '{callback_name}': {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to add callback: {str(e)}", ) logger.info(f"Callback '{callback_name}' created successfully") return {"success": True, "callback": callback_name} @router.put("/update/{callback_name}") async def update_callback( callback_name: str, request: CallbackCreateRequest, _: str = Depends(verify_token), ) -> dict[str, Any]: """Update an existing callback. Args: callback_name: Callback event name. request: Updated callback configuration. Returns: Success response with callback name. Raises: HTTPException: If callback does not exist. """ # Validate name _validate_callback_name(callback_name) # Check if callback exists if callback_name not in settings.callbacks: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Callback '{callback_name}' not found. Use POST /api/callbacks/create/{callback_name} to create it.", ) # Create updated callback config callback_config = CallbackConfig(**request.model_dump()) # Update config file and in-memory try: config_manager.update_callback(callback_name, callback_config) except Exception as e: logger.error(f"Failed to update callback '{callback_name}': {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update callback: {str(e)}", ) logger.info(f"Callback '{callback_name}' updated successfully") return {"success": True, "callback": callback_name} @router.delete("/delete/{callback_name}") async def delete_callback( callback_name: str, _: str = Depends(verify_token), ) -> dict[str, Any]: """Delete a callback. Args: callback_name: Callback event name. Returns: Success response with callback name. Raises: HTTPException: If callback does not exist. """ # Validate name _validate_callback_name(callback_name) # Check if callback exists if callback_name not in settings.callbacks: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Callback '{callback_name}' not found", ) # Delete from config file and in-memory try: config_manager.delete_callback(callback_name) except Exception as e: logger.error(f"Failed to delete callback '{callback_name}': {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete callback: {str(e)}", ) logger.info(f"Callback '{callback_name}' deleted successfully") return {"success": True, "callback": callback_name}