Files
miku-discord/bot/api.py
koko210Serve d58be3b33e Remove all Ollama remnants and complete migration to llama.cpp
- Remove Ollama-specific files (Dockerfile.ollama, entrypoint.sh)
- Replace all query_ollama imports and calls with query_llama
- Remove langchain-ollama dependency from requirements.txt
- Update all utility files (autonomous, kindness, image_generation, etc.)
- Update README.md documentation references
- Maintain backward compatibility alias in llm.py
2025-12-07 17:50:28 +02:00

1495 lines
62 KiB
Python

# 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_llama
async def send_dm_custom_prompt():
try:
response = await query_llama(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)