# api.py from fastapi import ( FastAPI, Query, Request, UploadFile, File, Form ) from typing import List from pydantic import BaseModel import globals from server_manager import server_manager from utils.conversation_history import conversation_history from commands.actions import ( force_sleep, wake_up, set_mood, reset_mood, check_mood, calm_miku, reset_conversation, send_bedtime_now ) from utils.autonomous import ( miku_autonomous_tick, miku_say_something_general, miku_engage_random_user, share_miku_tweet, handle_custom_prompt ) import asyncio import nest_asyncio import subprocess import io import discord import aiofiles from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, PlainTextResponse import os import json from utils.figurine_notifier import ( load_subscribers as figurine_load_subscribers, add_subscriber as figurine_add_subscriber, remove_subscriber as figurine_remove_subscriber, send_figurine_dm_to_all_subscribers, send_figurine_dm_to_single_user ) from utils.dm_logger import dm_logger nest_asyncio.apply() app = FastAPI() # Serve static folder app.mount("/static", StaticFiles(directory="static"), name="static") # ========== Models ========== class MoodSetRequest(BaseModel): mood: str class ConversationResetRequest(BaseModel): user_id: str class CustomPromptRequest(BaseModel): prompt: str class ServerConfigRequest(BaseModel): guild_id: int guild_name: str autonomous_channel_id: int autonomous_channel_name: str bedtime_channel_ids: List[int] = None enabled_features: List[str] = None # ========== Routes ========== @app.get("/") def read_index(): return FileResponse("static/index.html") @app.get("/logs") def get_logs(): try: # Read last 100 lines of the log file with open("/app/bot.log", "r", encoding="utf-8") as f: lines = f.readlines() last_100 = lines[-100:] if len(lines) >= 100 else lines return "".join(last_100) except Exception as e: return f"Error reading log file: {e}" @app.get("/prompt") def get_last_prompt(): return {"prompt": globals.LAST_FULL_PROMPT or "No prompt has been issued yet."} @app.get("/mood") def get_current_mood(): return {"mood": globals.DM_MOOD, "description": globals.DM_MOOD_DESCRIPTION} @app.post("/mood") async def set_mood_endpoint(data: MoodSetRequest): # This endpoint now operates on DM_MOOD from utils.moods import MOOD_EMOJIS if data.mood not in MOOD_EMOJIS: return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"} # Update DM mood (DMs don't have nicknames, so no nickname update needed) globals.DM_MOOD = data.mood from utils.moods import load_mood_description globals.DM_MOOD_DESCRIPTION = load_mood_description(data.mood) return {"status": "ok", "new_mood": data.mood} @app.post("/mood/reset") async def reset_mood_endpoint(): # Reset DM mood to neutral (DMs don't have nicknames, so no nickname update needed) globals.DM_MOOD = "neutral" from utils.moods import load_mood_description globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral") return {"status": "ok", "new_mood": "neutral"} @app.post("/mood/calm") def calm_miku_endpoint(): # Calm DM mood to neutral (DMs don't have nicknames, so no nickname update needed) globals.DM_MOOD = "neutral" from utils.moods import load_mood_description globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral") return {"status": "ok", "message": "Miku has been calmed down"} # ========== Per-Server Mood Management ========== @app.get("/servers/{guild_id}/mood") def get_server_mood(guild_id: int): """Get current mood for a specific server""" mood_name, mood_description = server_manager.get_server_mood(guild_id) return { "guild_id": guild_id, "mood": mood_name, "description": mood_description } @app.post("/servers/{guild_id}/mood") async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest): """Set mood for a specific server""" # Check if server exists if guild_id not in server_manager.servers: print(f"๐ŸŽญ API: Server {guild_id} not found in server_manager.servers") return {"status": "error", "message": "Server not found"} # Check if mood is valid from utils.moods import MOOD_EMOJIS if data.mood not in MOOD_EMOJIS: print(f"๐ŸŽญ API: Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}") return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"} success = server_manager.set_server_mood(guild_id, data.mood) print(f"๐ŸŽญ API: Server mood set result: {success}") if success: # V2: Notify autonomous engine of mood change try: from utils.autonomous import on_mood_change on_mood_change(guild_id, data.mood) except Exception as e: print(f"โš ๏ธ API: Failed to notify autonomous engine of mood change: {e}") # Update the nickname for this server from utils.moods import update_server_nickname print(f"๐ŸŽญ API: Updating nickname for server {guild_id}") globals.client.loop.create_task(update_server_nickname(guild_id)) return {"status": "ok", "new_mood": data.mood, "guild_id": guild_id} print(f"๐ŸŽญ API: set_server_mood returned False for unknown reason") return {"status": "error", "message": "Failed to set server mood"} @app.post("/servers/{guild_id}/mood/reset") async def reset_server_mood_endpoint(guild_id: int): """Reset mood to neutral for a specific server""" print(f"๐ŸŽญ API: Resetting mood for server {guild_id} to neutral") # Check if server exists if guild_id not in server_manager.servers: print(f"๐ŸŽญ API: Server {guild_id} not found in server_manager.servers") return {"status": "error", "message": "Server not found"} print(f"๐ŸŽญ API: Server validation passed, calling set_server_mood") success = server_manager.set_server_mood(guild_id, "neutral") print(f"๐ŸŽญ API: Server mood reset result: {success}") if success: # V2: Notify autonomous engine of mood change try: from utils.autonomous import on_mood_change on_mood_change(guild_id, "neutral") except Exception as e: print(f"โš ๏ธ API: Failed to notify autonomous engine of mood reset: {e}") # Update the nickname for this server from utils.moods import update_server_nickname print(f"๐ŸŽญ API: Updating nickname for server {guild_id}") globals.client.loop.create_task(update_server_nickname(guild_id)) return {"status": "ok", "new_mood": "neutral", "guild_id": guild_id} print(f"๐ŸŽญ API: set_server_mood returned False for unknown reason") return {"status": "error", "message": "Failed to reset server mood"} @app.get("/servers/{guild_id}/mood/state") def get_server_mood_state(guild_id: int): """Get complete mood state for a specific server""" mood_state = server_manager.get_server_mood_state(guild_id) if mood_state: return {"status": "ok", "guild_id": guild_id, "mood_state": mood_state} return {"status": "error", "message": "Server not found"} @app.post("/conversation/reset") def reset_convo(data: ConversationResetRequest): reset_conversation(data.user_id) return {"status": "ok", "message": "Conversation reset"} @app.post("/sleep") async def force_sleep_endpoint(): await force_sleep() return {"status": "ok", "message": "Miku is now sleeping"} @app.post("/wake") async def wake_up_endpoint(): await wake_up() return {"status": "ok", "message": "Miku is now awake"} @app.post("/bedtime") async def bedtime_endpoint(guild_id: int = None): # If guild_id is provided, send bedtime reminder only to that server # If no guild_id, send to all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Send to specific server only from utils.scheduled import send_bedtime_reminder_for_server globals.client.loop.create_task(send_bedtime_reminder_for_server(guild_id, globals.client)) return {"status": "ok", "message": f"Bedtime reminder queued for server {guild_id}"} else: # Send to all servers (legacy behavior) from utils.scheduled import send_bedtime_now globals.client.loop.create_task(send_bedtime_now()) return {"status": "ok", "message": "Bedtime reminder queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/autonomous/general") async def trigger_autonomous_general(guild_id: int = None): # If guild_id is provided, send autonomous message only to that server # If no guild_id, send to all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Send to specific server only from utils.autonomous import miku_say_something_general_for_server globals.client.loop.create_task(miku_say_something_general_for_server(guild_id)) return {"status": "ok", "message": f"Autonomous general message queued for server {guild_id}"} else: # Send to all servers (legacy behavior) from utils.autonomous import miku_say_something_general globals.client.loop.create_task(miku_say_something_general()) return {"status": "ok", "message": "Autonomous general message queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/autonomous/engage") async def trigger_autonomous_engage_user(guild_id: int = None): # If guild_id is provided, send autonomous engagement only to that server # If no guild_id, send to all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Send to specific server only from utils.autonomous import miku_engage_random_user_for_server globals.client.loop.create_task(miku_engage_random_user_for_server(guild_id)) return {"status": "ok", "message": f"Autonomous user engagement queued for server {guild_id}"} else: # Send to all servers (legacy behavior) from utils.autonomous import miku_engage_random_user globals.client.loop.create_task(miku_engage_random_user()) return {"status": "ok", "message": "Autonomous user engagement queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/autonomous/tweet") async def trigger_autonomous_tweet(guild_id: int = None): # If guild_id is provided, send tweet only to that server # If no guild_id, send to all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Send to specific server only from utils.autonomous import share_miku_tweet_for_server globals.client.loop.create_task(share_miku_tweet_for_server(guild_id)) return {"status": "ok", "message": f"Autonomous tweet sharing queued for server {guild_id}"} else: # Send to all servers (legacy behavior) from utils.autonomous import share_miku_tweet globals.client.loop.create_task(share_miku_tweet()) return {"status": "ok", "message": "Autonomous tweet sharing queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/autonomous/custom") async def custom_autonomous_message(req: CustomPromptRequest, guild_id: int = None): # If guild_id is provided, send custom prompt only to that server # If no guild_id, send to all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Send to specific server only from utils.autonomous import handle_custom_prompt_for_server # Use create_task to avoid timeout context manager error globals.client.loop.create_task(handle_custom_prompt_for_server(guild_id, req.prompt)) return {"status": "ok", "message": f"Custom autonomous message queued for server {guild_id}"} else: # Send to all servers (legacy behavior) from utils.autonomous import handle_custom_prompt # Use create_task to avoid timeout context manager error globals.client.loop.create_task(handle_custom_prompt(req.prompt)) return {"status": "ok", "message": "Custom autonomous message queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/autonomous/reaction") async def trigger_autonomous_reaction(guild_id: int = None): # If guild_id is provided, trigger reaction only for that server # If no guild_id, trigger for all servers (legacy behavior) if globals.client and globals.client.loop and globals.client.loop.is_running(): if guild_id is not None: # Trigger for specific server only (force=True bypasses 50% chance) from utils.autonomous import miku_autonomous_reaction_for_server globals.client.loop.create_task(miku_autonomous_reaction_for_server(guild_id, force=True)) return {"status": "ok", "message": f"Autonomous reaction queued for server {guild_id}"} else: # Trigger for all servers (legacy behavior, force=True bypasses 50% chance) from utils.autonomous import miku_autonomous_reaction globals.client.loop.create_task(miku_autonomous_reaction(force=True)) return {"status": "ok", "message": "Autonomous reaction queued for all servers"} else: return {"status": "error", "message": "Bot not ready"} @app.post("/profile-picture/change") async def trigger_profile_picture_change( guild_id: int = None, file: UploadFile = File(None) ): """ Change Miku's profile picture. If a file is provided, use it. Otherwise, search Danbooru. """ if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): return {"status": "error", "message": "Bot not ready"} try: from utils.profile_picture_manager import profile_picture_manager from server_manager import server_manager # Get mood from guild_id (if provided) mood = None if guild_id is not None: mood, _ = server_manager.get_server_mood(guild_id) else: # Use DM mood as fallback mood = globals.DM_MOOD # If file provided, use it custom_image_bytes = None if file: custom_image_bytes = await file.read() print(f"๐Ÿ–ผ๏ธ Received custom image upload ({len(custom_image_bytes)} bytes)") # Change profile picture result = await profile_picture_manager.change_profile_picture( mood=mood, custom_image_bytes=custom_image_bytes, debug=True ) if result["success"]: return { "status": "ok", "message": "Profile picture changed successfully", "source": result["source"], "metadata": result.get("metadata", {}) } else: return { "status": "error", "message": result.get("error", "Unknown error"), "source": result["source"] } except Exception as e: print(f"โš ๏ธ Error in profile picture API: {e}") import traceback traceback.print_exc() return {"status": "error", "message": f"Unexpected error: {str(e)}"} @app.get("/profile-picture/metadata") async def get_profile_picture_metadata(): """Get metadata about the current profile picture""" try: from utils.profile_picture_manager import profile_picture_manager metadata = profile_picture_manager.load_metadata() if metadata: return {"status": "ok", "metadata": metadata} else: return {"status": "ok", "metadata": None, "message": "No metadata found"} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/profile-picture/restore-fallback") async def restore_fallback_profile_picture(): """Restore the original fallback profile picture""" if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): return {"status": "error", "message": "Bot not ready"} try: from utils.profile_picture_manager import profile_picture_manager success = await profile_picture_manager.restore_fallback() if success: return {"status": "ok", "message": "Fallback profile picture restored"} else: return {"status": "error", "message": "Failed to restore fallback"} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/role-color/custom") async def set_custom_role_color(hex_color: str = Form(...)): """Set a custom role color across all servers""" if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): return {"status": "error", "message": "Bot not ready"} try: from utils.profile_picture_manager import profile_picture_manager result = await profile_picture_manager.set_custom_role_color(hex_color, debug=True) if result["success"]: return { "status": "ok", "message": f"Role color updated to {result['color']['hex']}", "color": result["color"] } else: return {"status": "error", "message": result.get("error", "Unknown error")} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/role-color/reset-fallback") async def reset_role_color_to_fallback(): """Reset role color to fallback (#86cecb)""" if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): return {"status": "error", "message": "Bot not ready"} try: from utils.profile_picture_manager import profile_picture_manager result = await profile_picture_manager.reset_to_fallback_color(debug=True) if result["success"]: return { "status": "ok", "message": f"Role color reset to fallback {result['color']['hex']}", "color": result["color"] } else: return {"status": "error", "message": "Failed to reset color"} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/manual/send") async def manual_send( message: str = Form(...), channel_id: str = Form(...), files: List[UploadFile] = File(default=[]) ): try: channel = globals.client.get_channel(int(channel_id)) if not channel: return {"status": "error", "message": "Channel not found"} # Read file content immediately before the request closes file_data = [] for file in files: try: file_content = await file.read() file_data.append({ 'filename': file.filename, 'content': file_content }) except Exception as e: print(f"โŒ Failed to read file {file.filename}: {e}") return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"} # Use create_task to avoid timeout context manager error async def send_message_and_files(): try: # Send the main message if message.strip(): await channel.send(message) print(f"โœ… Manual message sent to #{channel.name}") # Send files if any for file_info in file_data: try: await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename'])) print(f"โœ… File {file_info['filename']} sent to #{channel.name}") except Exception as e: print(f"โŒ Failed to send file {file_info['filename']}: {e}") except Exception as e: print(f"โŒ Failed to send message: {e}") globals.client.loop.create_task(send_message_and_files()) return {"status": "ok", "message": "Message and files queued for sending"} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.get("/status") def status(): # Get per-server mood summary server_moods = {} for guild_id in server_manager.servers: mood_name, _ = server_manager.get_server_mood(guild_id) server_moods[str(guild_id)] = mood_name return { "status": "online", "mood": globals.DM_MOOD, "servers": len(server_manager.servers), "active_schedulers": len(server_manager.schedulers), "server_moods": server_moods } @app.get("/autonomous/stats") def get_autonomous_stats(): """Get autonomous engine stats for all servers""" from utils.autonomous import autonomous_engine stats = {} for guild_id in server_manager.servers: server_info = server_manager.servers[guild_id] mood_name, _ = server_manager.get_server_mood(guild_id) # Get context signals for this server if guild_id in autonomous_engine.server_contexts: ctx = autonomous_engine.server_contexts[guild_id] # Get mood profile mood_profile = autonomous_engine.mood_profiles.get(mood_name, { "energy": 0.5, "sociability": 0.5, "impulsiveness": 0.5 }) # Sanitize float values for JSON serialization (replace inf with large number) time_since_action = ctx.time_since_last_action if time_since_action == float('inf'): time_since_action = 999999 time_since_interaction = ctx.time_since_last_interaction if time_since_interaction == float('inf'): time_since_interaction = 999999 stats[str(guild_id)] = { "guild_name": server_info.guild_name, "mood": mood_name, "mood_profile": mood_profile, "context": { "messages_last_5min": ctx.messages_last_5min, "messages_last_hour": ctx.messages_last_hour, "unique_users_active": ctx.unique_users_active, "conversation_momentum": round(ctx.conversation_momentum, 2), "users_joined_recently": ctx.users_joined_recently, "users_status_changed": ctx.users_status_changed, "users_started_activity": ctx.users_started_activity, "time_since_last_action": round(time_since_action, 1), "time_since_last_interaction": round(time_since_interaction, 1), "messages_since_last_appearance": ctx.messages_since_last_appearance, "hour_of_day": ctx.hour_of_day, "is_weekend": ctx.is_weekend, "mood_energy_level": round(ctx.mood_energy_level, 2) } } else: # Server not yet initialized in autonomous engine mood_profile = autonomous_engine.mood_profiles.get(mood_name, { "energy": 0.5, "sociability": 0.5, "impulsiveness": 0.5 }) stats[str(guild_id)] = { "guild_name": server_info.guild_name, "mood": mood_name, "mood_profile": mood_profile, "context": None } return {"servers": stats} @app.get("/conversation/{user_id}") def get_conversation(user_id: str): if user_id in globals.conversation_history: return {"conversation": list(globals.conversation_history[user_id])} return {"conversation": []} # ========== Figurine DM Subscription APIs ========== @app.get("/figurines/subscribers") async def get_figurine_subscribers(): subs = figurine_load_subscribers() return {"subscribers": [str(uid) for uid in subs]} @app.post("/figurines/subscribers") async def add_figurine_subscriber(user_id: str = Form(...)): try: uid = int(user_id) ok = figurine_add_subscriber(uid) return {"status": "ok", "added": ok} except Exception as e: return {"status": "error", "message": str(e)} @app.delete("/figurines/subscribers/{user_id}") async def delete_figurine_subscriber(user_id: str): try: uid = int(user_id) ok = figurine_remove_subscriber(uid) return {"status": "ok", "removed": ok} except Exception as e: return {"status": "error", "message": str(e)} @app.post("/figurines/send_now") async def figurines_send_now(tweet_url: str = Form(None)): """Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL""" if globals.client and globals.client.loop and globals.client.loop.is_running(): print(f"๐Ÿš€ API: Sending figurine DMs to all subscribers, tweet_url: {tweet_url}") globals.client.loop.create_task(send_figurine_dm_to_all_subscribers(globals.client, tweet_url=tweet_url)) return {"status": "ok", "message": "Figurine DMs queued"} return {"status": "error", "message": "Bot not ready"} @app.post("/figurines/send_to_user") async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)): """Send figurine DM to a specific user, optionally with specific tweet URL""" print(f"๐ŸŽฏ API: Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'") if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): print("โŒ API: Bot not ready") return {"status": "error", "message": "Bot not ready"} try: user_id_int = int(user_id) print(f"โœ… API: Parsed user_id as {user_id_int}") except ValueError: print(f"โŒ API: Invalid user ID: '{user_id}'") return {"status": "error", "message": "Invalid user ID"} # Clean up tweet URL if it's empty string if tweet_url == "": tweet_url = None print(f"๐ŸŽฏ API: Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}") # Queue the DM send task in the bot's event loop globals.client.loop.create_task(send_figurine_dm_to_single_user(globals.client, user_id_int, tweet_url=tweet_url)) return {"status": "ok", "message": f"Figurine DM to user {user_id} queued"} # ========== Server Management Endpoints ========== @app.get("/servers") def get_servers(): """Get all configured servers""" print(f"๐ŸŽญ API: /servers endpoint called") print(f"๐ŸŽญ API: server_manager.servers keys: {list(server_manager.servers.keys())}") print(f"๐ŸŽญ API: server_manager.servers count: {len(server_manager.servers)}") # Debug: Check config file directly config_file = server_manager.config_file print(f"๐ŸŽญ API: Config file path: {config_file}") if os.path.exists(config_file): try: with open(config_file, "r", encoding="utf-8") as f: config_data = json.load(f) print(f"๐ŸŽญ API: Config file contains: {list(config_data.keys())}") except Exception as e: print(f"๐ŸŽญ API: Failed to read config file: {e}") else: print(f"๐ŸŽญ API: Config file does not exist") servers = [] for server in server_manager.get_all_servers(): server_data = server.to_dict() # Convert set to list for JSON serialization server_data['enabled_features'] = list(server_data['enabled_features']) # Convert guild_id to string to prevent JavaScript integer precision loss server_data['guild_id'] = str(server_data['guild_id']) servers.append(server_data) print(f"๐ŸŽญ API: Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}") print(f"๐ŸŽญ API: Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}") print(f"๐ŸŽญ API: Returning {len(servers)} servers") # Debug: Show exact JSON being sent import json response_data = {"servers": servers} return {"servers": servers} @app.post("/servers") def add_server(data: ServerConfigRequest): """Add a new server configuration""" enabled_features = set(data.enabled_features) if data.enabled_features else None success = server_manager.add_server( guild_id=data.guild_id, guild_name=data.guild_name, autonomous_channel_id=data.autonomous_channel_id, autonomous_channel_name=data.autonomous_channel_name, bedtime_channel_ids=data.bedtime_channel_ids, enabled_features=enabled_features ) if success: # Restart schedulers to include the new server server_manager.stop_all_schedulers() server_manager.start_all_schedulers(globals.client) return {"status": "ok", "message": f"Server {data.guild_name} added successfully"} else: return {"status": "error", "message": "Failed to add server"} @app.delete("/servers/{guild_id}") def remove_server(guild_id: int): """Remove a server configuration""" success = server_manager.remove_server(guild_id) if success: return {"status": "ok", "message": "Server removed successfully"} else: return {"status": "error", "message": "Failed to remove server"} @app.put("/servers/{guild_id}") def update_server(guild_id: int, data: dict): """Update server configuration""" success = server_manager.update_server_config(guild_id, **data) if success: # Restart schedulers to apply changes server_manager.stop_all_schedulers() server_manager.start_all_schedulers(globals.client) return {"status": "ok", "message": "Server configuration updated"} else: return {"status": "error", "message": "Failed to update server configuration"} @app.post("/servers/{guild_id}/bedtime-range") def update_server_bedtime_range(guild_id: int, data: dict): """Update server bedtime range configuration""" print(f"โฐ API: Updating bedtime range for server {guild_id}: {data}") # Validate the data required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end'] for field in required_fields: if field not in data: return {"status": "error", "message": f"Missing required field: {field}"} # Validate time ranges try: bedtime_hour = int(data['bedtime_hour']) bedtime_minute = int(data['bedtime_minute']) bedtime_hour_end = int(data['bedtime_hour_end']) bedtime_minute_end = int(data['bedtime_minute_end']) # Basic validation if not (0 <= bedtime_hour <= 23) or not (0 <= bedtime_hour_end <= 23): return {"status": "error", "message": "Hours must be between 0 and 23"} if not (0 <= bedtime_minute <= 59) or not (0 <= bedtime_minute_end <= 59): return {"status": "error", "message": "Minutes must be between 0 and 59"} except (ValueError, TypeError): return {"status": "error", "message": "Invalid time values provided"} # Update the server configuration success = server_manager.update_server_config(guild_id, **data) if success: # Update just the bedtime job for this server (avoid restarting all schedulers) job_success = server_manager.update_server_bedtime_job(guild_id, globals.client) if job_success: print(f"โœ… API: Bedtime range updated for server {guild_id}") return { "status": "ok", "message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}" } else: return {"status": "error", "message": "Updated config but failed to update scheduler"} else: return {"status": "error", "message": "Failed to update bedtime range"} @app.post("/servers/{guild_id}/autonomous/general") async def trigger_autonomous_general_for_server(guild_id: int): """Trigger autonomous general message for a specific server""" from utils.autonomous import miku_say_something_general_for_server try: await miku_say_something_general_for_server(guild_id) return {"status": "ok", "message": f"Autonomous general message triggered for server {guild_id}"} except Exception as e: return {"status": "error", "message": f"Failed to trigger autonomous message: {e}"} @app.post("/servers/{guild_id}/autonomous/engage") async def trigger_autonomous_engage_for_server(guild_id: int): """Trigger autonomous user engagement for a specific server""" from utils.autonomous import miku_engage_random_user_for_server try: await miku_engage_random_user_for_server(guild_id) return {"status": "ok", "message": f"Autonomous user engagement triggered for server {guild_id}"} except Exception as e: return {"status": "error", "message": f"Failed to trigger user engagement: {e}"} @app.post("/servers/{guild_id}/autonomous/custom") async def custom_autonomous_message_for_server(guild_id: int, req: CustomPromptRequest): """Send custom autonomous message to a specific server""" from utils.autonomous import handle_custom_prompt_for_server try: success = await handle_custom_prompt_for_server(guild_id, req.prompt) if success: return {"status": "ok", "message": f"Custom autonomous message sent to server {guild_id}"} else: return {"status": "error", "message": f"Failed to send custom message to server {guild_id}"} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.post("/dm/{user_id}/custom") async def send_custom_prompt_dm(user_id: str, req: CustomPromptRequest): """Send custom prompt via DM to a specific user""" try: user_id_int = int(user_id) user = globals.client.get_user(user_id_int) if not user: return {"status": "error", "message": f"User {user_id} not found"} # Use the LLM query function for DM context from utils.llm import query_ollama async def send_dm_custom_prompt(): try: response = await query_ollama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response") await user.send(response) print(f"โœ… Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...") # Log to DM history from utils.dm_logger import dm_logger dm_logger.log_conversation(user_id, req.prompt, response) except Exception as e: print(f"โŒ Failed to send custom DM prompt to user {user_id}: {e}") # Use create_task to avoid timeout context manager error globals.client.loop.create_task(send_dm_custom_prompt()) return {"status": "ok", "message": f"Custom DM prompt queued for user {user_id}"} except ValueError: return {"status": "error", "message": "Invalid user ID format"} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.post("/dm/{user_id}/manual") async def send_manual_message_dm( user_id: str, message: str = Form(...), files: List[UploadFile] = File(default=[]) ): """Send manual message via DM to a specific user""" try: user_id_int = int(user_id) user = globals.client.get_user(user_id_int) if not user: return {"status": "error", "message": f"User {user_id} not found"} # Read file content immediately before the request closes file_data = [] for file in files: try: file_content = await file.read() file_data.append({ 'filename': file.filename, 'content': file_content }) except Exception as e: print(f"โŒ Failed to read file {file.filename}: {e}") return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"} async def send_dm_message_and_files(): try: # Send the main message if message.strip(): await user.send(message) print(f"โœ… Manual DM message sent to user {user_id}") # Send files if any for file_info in file_data: try: await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename'])) print(f"โœ… File {file_info['filename']} sent via DM to user {user_id}") except Exception as e: print(f"โŒ Failed to send file {file_info['filename']} via DM: {e}") # Log to DM history (user message = manual override trigger, miku response = the message sent) from utils.dm_logger import dm_logger dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data]) except Exception as e: print(f"โŒ Failed to send manual DM to user {user_id}: {e}") # Use create_task to avoid timeout context manager error globals.client.loop.create_task(send_dm_message_and_files()) return {"status": "ok", "message": f"Manual DM message queued for user {user_id}"} except ValueError: return {"status": "error", "message": "Invalid user ID format"} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.post("/image/generate") async def manual_image_generation(req: dict): """Manually trigger image generation for testing""" try: prompt = req.get("prompt", "").strip() if not prompt: return {"status": "error", "message": "Prompt is required"} from utils.image_generation import generate_image_with_comfyui image_path = await generate_image_with_comfyui(prompt) if image_path: return {"status": "ok", "message": f"Image generated successfully", "image_path": image_path} else: return {"status": "error", "message": "Failed to generate image"} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.get("/image/status") async def get_image_generation_status(): """Get status of image generation system""" try: from utils.image_generation import check_comfyui_status status = await check_comfyui_status() return {"status": "ok", **status} except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.post("/image/test-detection") async def test_image_detection(req: dict): """Test the natural language image detection system""" try: message = req.get("message", "").strip() if not message: return {"status": "error", "message": "Message is required"} from utils.image_generation import detect_image_request is_image_request, extracted_prompt = await detect_image_request(message) return { "status": "ok", "is_image_request": is_image_request, "extracted_prompt": extracted_prompt, "original_message": message } except Exception as e: return {"status": "error", "message": f"Error: {e}"} @app.post("/servers/{guild_id}/autonomous/tweet") async def trigger_autonomous_tweet_for_server(guild_id: int): """Trigger autonomous tweet sharing for a specific server""" from utils.autonomous import share_miku_tweet_for_server try: await share_miku_tweet_for_server(guild_id) return {"status": "ok", "message": f"Autonomous tweet sharing triggered for server {guild_id}"} except Exception as e: return {"status": "error", "message": f"Failed to trigger tweet sharing: {e}"} @app.get("/servers/{guild_id}/memory") def get_server_memory(guild_id: int, key: str = None): """Get server-specific memory""" memory = server_manager.get_server_memory(guild_id, key) return {"guild_id": guild_id, "key": key, "memory": memory} @app.post("/servers/{guild_id}/memory") def set_server_memory(guild_id: int, key: str, value): """Set server-specific memory""" server_manager.set_server_memory(guild_id, key, value) return {"status": "ok", "message": f"Memory set for server {guild_id}"} @app.post("/servers/repair") def repair_server_config(): """Repair corrupted server configuration""" try: server_manager.repair_config() return {"status": "ok", "message": "Server configuration repaired and saved"} except Exception as e: return {"status": "error", "message": f"Failed to repair configuration: {e}"} @app.get("/moods/available") def get_available_moods(): """Get list of all available moods""" from utils.moods import MOOD_EMOJIS return {"moods": list(MOOD_EMOJIS.keys())} @app.post("/test/mood/{guild_id}") async def test_mood_change(guild_id: int, data: MoodSetRequest): """Test endpoint for debugging mood changes""" print(f"๐Ÿงช TEST: Testing mood change for server {guild_id} to {data.mood}") # Check if server exists if guild_id not in server_manager.servers: return {"status": "error", "message": f"Server {guild_id} not found"} server_config = server_manager.get_server_config(guild_id) print(f"๐Ÿงช TEST: Server config found: {server_config.guild_name if server_config else 'None'}") # Try to set mood success = server_manager.set_server_mood(guild_id, data.mood) print(f"๐Ÿงช TEST: Mood set result: {success}") if success: # V2: Notify autonomous engine of mood change try: from utils.autonomous import on_mood_change on_mood_change(guild_id, data.mood) print(f"๐Ÿงช TEST: Notified autonomous engine of mood change") except Exception as e: print(f"โš ๏ธ TEST: Failed to notify autonomous engine: {e}") # Try to update nickname from utils.moods import update_server_nickname print(f"๐Ÿงช TEST: Attempting nickname update...") try: await update_server_nickname(guild_id) print(f"๐Ÿงช TEST: Nickname update completed") except Exception as e: print(f"๐Ÿงช TEST: Nickname update failed: {e}") import traceback traceback.print_exc() return {"status": "ok", "message": f"Test mood change completed", "success": success} return {"status": "error", "message": "Mood change failed"} # ========== DM Logging Endpoints ========== @app.get("/dms/users") def get_dm_users(): """Get summary of all users who have DMed the bot""" try: from utils.dm_logger import dm_logger users = dm_logger.get_all_dm_users() return {"status": "ok", "users": users} except Exception as e: return {"status": "error", "message": f"Failed to get DM users: {e}"} @app.get("/dms/users/{user_id}") def get_dm_user_conversation(user_id: str): """Get conversation summary for a specific user""" try: from utils.dm_logger import dm_logger # Convert string user_id to int for internal processing user_id_int = int(user_id) summary = dm_logger.get_user_conversation_summary(user_id_int) return {"status": "ok", "summary": summary} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: return {"status": "error", "message": f"Failed to get user conversation: {e}"} @app.get("/dms/users/{user_id}/conversations") def get_dm_conversations(user_id: str, limit: int = 50): """Get recent conversations with a specific user""" try: from utils.dm_logger import dm_logger # Convert string user_id to int for internal processing user_id_int = int(user_id) print(f"๐Ÿ” API: Loading conversations for user {user_id_int}, limit: {limit}") logs = dm_logger._load_user_logs(user_id_int) print(f"๐Ÿ” API: Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations") conversations = logs["conversations"][-limit:] if limit > 0 else logs["conversations"] # Convert message IDs to strings to prevent JavaScript precision loss for conv in conversations: if "message_id" in conv: conv["message_id"] = str(conv["message_id"]) print(f"๐Ÿ” API: Returning {len(conversations)} conversations") # Debug: Show message IDs being returned for i, conv in enumerate(conversations): msg_id = conv.get("message_id", "") is_bot = conv.get("is_bot_message", False) content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]" print(f"๐Ÿ” API: Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'") return {"status": "ok", "conversations": conversations} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to get conversations for user {user_id}: {e}") return {"status": "error", "message": f"Failed to get conversations: {e}"} @app.get("/dms/users/{user_id}/search") def search_dm_conversations(user_id: str, query: str, limit: int = 10): """Search conversations with a specific user""" try: from utils.dm_logger import dm_logger # Convert string user_id to int for internal processing user_id_int = int(user_id) results = dm_logger.search_user_conversations(user_id_int, query, limit) return {"status": "ok", "results": results} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: return {"status": "error", "message": f"Failed to search conversations: {e}"} @app.get("/dms/users/{user_id}/export") def export_dm_conversation(user_id: str, format: str = "json"): """Export all conversations with a user""" try: from utils.dm_logger import dm_logger # Convert string user_id to int for internal processing user_id_int = int(user_id) export_path = dm_logger.export_user_conversation(user_id_int, format) return {"status": "ok", "export_path": export_path, "format": format} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: return {"status": "error", "message": f"Failed to export conversation: {e}"} @app.delete("/dms/users/{user_id}") def delete_dm_user_logs(user_id: str): """Delete all DM logs for a specific user""" try: from utils.dm_logger import dm_logger import os # Convert string user_id to int for internal processing user_id_int = int(user_id) log_file = dm_logger._get_user_log_file(user_id_int) if os.path.exists(log_file): os.remove(log_file) return {"status": "ok", "message": f"Deleted DM logs for user {user_id}"} else: return {"status": "error", "message": f"No DM logs found for user {user_id}"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: return {"status": "error", "message": f"Failed to delete DM logs: {e}"} # ========== User Blocking & DM Management ========== @app.get("/dms/blocked-users") def get_blocked_users(): """Get list of all blocked users""" try: blocked_users = dm_logger.get_blocked_users() return {"status": "ok", "blocked_users": blocked_users} except Exception as e: print(f"โŒ API: Failed to get blocked users: {e}") return {"status": "error", "message": f"Failed to get blocked users: {e}"} @app.post("/dms/users/{user_id}/block") def block_user(user_id: str): """Block a user from sending DMs to Miku""" try: user_id_int = int(user_id) # Get username from DM logs if available user_summary = dm_logger.get_user_conversation_summary(user_id_int) username = user_summary.get("username", "Unknown") success = dm_logger.block_user(user_id_int, username) if success: print(f"๐Ÿšซ API: User {user_id} ({username}) blocked") return {"status": "ok", "message": f"User {username} has been blocked"} else: return {"status": "error", "message": f"User {username} is already blocked"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to block user {user_id}: {e}") return {"status": "error", "message": f"Failed to block user: {e}"} @app.post("/dms/users/{user_id}/unblock") def unblock_user(user_id: str): """Unblock a user""" try: user_id_int = int(user_id) success = dm_logger.unblock_user(user_id_int) if success: print(f"โœ… API: User {user_id} unblocked") return {"status": "ok", "message": f"User has been unblocked"} else: return {"status": "error", "message": f"User is not blocked"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to unblock user {user_id}: {e}") return {"status": "error", "message": f"Failed to unblock user: {e}"} @app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete") def delete_conversation(user_id: str, conversation_id: str): """Delete a specific conversation/message from both Discord and logs""" try: user_id_int = int(user_id) # Queue the async deletion in the bot's event loop async def do_delete(): return await dm_logger.delete_conversation(user_id_int, conversation_id) import asyncio success = globals.client.loop.create_task(do_delete()) # For now, return success immediately since we can't await in FastAPI sync endpoint # The actual deletion happens asynchronously print(f"๐Ÿ—‘๏ธ API: Queued deletion of conversation {conversation_id} for user {user_id}") return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to queue conversation deletion {conversation_id}: {e}") return {"status": "error", "message": f"Failed to delete conversation: {e}"} @app.post("/dms/users/{user_id}/conversations/delete-all") def delete_all_conversations(user_id: str): """Delete all conversations with a user from both Discord and logs""" try: user_id_int = int(user_id) # Queue the async bulk deletion in the bot's event loop async def do_delete_all(): return await dm_logger.delete_all_conversations(user_id_int) import asyncio success = globals.client.loop.create_task(do_delete_all()) # Return success immediately since we can't await in FastAPI sync endpoint print(f"๐Ÿ—‘๏ธ API: Queued bulk deletion of all conversations for user {user_id}") return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to queue bulk conversation deletion for user {user_id}: {e}") return {"status": "error", "message": f"Failed to delete conversations: {e}"} @app.post("/dms/users/{user_id}/delete-completely") def delete_user_completely(user_id: str): """Delete user's log file completely""" try: user_id_int = int(user_id) success = dm_logger.delete_user_completely(user_id_int) if success: print(f"๐Ÿ—‘๏ธ API: Completely deleted user {user_id}") return {"status": "ok", "message": "User data deleted completely"} else: return {"status": "error", "message": "No user data found"} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to completely delete user {user_id}: {e}") return {"status": "error", "message": f"Failed to delete user: {e}"} # ========== DM Interaction Analysis Endpoints ========== @app.post("/dms/analysis/run") def run_dm_analysis(): """Manually trigger the daily DM interaction analysis""" try: from utils.dm_interaction_analyzer import dm_analyzer if dm_analyzer is None: return {"status": "error", "message": "DM Analyzer not initialized. Set OWNER_USER_ID environment variable."} # Schedule analysis in Discord's event loop async def run_analysis(): await dm_analyzer.run_daily_analysis() globals.client.loop.create_task(run_analysis()) return {"status": "ok", "message": "DM analysis started"} except Exception as e: print(f"โŒ API: Failed to run DM analysis: {e}") return {"status": "error", "message": f"Failed to run DM analysis: {e}"} @app.post("/dms/users/{user_id}/analyze") def analyze_user_interaction(user_id: str): """Analyze a specific user's interaction and optionally send report""" try: from utils.dm_interaction_analyzer import dm_analyzer if dm_analyzer is None: return {"status": "error", "message": "DM Analyzer not initialized. Set OWNER_USER_ID environment variable."} user_id_int = int(user_id) # Schedule analysis in Discord's event loop async def run_analysis(): return await dm_analyzer.analyze_and_report(user_id_int) globals.client.loop.create_task(run_analysis()) # Return immediately - the analysis will run in the background return {"status": "ok", "message": f"Analysis started for user {user_id}", "reported": True} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to analyze user {user_id}: {e}") return {"status": "error", "message": f"Failed to analyze user: {e}"} @app.get("/dms/analysis/reports") def get_analysis_reports(limit: int = 20): """Get recent analysis reports""" try: import os import json from utils.dm_interaction_analyzer import REPORTS_DIR if not os.path.exists(REPORTS_DIR): return {"status": "ok", "reports": []} reports = [] files = sorted([f for f in os.listdir(REPORTS_DIR) if f.endswith('.json') and f != 'reported_today.json'], reverse=True)[:limit] for filename in files: try: with open(os.path.join(REPORTS_DIR, filename), 'r', encoding='utf-8') as f: report = json.load(f) report['filename'] = filename reports.append(report) except Exception as e: print(f"โš ๏ธ Failed to load report {filename}: {e}") return {"status": "ok", "reports": reports} except Exception as e: print(f"โŒ API: Failed to get reports: {e}") return {"status": "error", "message": f"Failed to get reports: {e}"} @app.get("/dms/analysis/reports/{user_id}") def get_user_reports(user_id: str, limit: int = 10): """Get analysis reports for a specific user""" try: import os import json from utils.dm_interaction_analyzer import REPORTS_DIR if not os.path.exists(REPORTS_DIR): return {"status": "ok", "reports": []} user_id_int = int(user_id) reports = [] files = sorted([f for f in os.listdir(REPORTS_DIR) if f.startswith(f"{user_id}_") and f.endswith('.json')], reverse=True)[:limit] for filename in files: try: with open(os.path.join(REPORTS_DIR, filename), 'r', encoding='utf-8') as f: report = json.load(f) report['filename'] = filename reports.append(report) except Exception as e: print(f"โš ๏ธ Failed to load report {filename}: {e}") return {"status": "ok", "reports": reports} except ValueError: return {"status": "error", "message": f"Invalid user ID format: {user_id}"} except Exception as e: print(f"โŒ API: Failed to get user reports: {e}") return {"status": "error", "message": f"Failed to get user reports: {e}"} # ========== Message Reaction Endpoint ========== @app.post("/messages/react") async def add_reaction_to_message( message_id: str = Form(...), channel_id: str = Form(...), emoji: str = Form(...) ): """Add a reaction to a specific message""" try: if not globals.client or not globals.client.loop or not globals.client.loop.is_running(): return {"status": "error", "message": "Bot not ready"} # Convert IDs to integers try: msg_id = int(message_id) chan_id = int(channel_id) except ValueError: return {"status": "error", "message": "Invalid message ID or channel ID format"} # Fetch the channel channel = globals.client.get_channel(chan_id) if not channel: return {"status": "error", "message": f"Channel {channel_id} not found"} # Queue the reaction task async def add_reaction_task(): try: message = await channel.fetch_message(msg_id) await message.add_reaction(emoji) print(f"โœ… Added reaction {emoji} to message {msg_id} in channel #{channel.name}") except discord.NotFound: print(f"โŒ Message {msg_id} not found in channel #{channel.name}") except discord.Forbidden: print(f"โŒ Bot doesn't have permission to add reactions in channel #{channel.name}") except discord.HTTPException as e: print(f"โŒ Failed to add reaction: {e}") except Exception as e: print(f"โŒ Unexpected error adding reaction: {e}") globals.client.loop.create_task(add_reaction_task()) return { "status": "ok", "message": f"Reaction {emoji} queued for message {message_id}" } except Exception as e: print(f"โŒ API: Failed to add reaction: {e}") return {"status": "error", "message": f"Failed to add reaction: {e}"} # ========== Autonomous V2 Endpoints ========== @app.get("/autonomous/v2/stats/{guild_id}") async def get_v2_stats(guild_id: int): """Get current V2 social stats for a server""" try: from utils.autonomous_v2_integration import get_v2_stats_for_server stats = get_v2_stats_for_server(guild_id) return {"status": "ok", "guild_id": guild_id, "stats": stats} except Exception as e: return {"status": "error", "message": str(e)} @app.get("/autonomous/v2/check/{guild_id}") async def manual_v2_check(guild_id: int): """ Manually trigger a V2 context check (doesn't make Miku act, just shows what she's thinking) Useful for debugging and understanding the decision system. """ try: from utils.autonomous_v2_integration import manual_trigger_v2_check if not globals.client: return {"status": "error", "message": "Bot not ready"} result = await manual_trigger_v2_check(guild_id, globals.client) if isinstance(result, str): return {"status": "error", "message": result} return {"status": "ok", "guild_id": guild_id, "analysis": result} except Exception as e: return {"status": "error", "message": str(e)} @app.get("/autonomous/v2/status") async def get_v2_status(): """Get V2 system status for all servers""" try: from utils.autonomous_v2 import autonomous_system_v2 status = {} for guild_id in server_manager.servers: server_config = server_manager.get_server_config(guild_id) if server_config: stats = autonomous_system_v2.get_stats(guild_id) status[str(guild_id)] = { "server_name": server_config.guild_name, "loop_running": autonomous_system_v2.running_loops.get(guild_id, False), "action_urgency": f"{stats.get_action_urgency():.2f}", "loneliness": f"{stats.loneliness:.2f}", "boredom": f"{stats.boredom:.2f}", "excitement": f"{stats.excitement:.2f}", "chattiness": f"{stats.chattiness:.2f}", } return {"status": "ok", "servers": status} except Exception as e: return {"status": "error", "message": str(e)} def start_api(): import uvicorn uvicorn.run(app, host="0.0.0.0", port=3939)