From caab444c08d687805c081fccf33e990ab74ba1f0 Mon Sep 17 00:00:00 2001 From: koko210Serve Date: Wed, 7 Jan 2026 10:21:46 +0200 Subject: [PATCH] Add webhook option to manual message override for persona selection Users can now send manual messages as either Hatsune Miku or Evil Miku via webhooks without needing to toggle Evil Mode. This provides more flexibility for controlling which persona sends messages. Features: - Checkbox option to "Send as Webhook" in manual message section - Radio buttons to select between Hatsune Miku and Evil Miku - Both personas use their respective profile pictures and mood emojis - Webhooks only available for channel messages (not DMs) - DM option automatically disabled when webhook mode is enabled - New API endpoint: POST /manual/send-webhook Frontend Changes: - Added webhook checkbox and persona selection UI - toggleWebhookOptions() function to show/hide persona options - Updated sendManualMessage() to handle webhook mode - Automatic channel selection when webhook is enabled Backend Changes: - New /manual/send-webhook endpoint in api.py - Integrates with bipolar_mode.py webhook management - Uses get_or_create_webhooks_for_channel() for webhook creation - Applies correct display name with mood emoji based on persona - Supports file attachments via webhook This allows manual control over which Miku persona sends messages, useful for testing, demonstrations, or creative scenarios without needing to switch the entire bot mode. --- bot/api.py | 73 +++++++++++++++++++++++++++++++++++++++++++ bot/static/index.html | 59 +++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) 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);