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: except Exception as e:
return {"status": "error", "message": f"Error: {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") @app.get("/status")
def status(): def status():
# Get per-server mood summary # Get per-server mood summary

View File

@@ -997,6 +997,28 @@
<div class="section" id="manual-message-section"> <div class="section" id="manual-message-section">
<h3>🎭 Send Message as Miku (Manual Override)</h3> <h3>🎭 Send Message as Miku (Manual Override)</h3>
<!-- Webhook Option -->
<div style="margin-bottom: 1rem; padding: 0.5rem; background: #2a2a2a; border-radius: 4px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="manual-use-webhook" onchange="toggleWebhookOptions()" style="margin-right: 0.5rem;" />
<span>Send as Webhook (allows choosing persona)</span>
</label>
<div id="webhook-persona-options" style="display: none; margin-top: 0.5rem; padding-left: 1.5rem;">
<label style="display: block; margin-bottom: 0.3rem;">
<input type="radio" name="webhook-persona" value="miku" checked style="margin-right: 0.5rem;" />
Hatsune Miku 💙 (with mood emoji)
</label>
<label style="display: block;">
<input type="radio" name="webhook-persona" value="evil" style="margin-right: 0.5rem;" />
Evil Miku 😈 (with mood emoji)
</label>
<p style="font-size: 0.8rem; color: #888; margin: 0.3rem 0 0 0;">
Note: Webhooks only work in channels, not DMs. Profile picture and mood emoji will be used.
</p>
</div>
</div>
<!-- Target Selection --> <!-- Target Selection -->
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<label for="manual-target-type">Target Type:</label> <label for="manual-target-type">Target Type:</label>
@@ -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() { function toggleManualMessageTarget() {
const targetType = document.getElementById('manual-target-type').value; const targetType = document.getElementById('manual-target-type').value;
const channelSection = document.getElementById('manual-channel-section'); const channelSection = document.getElementById('manual-channel-section');
@@ -2899,12 +2942,20 @@ async function sendManualMessage() {
const targetType = document.getElementById('manual-target-type').value; const targetType = document.getElementById('manual-target-type').value;
const replyMessageId = document.getElementById('manualReplyMessageId').value.trim(); const replyMessageId = document.getElementById('manualReplyMessageId').value.trim();
const replyMention = document.querySelector('input[name="manualReplyMention"]:checked').value === 'true'; 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) { if (!message) {
showNotification('Please enter a message', 'error'); showNotification('Please enter a message', 'error');
return; return;
} }
// Webhooks only work in channels
if (useWebhook && targetType === 'dm') {
showNotification('Webhooks only work in channels, not DMs', 'error');
return;
}
let targetId, endpoint; let targetId, endpoint;
if (targetType === 'dm') { if (targetType === 'dm') {
@@ -2920,13 +2971,19 @@ async function sendManualMessage() {
showNotification('Please enter a channel ID', 'error'); showNotification('Please enter a channel ID', 'error');
return; return;
} }
endpoint = '/manual/send'; // Use webhook endpoint if webhook is enabled
endpoint = useWebhook ? '/manual/send-webhook' : '/manual/send';
} }
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('message', message); formData.append('message', message);
// Add webhook persona if using webhook
if (useWebhook) {
formData.append('persona', webhookPersona);
}
// Add reply parameters if message ID is provided // Add reply parameters if message ID is provided
if (replyMessageId) { if (replyMessageId) {
formData.append('reply_to_message_id', replyMessageId); formData.append('reply_to_message_id', replyMessageId);