# api.py from fastapi import ( FastAPI, Query, BackgroundTasks, Request, UploadFile, File, Form ) from typing import List from pydantic import BaseModel import globals from commands.actions import ( force_sleep, wake_up, set_mood, reset_mood, check_mood, calm_miku, reset_conversation, send_bedtime_now ) from utils.moods import nickname_mood_emoji 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 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 # ========== 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:] return "".join(lines[-100] if len(lines) >= 100 else lines) 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": check_mood()} @app.post("/mood") async def set_mood_endpoint(data: MoodSetRequest): success = set_mood(data.mood) if success: globals.client.loop.create_task(nickname_mood_emoji()) return {"status": "ok", "new_mood": data.mood} return {"status": "error", "message": "Mood not recognized"} @app.post("/mood/reset") async def reset_mood_endpoint(background_tasks: BackgroundTasks): reset_mood() globals.client.loop.create_task(nickname_mood_emoji()) return {"status": "ok", "new_mood": "neutral"} @app.post("/mood/calm") def calm_miku_endpoint(): calm_miku() return {"status": "ok", "message": "Miku has calmed down."} @app.post("/conversation/reset") def reset_convo(data: ConversationResetRequest): reset_conversation(data.user_id) return {"status": "ok", "message": f"Memory reset for {data.user_id}"} @app.post("/sleep") async def force_sleep_endpoint(): await force_sleep() globals.client.loop.create_task(nickname_mood_emoji()) return {"status": "ok", "message": "Miku is now sleeping"} @app.post("/wake") async def wake_up_endpoint(): await wake_up() globals.client.loop.create_task(nickname_mood_emoji()) return {"status": "ok", "message": "Miku is now awake"} @app.post("/bedtime") async def bedtime_endpoint(background_tasks: BackgroundTasks): globals.client.loop.create_task(send_bedtime_now()) return {"status": "ok", "message": "Bedtime message sent"} @app.post("/autonomous/general") async def trigger_autonomous_general(): globals.client.loop.create_task(miku_autonomous_tick(force=True, force_action="general")) return {"status": "ok", "message": "Miku say something general triggered manually"} @app.post("/autonomous/engage") async def trigger_autonomous_engage_user(): globals.client.loop.create_task(miku_autonomous_tick(force=True, force_action="engage_user")) return {"status": "ok", "message": "Miku engage random user triggered manually"} @app.post("/autonomous/tweet") async def trigger_autonomous_tweet(): globals.client.loop.create_task(miku_autonomous_tick(force=True, force_action="share_tweet")) return {"status": "ok", "message": "Miku share tweet triggered manually"} @app.post("/autonomous/custom") async def custom_autonomous_message(req: CustomPromptRequest): try: asyncio.run_coroutine_threadsafe( handle_custom_prompt(req.prompt), globals.client.loop ) return {"success": True, "message": "Miku is working on it!"} except Exception as e: print(f"❌ Error running custom prompt in bot loop: {repr(e)}") return {"success": False, "error": str(e)} @app.post("/manual/send") async def manual_send( message: str = Form(...), channel_id: str = Form(...), files: List[UploadFile] = File(default=[]) ): try: # Get the Discord channel Miku should post in channel = globals.client.get_channel(int(channel_id)) if not channel: return {"success": False, "error": "Target channel not found"} # Prepare file data (read in the async FastAPI thread) prepared_files = [] for f in files: contents = await f.read() prepared_files.append((f.filename, contents)) # Define a coroutine that will run inside the bot loop async def send_message(): channel = globals.client.get_channel(int(channel_id)) if not channel: raise ValueError(f"Channel ID {channel_id} not found or bot cannot access it.") discord_files = [ discord.File(io.BytesIO(content), filename=filename) for filename, content in prepared_files ] await channel.send(content=message or None, files=discord_files or None) # Schedule coroutine in bot's event loop future = asyncio.run_coroutine_threadsafe(send_message(), globals.client.loop) future.result(timeout=10) # Wait max 10 seconds for it to finish return {"success": True} except Exception as e: print(f"❌ Error in /manual/send: {repr(e)}") return {"success": False, "error": str(e)} @app.get("/status") def status(): return { "mood": globals.CURRENT_MOOD_NAME, "is_sleeping": globals.IS_SLEEPING, "previous_mood": globals.PREVIOUS_MOOD_NAME } @app.get("/conversation/{user_id}") def get_conversation(user_id: str): return globals.conversation_history.get(user_id, [])