208 lines
6.2 KiB
Python
208 lines
6.2 KiB
Python
|
|
# 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, [])
|