Split 3,598-line api.py into thin orchestrator (128 lines) + 19 route modules in bot/routes/: core.py (7 routes), mood.py (10), language.py (3), evil_mode.py (6), bipolar_mode.py (9), gpu.py (2), bot_actions.py (4), autonomous.py (13), profile_picture.py (26), manual_send.py (3), servers.py (6), figurines.py (5), dms.py (18), image_generation.py (4), chat.py (1), config.py (7), logging_config.py (9), voice.py (3), memory.py (10) All 146 routes verified present via test_route_split.py (149 tests). 21/21 regression tests (test_config_state.py) pass. Monolith backup: bot/api_monolith_backup.py (revert: cp it to api.py).
197 lines
7.8 KiB
Python
197 lines
7.8 KiB
Python
"""Manual message sending routes + message reactions."""
|
|
|
|
import io
|
|
from typing import List
|
|
from fastapi import APIRouter, UploadFile, File, Form
|
|
import discord
|
|
import globals
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger('api')
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/manual/send")
|
|
async def manual_send(
|
|
message: str = Form(...),
|
|
channel_id: str = Form(...),
|
|
files: List[UploadFile] = File(default=[]),
|
|
reply_to_message_id: str = Form(None),
|
|
mention_author: bool = Form(True)
|
|
):
|
|
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:
|
|
logger.error(f"Failed to read file {file.filename}: {e}")
|
|
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
|
|
|
|
async def send_message_and_files():
|
|
try:
|
|
reference_message = None
|
|
if reply_to_message_id:
|
|
try:
|
|
reference_message = await channel.fetch_message(int(reply_to_message_id))
|
|
except Exception as e:
|
|
logger.error(f"Could not fetch message {reply_to_message_id} for reply: {e}")
|
|
return
|
|
|
|
if message.strip():
|
|
if reference_message:
|
|
await channel.send(message, reference=reference_message, mention_author=mention_author)
|
|
logger.info(f"Manual message sent as reply to #{channel.name}")
|
|
else:
|
|
await channel.send(message)
|
|
logger.info(f"Manual message sent to #{channel.name}")
|
|
|
|
for file_info in file_data:
|
|
try:
|
|
await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
|
|
logger.info(f"File {file_info['filename']} sent to #{channel.name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send file {file_info['filename']}: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(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}"}
|
|
|
|
|
|
@router.post("/manual/send-webhook")
|
|
async def manual_send_webhook(
|
|
message: str = Form(...),
|
|
channel_id: str = Form(...),
|
|
persona: str = Form("miku"),
|
|
files: List[UploadFile] = File(default=[]),
|
|
reply_to_message_id: str = Form(None),
|
|
mention_author: bool = Form(True)
|
|
):
|
|
"""Send a manual message via webhook as either Hatsune Miku or Evil Miku"""
|
|
try:
|
|
from utils.bipolar_mode import get_or_create_webhooks_for_channel, get_miku_display_name, get_evil_miku_display_name
|
|
|
|
channel = globals.client.get_channel(int(channel_id))
|
|
if not channel:
|
|
return {"status": "error", "message": "Channel not found"}
|
|
|
|
if persona not in ["miku", "evil"]:
|
|
return {"status": "error", "message": "Invalid persona. Must be 'miku' or 'evil'"}
|
|
|
|
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:
|
|
logger.error(f"Failed to read file {file.filename}: {e}")
|
|
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
|
|
|
|
async def send_webhook_message():
|
|
try:
|
|
webhooks = await get_or_create_webhooks_for_channel(channel)
|
|
if not webhooks:
|
|
logger.error(f"Failed to create webhooks for channel #{channel.name}")
|
|
return
|
|
|
|
webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"]
|
|
display_name = get_evil_miku_display_name() if persona == "evil" else get_miku_display_name()
|
|
|
|
discord_files = []
|
|
for file_info in file_data:
|
|
discord_files.append(discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
|
|
|
|
from utils.bipolar_mode import get_persona_avatar_urls
|
|
avatar_urls = get_persona_avatar_urls()
|
|
avatar_url = avatar_urls.get("evil_miku") if persona == "evil" else avatar_urls.get("miku")
|
|
|
|
if discord_files:
|
|
await webhook.send(
|
|
content=message, username=display_name,
|
|
avatar_url=avatar_url, files=discord_files, wait=True
|
|
)
|
|
else:
|
|
await webhook.send(
|
|
content=message, username=display_name,
|
|
avatar_url=avatar_url, wait=True
|
|
)
|
|
|
|
persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku"
|
|
logger.info(f"Manual webhook message sent as {persona_name} to #{channel.name}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send webhook message: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
globals.client.loop.create_task(send_webhook_message())
|
|
return {"status": "ok", "message": f"Webhook message queued for sending as {persona}"}
|
|
|
|
except Exception as e:
|
|
return {"status": "error", "message": f"Error: {e}"}
|
|
|
|
|
|
@router.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"}
|
|
|
|
try:
|
|
msg_id = int(message_id)
|
|
chan_id = int(channel_id)
|
|
except ValueError:
|
|
return {"status": "error", "message": "Invalid message ID or channel ID format"}
|
|
|
|
channel = globals.client.get_channel(chan_id)
|
|
if not channel:
|
|
return {"status": "error", "message": f"Channel {channel_id} not found"}
|
|
|
|
async def add_reaction_task():
|
|
try:
|
|
message = await channel.fetch_message(msg_id)
|
|
await message.add_reaction(emoji)
|
|
logger.info(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
|
|
except discord.NotFound:
|
|
logger.error(f"Message {msg_id} not found in channel #{channel.name}")
|
|
except discord.Forbidden:
|
|
logger.error(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
|
|
except discord.HTTPException as e:
|
|
logger.error(f"Failed to add reaction: {e}")
|
|
except Exception as e:
|
|
logger.error(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:
|
|
logger.error(f"Failed to add reaction: {e}")
|
|
return {"status": "error", "message": f"Failed to add reaction: {e}"}
|