diff --git a/bot/api.py b/bot/api.py index d94f246..dbafb63 100644 --- a/bot/api.py +++ b/bot/api.py @@ -844,6 +844,79 @@ async def manual_send( except Exception as e: return {"status": "error", "message": f"Error: {e}"} + +@app.post("/manual/send-webhook") +async def manual_send_webhook( + message: str = Form(...), + channel_id: str = Form(...), + persona: str = Form("miku"), # "miku" or "evil" + 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"} + + # Validate persona + if persona not in ["miku", "evil"]: + return {"status": "error", "message": "Invalid persona. Must be 'miku' or 'evil'"} + + # Get or create webhooks for this channel + webhooks = await get_or_create_webhooks_for_channel(channel) + if not webhooks: + return {"status": "error", "message": "Failed to create webhooks for this channel"} + + # Select the appropriate webhook + 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() + + # 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_webhook_message(): + try: + # Prepare files for webhook + discord_files = [] + for file_info in file_data: + discord_files.append(discord.File(io.BytesIO(file_info['content']), filename=file_info['filename'])) + + # Send via webhook with display name + await webhook.send( + content=message, + username=display_name, + files=discord_files if discord_files else None, + wait=True + ) + + persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku" + print(f"✅ Manual webhook message sent as {persona_name} to #{channel.name}") + + except Exception as e: + print(f"❌ Failed to send webhook message: {e}") + + 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}"} + + @app.get("/status") def status(): # Get per-server mood summary diff --git a/bot/static/index.html b/bot/static/index.html index 373649a..71acab2 100644 --- a/bot/static/index.html +++ b/bot/static/index.html @@ -997,6 +997,28 @@

🎭 Send Message as Miku (Manual Override)

+ +
+ + + +
+
@@ -2736,6 +2758,27 @@ function toggleCustomPromptTarget() { } } +function toggleWebhookOptions() { + const useWebhook = document.getElementById('manual-use-webhook').checked; + const webhookOptions = document.getElementById('webhook-persona-options'); + const targetType = document.getElementById('manual-target-type'); + + if (useWebhook) { + webhookOptions.style.display = 'block'; + // Webhooks only work in channels, so switch to channel if DM is selected + if (targetType.value === 'dm') { + targetType.value = 'channel'; + toggleManualMessageTarget(); + } + // Disable DM option when webhook is enabled + targetType.options[1].disabled = true; + } else { + webhookOptions.style.display = 'none'; + // Re-enable DM option + targetType.options[1].disabled = false; + } +} + function toggleManualMessageTarget() { const targetType = document.getElementById('manual-target-type').value; const channelSection = document.getElementById('manual-channel-section'); @@ -2899,12 +2942,20 @@ async function sendManualMessage() { const targetType = document.getElementById('manual-target-type').value; const replyMessageId = document.getElementById('manualReplyMessageId').value.trim(); const replyMention = document.querySelector('input[name="manualReplyMention"]:checked').value === 'true'; + const useWebhook = document.getElementById('manual-use-webhook').checked; + const webhookPersona = document.querySelector('input[name="webhook-persona"]:checked')?.value || 'miku'; if (!message) { showNotification('Please enter a message', 'error'); return; } + // Webhooks only work in channels + if (useWebhook && targetType === 'dm') { + showNotification('Webhooks only work in channels, not DMs', 'error'); + return; + } + let targetId, endpoint; if (targetType === 'dm') { @@ -2920,13 +2971,19 @@ async function sendManualMessage() { showNotification('Please enter a channel ID', 'error'); return; } - endpoint = '/manual/send'; + // Use webhook endpoint if webhook is enabled + endpoint = useWebhook ? '/manual/send-webhook' : '/manual/send'; } try { const formData = new FormData(); formData.append('message', message); + // Add webhook persona if using webhook + if (useWebhook) { + formData.append('persona', webhookPersona); + } + // Add reply parameters if message ID is provided if (replyMessageId) { formData.append('reply_to_message_id', replyMessageId);