Fix: Resolve webhook send timeout context error

ISSUE
=====
When using the manual webhook message feature via API, the following error occurred:
- 'Timeout context manager should be used inside a task'
- 'NoneType' object is not iterable (when sending without files)

The error happened because Discord.py's webhook operations were being awaited
directly in the FastAPI endpoint context, rather than within a task running in
the bot's event loop.

SOLUTION
========
Refactored /manual/send-webhook endpoint to properly handle async operations:

1. Moved webhook creation inside task function
   - get_or_create_webhooks_for_channel() now runs in send_webhook_message()
   - All Discord operations (webhook selection, sending) happen inside the task
   - Follows same pattern as working /manual/send endpoint

2. Fixed file parameter handling
   - Changed from 'files=discord_files if discord_files else None'
   - To conditional: only pass files parameter when list is non-empty
   - Discord.py's webhook.send() cannot iterate over None, requires list or omit

3. Maintained proper file reading
   - File content still read in endpoint context (before form closes)
   - File data passed to task as pre-read byte arrays
   - Prevents form closure issues

TECHNICAL DETAILS
=================
- Discord.py HTTP operations use timeout context managers
- Context managers must run inside bot's event loop (via create_task)
- FastAPI endpoint context is separate from bot's event loop
- Solution: Wrap all Discord API calls in async task function
- Pattern: Read files → Create task → Task handles Discord operations

TESTING
=======
- Manual webhook sending now works without timeout errors
- Both personas (Miku/Evil) send correctly
- File attachments work properly
- Messages without files send correctly
This commit is contained in:
2026-01-07 13:44:13 +02:00
parent caab444c08
commit ed5994ec78
3 changed files with 272 additions and 15 deletions

View File

@@ -866,15 +866,6 @@ async def manual_send_webhook(
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:
@@ -891,24 +882,43 @@ async def manual_send_webhook(
# Use create_task to avoid timeout context manager error
async def send_webhook_message():
try:
# Get or create webhooks for this channel (inside the task)
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"❌ Failed to create webhooks for channel #{channel.name}")
return
# 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()
# 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
)
if discord_files:
await webhook.send(
content=message,
username=display_name,
files=discord_files,
wait=True
)
else:
await webhook.send(
content=message,
username=display_name,
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}")
import traceback
traceback.print_exc()
globals.client.loop.create_task(send_webhook_message())
return {"status": "ok", "message": f"Webhook message queued for sending as {persona}"}