"""Manual message sending routes + message reactions.""" import io from typing import List from fastapi import APIRouter, UploadFile, File, Form from fastapi.responses import JSONResponse 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 JSONResponse(status_code=404, content={"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 JSONResponse(status_code=500, content={"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 JSONResponse(status_code=500, content={"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 JSONResponse(status_code=404, content={"status": "error", "message": "Channel not found"}) if persona not in ["miku", "evil"]: return JSONResponse(status_code=400, content={"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 JSONResponse(status_code=500, content={"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 JSONResponse(status_code=500, content={"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 JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"}) try: msg_id = int(message_id) chan_id = int(channel_id) except ValueError: return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid message ID or channel ID format"}) channel = globals.client.get_channel(chan_id) if not channel: return JSONResponse(status_code=404, content={"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 JSONResponse(status_code=500, content={"status": "error", "message": f"Failed to add reaction: {e}"})