refactor: split api.py monolith into 19 route modules (Phase B)
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).
This commit is contained in:
196
bot/routes/manual_send.py
Normal file
196
bot/routes/manual_send.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""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}"}
|
||||
Reference in New Issue
Block a user