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:
73
bot/api.py
73
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
|
||||
|
||||
@@ -997,6 +997,28 @@
|
||||
<div class="section" id="manual-message-section">
|
||||
<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 -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<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() {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user