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.
This commit is contained in:
2026-01-07 10:21:46 +02:00
parent 86a54dd0ba
commit caab444c08
2 changed files with 131 additions and 1 deletions

View File

@@ -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