437 lines
22 KiB
Python
437 lines
22 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
import traceback
|
|
import os
|
|
import datetime
|
|
|
|
# Global function for storing interaction content
|
|
store_interaction_content = None
|
|
|
|
# Utility functions to store message content before sending
|
|
async def store_and_send(ctx_or_interaction, content, **kwargs):
|
|
"""Store the message content and then send it."""
|
|
# Store the content for potential error handling
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
ctx_or_interaction._last_message_content = content
|
|
return await ctx_or_interaction.send(content, **kwargs)
|
|
else: # It's an interaction
|
|
ctx_or_interaction._last_response_content = content
|
|
if not ctx_or_interaction.response.is_done():
|
|
return await ctx_or_interaction.response.send_message(content, **kwargs)
|
|
else:
|
|
return await ctx_or_interaction.followup.send(content, **kwargs)
|
|
|
|
async def store_and_reply(ctx, content, **kwargs):
|
|
"""Store the message content and then reply to the message."""
|
|
ctx._last_message_content = content
|
|
return await ctx.reply(content, **kwargs)
|
|
|
|
def extract_message_content(ctx_or_interaction):
|
|
"""Extract message content from a Context or Interaction object."""
|
|
content = None
|
|
|
|
# Check if this is an AI command error
|
|
is_ai_command = False
|
|
if isinstance(ctx_or_interaction, commands.Context) and hasattr(ctx_or_interaction, 'command'):
|
|
is_ai_command = ctx_or_interaction.command and ctx_or_interaction.command.name == 'ai'
|
|
elif hasattr(ctx_or_interaction, 'command') and ctx_or_interaction.command:
|
|
is_ai_command = ctx_or_interaction.command.name == 'ai'
|
|
|
|
# For AI commands, try to load from the ai_response.txt file if it exists
|
|
if is_ai_command and os.path.exists('ai_response.txt'):
|
|
try:
|
|
with open('ai_response.txt', 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
if content:
|
|
return content
|
|
except Exception as e:
|
|
print(f"Error reading ai_response.txt: {e}")
|
|
|
|
# For interactions, try to get content from the AI cog's dictionary
|
|
if not isinstance(ctx_or_interaction, commands.Context) and is_ai_command:
|
|
try:
|
|
# Try to import the dictionary from the AI cog
|
|
from cogs.ai_cog import interaction_responses
|
|
|
|
# Get the interaction ID
|
|
interaction_id = getattr(ctx_or_interaction, 'id', None)
|
|
if interaction_id and interaction_id in interaction_responses:
|
|
content = interaction_responses[interaction_id]
|
|
print(f"Retrieved content for interaction {interaction_id} from dictionary")
|
|
if content:
|
|
return content
|
|
except Exception as e:
|
|
print(f"Error retrieving from interaction_responses dictionary: {e}")
|
|
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
# For Context objects
|
|
if hasattr(ctx_or_interaction, '_last_message_content'):
|
|
content = ctx_or_interaction._last_message_content
|
|
elif hasattr(ctx_or_interaction, 'message') and hasattr(ctx_or_interaction.message, 'content'):
|
|
content = ctx_or_interaction.message.content
|
|
elif hasattr(ctx_or_interaction, '_internal_response'):
|
|
content = str(ctx_or_interaction._internal_response)
|
|
# Try to extract from command invocation
|
|
elif hasattr(ctx_or_interaction, 'command') and hasattr(ctx_or_interaction, 'kwargs'):
|
|
# Reconstruct command invocation
|
|
cmd_name = ctx_or_interaction.command.name if hasattr(ctx_or_interaction.command, 'name') else 'unknown_command'
|
|
args_str = ' '.join([str(arg) for arg in ctx_or_interaction.args[1:]]) if hasattr(ctx_or_interaction, 'args') else ''
|
|
kwargs_str = ' '.join([f'{k}={v}' for k, v in ctx_or_interaction.kwargs.items()]) if ctx_or_interaction.kwargs else ''
|
|
content = f"Command: {cmd_name} {args_str} {kwargs_str}".strip()
|
|
else:
|
|
# For Interaction objects
|
|
if hasattr(ctx_or_interaction, '_last_response_content'):
|
|
content = ctx_or_interaction._last_response_content
|
|
elif hasattr(ctx_or_interaction, '_internal_response'):
|
|
content = str(ctx_or_interaction._internal_response)
|
|
# Try to extract from interaction data
|
|
elif hasattr(ctx_or_interaction, 'data'):
|
|
try:
|
|
# Extract command name and options
|
|
cmd_name = ctx_or_interaction.data.get('name', 'unknown_command')
|
|
options = ctx_or_interaction.data.get('options', [])
|
|
options_str = ' '.join([f"{opt.get('name')}={opt.get('value')}" for opt in options]) if options else ''
|
|
content = f"Slash Command: /{cmd_name} {options_str}".strip()
|
|
except (AttributeError, KeyError):
|
|
# If we can't extract structured data, try to get the raw data
|
|
content = f"Interaction Data: {str(ctx_or_interaction.data)}"
|
|
|
|
# For AI commands, add a note if we couldn't retrieve the full response
|
|
if is_ai_command and (not content or len(content) < 100):
|
|
content = "The AI response was too long and could not be retrieved. " + \
|
|
"This is likely due to a message that exceeded Discord's length limits. " + \
|
|
"Please try again with a shorter prompt or request fewer details."
|
|
|
|
return content
|
|
|
|
def log_error_details(ctx_or_interaction, error, content=None):
|
|
"""Log detailed error information to a file for debugging."""
|
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
log_dir = "error_logs"
|
|
|
|
# Create logs directory if it doesn't exist
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir)
|
|
|
|
# Create a unique filename based on timestamp
|
|
log_file = os.path.join(log_dir, f"error_{timestamp.replace(':', '-').replace(' ', '_')}.log")
|
|
|
|
with open(log_file, "w", encoding="utf-8") as f:
|
|
f.write(f"=== Error Log: {timestamp} ===\n\n")
|
|
|
|
# Log error details
|
|
f.write(f"Error Type: {type(error).__name__}\n")
|
|
f.write(f"Error Message: {str(error)}\n\n")
|
|
|
|
# Log error attributes
|
|
if hasattr(error, '__dict__'):
|
|
f.write("Error Attributes:\n")
|
|
for key, value in error.__dict__.items():
|
|
f.write(f" {key}: {value}\n")
|
|
f.write("\n")
|
|
|
|
# Log cause if available
|
|
if error.__cause__:
|
|
f.write(f"Cause: {type(error.__cause__).__name__}\n")
|
|
f.write(f"Cause Message: {str(error.__cause__)}\n\n")
|
|
|
|
if hasattr(error.__cause__, '__dict__'):
|
|
f.write("Cause Attributes:\n")
|
|
for key, value in error.__cause__.__dict__.items():
|
|
f.write(f" {key}: {value}\n")
|
|
f.write("\n")
|
|
|
|
# Log traceback
|
|
f.write("Traceback:\n")
|
|
f.write(traceback.format_exc())
|
|
f.write("\n")
|
|
|
|
# Log context/interaction details
|
|
f.write("Context/Interaction Details:\n")
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
f.write(f" Type: Context\n")
|
|
if hasattr(ctx_or_interaction, 'command') and ctx_or_interaction.command:
|
|
f.write(f" Command: {ctx_or_interaction.command.name}\n")
|
|
if hasattr(ctx_or_interaction, 'author') and ctx_or_interaction.author:
|
|
f.write(f" Author: {ctx_or_interaction.author.name} (ID: {ctx_or_interaction.author.id})\n")
|
|
if hasattr(ctx_or_interaction, 'guild') and ctx_or_interaction.guild:
|
|
f.write(f" Guild: {ctx_or_interaction.guild.name} (ID: {ctx_or_interaction.guild.id})\n")
|
|
if hasattr(ctx_or_interaction, 'channel') and ctx_or_interaction.channel:
|
|
f.write(f" Channel: {ctx_or_interaction.channel.name} (ID: {ctx_or_interaction.channel.id})\n")
|
|
else:
|
|
f.write(f" Type: Interaction\n")
|
|
if hasattr(ctx_or_interaction, 'user') and ctx_or_interaction.user:
|
|
f.write(f" User: {ctx_or_interaction.user.name} (ID: {ctx_or_interaction.user.id})\n")
|
|
if hasattr(ctx_or_interaction, 'guild') and ctx_or_interaction.guild:
|
|
f.write(f" Guild: {ctx_or_interaction.guild.name} (ID: {ctx_or_interaction.guild.id})\n")
|
|
if hasattr(ctx_or_interaction, 'channel') and ctx_or_interaction.channel:
|
|
f.write(f" Channel: {ctx_or_interaction.channel.name} (ID: {ctx_or_interaction.channel.id})\n")
|
|
if hasattr(ctx_or_interaction, 'command') and ctx_or_interaction.command:
|
|
f.write(f" Command: {ctx_or_interaction.command.name}\n")
|
|
f.write("\n")
|
|
|
|
# Log message content if available
|
|
if content:
|
|
f.write("Message Content:\n")
|
|
f.write(content)
|
|
f.write("\n")
|
|
|
|
print(f"Error details logged to {log_file}")
|
|
return log_file
|
|
|
|
def patch_discord_methods():
|
|
"""Patch Discord methods to store message content before sending."""
|
|
# Save original methods for Context
|
|
original_context_send = commands.Context.send
|
|
original_context_reply = commands.Context.reply
|
|
|
|
# Patch Context.send
|
|
async def patched_context_send(self, content=None, **kwargs):
|
|
if content is not None:
|
|
self._last_message_content = content
|
|
return await original_context_send(self, content, **kwargs)
|
|
|
|
# Patch Context.reply
|
|
async def patched_context_reply(self, content=None, **kwargs):
|
|
if content is not None:
|
|
self._last_message_content = content
|
|
return await original_context_reply(self, content, **kwargs)
|
|
|
|
# Apply Context patches
|
|
commands.Context.send = patched_context_send
|
|
commands.Context.reply = patched_context_reply
|
|
|
|
# For Interaction, we'll use a simpler approach that doesn't rely on patching
|
|
# the internal classes, which can vary between Discord.py versions
|
|
|
|
# Instead, we'll add a utility function to store content that can be called
|
|
# before sending messages with interactions
|
|
|
|
# This function will be available globally for use in commands
|
|
global store_interaction_content
|
|
def store_interaction_content(interaction, content):
|
|
"""Store content in an interaction for potential error recovery"""
|
|
if interaction and content:
|
|
try:
|
|
# Try to import the dictionary from the AI cog
|
|
try:
|
|
from cogs.ai_cog import interaction_responses
|
|
|
|
# Store using the interaction ID as the key
|
|
interaction_id = getattr(interaction, 'id', None)
|
|
if interaction_id:
|
|
interaction_responses[interaction_id] = content
|
|
print(f"Stored response for interaction {interaction_id} in dictionary from error_handler")
|
|
return True
|
|
except ImportError:
|
|
pass
|
|
|
|
# Fallback: try to set attribute directly (may fail)
|
|
interaction._last_response_content = content
|
|
return True
|
|
except Exception as e:
|
|
print(f"Warning: Failed to store interaction content in error_handler: {e}")
|
|
return False
|
|
|
|
print("Discord Context methods patched successfully")
|
|
|
|
async def handle_error(ctx_or_interaction, error):
|
|
user_id = 452666956353503252 # Replace with the specific user ID
|
|
error_message = f"An error occurred: {error}"
|
|
|
|
# Check if this is an AI command error
|
|
is_ai_command = False
|
|
if isinstance(ctx_or_interaction, commands.Context) and hasattr(ctx_or_interaction, 'command'):
|
|
is_ai_command = ctx_or_interaction.command and ctx_or_interaction.command.name == 'ai'
|
|
elif hasattr(ctx_or_interaction, 'command') and ctx_or_interaction.command:
|
|
is_ai_command = ctx_or_interaction.command.name == 'ai'
|
|
|
|
# For AI command errors with HTTPException, try to handle specially
|
|
if is_ai_command and isinstance(error, commands.CommandInvokeError) and isinstance(error.original, discord.HTTPException):
|
|
if error.original.code == 50035 and "Must be 4000 or fewer in length" in str(error.original):
|
|
# Try to get the AI response from the stored content
|
|
if isinstance(ctx_or_interaction, commands.Context) and hasattr(ctx_or_interaction, '_last_message_content'):
|
|
content = ctx_or_interaction._last_message_content
|
|
# Save to file and send
|
|
with open('ai_response.txt', 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
await ctx_or_interaction.send("The AI response was too long. Here's the content as a file:", file=discord.File('ai_response.txt'))
|
|
return
|
|
elif hasattr(ctx_or_interaction, '_last_response_content'):
|
|
content = ctx_or_interaction._last_response_content
|
|
# Save to file and send
|
|
with open('ai_response.txt', 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
if not ctx_or_interaction.response.is_done():
|
|
await ctx_or_interaction.response.send_message("The AI response was too long. Here's the content as a file:", file=discord.File('ai_response.txt'))
|
|
else:
|
|
await ctx_or_interaction.followup.send("The AI response was too long. Here's the content as a file:", file=discord.File('ai_response.txt'))
|
|
return
|
|
|
|
# Extract message content for logging
|
|
content = extract_message_content(ctx_or_interaction)
|
|
|
|
# Log error details to file
|
|
log_file = log_error_details(ctx_or_interaction, error, content)
|
|
|
|
# Check if the command runner is the owner
|
|
is_owner = False
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
is_owner = ctx_or_interaction.author.id == user_id
|
|
else:
|
|
is_owner = ctx_or_interaction.user.id == user_id
|
|
|
|
# Only send detailed error DM if the command runner is the owner
|
|
if is_owner:
|
|
try:
|
|
owner = await ctx_or_interaction.bot.fetch_user(user_id)
|
|
if owner:
|
|
full_error = f"Full error details:\n```\n{str(error)}\n"
|
|
if hasattr(error, '__dict__'):
|
|
full_error += f"\nError attributes:\n{error.__dict__}\n"
|
|
if error.__cause__:
|
|
full_error += f"\nCause:\n{str(error.__cause__)}\n"
|
|
if hasattr(error.__cause__, '__dict__'):
|
|
full_error += f"\nCause attributes:\n{error.__cause__.__dict__}\n"
|
|
full_error += "```"
|
|
|
|
# Add log file path to the error message
|
|
full_error += f"\nDetailed error log saved to: `{log_file}`"
|
|
|
|
# Try to send the log file as an attachment
|
|
try:
|
|
await owner.send("Here's the detailed error log:", file=discord.File(log_file))
|
|
# Send a shorter message since we sent the file
|
|
short_error = f"Error: {str(error)}"
|
|
if error.__cause__:
|
|
short_error += f"\nCause: {str(error.__cause__)}"
|
|
await owner.send(short_error)
|
|
except discord.HTTPException:
|
|
# If sending the file fails, fall back to text messages
|
|
# Split long messages if needed
|
|
if len(full_error) > 1900:
|
|
parts = [full_error[i:i+1900] for i in range(0, len(full_error), 1900)]
|
|
for i, part in enumerate(parts):
|
|
await owner.send(f"Part {i+1}/{len(parts)}:\n{part}")
|
|
else:
|
|
await owner.send(full_error)
|
|
except Exception as e:
|
|
print(f"Failed to send error DM to owner: {e}")
|
|
|
|
# Determine the file name to use for saving content
|
|
file_name = 'message.txt'
|
|
|
|
# Special handling for AI command errors
|
|
if isinstance(error, commands.CommandInvokeError) and isinstance(error.original, discord.HTTPException):
|
|
# Check if this is an AI command
|
|
is_ai_command = False
|
|
if isinstance(ctx_or_interaction, commands.Context) and hasattr(ctx_or_interaction, 'command'):
|
|
is_ai_command = ctx_or_interaction.command and ctx_or_interaction.command.name == 'ai'
|
|
elif hasattr(ctx_or_interaction, 'command') and ctx_or_interaction.command:
|
|
is_ai_command = ctx_or_interaction.command.name == 'ai'
|
|
|
|
# If it's an AI command, use a different file name
|
|
if is_ai_command:
|
|
file_name = 'ai_response.txt'
|
|
|
|
# Handle message too long error (HTTP 400 - Code 50035 or 40005 for file uploads)
|
|
if (isinstance(error, discord.HTTPException) and
|
|
((error.code == 50035 and ("Must be 4000 or fewer in length" in str(error) or "Must be 2000 or fewer in length" in str(error))) or
|
|
(error.code == 40005 and "Request entity too large" in str(error)))) or \
|
|
(isinstance(error, commands.CommandInvokeError) and isinstance(error.original, discord.HTTPException) and
|
|
((error.original.code == 50035 and ("Must be 4000 or fewer in length" in str(error.original) or "Must be 2000 or fewer in length" in str(error.original))) or
|
|
(error.original.code == 40005 and "Request entity too large" in str(error.original)))):
|
|
# Try to extract the actual content from the error
|
|
content = None
|
|
|
|
# Handle CommandInvokeError specially
|
|
if isinstance(error, commands.CommandInvokeError):
|
|
# Use the original error for extraction
|
|
original_error = error.original
|
|
if isinstance(original_error, discord.HTTPException):
|
|
content = original_error.text if hasattr(original_error, 'text') else None
|
|
# If it's a wrapped error, get the original error's content
|
|
elif isinstance(error.__cause__, discord.HTTPException):
|
|
content = error.__cause__.text if hasattr(error.__cause__, 'text') else None
|
|
else:
|
|
content = error.text if hasattr(error, 'text') else None
|
|
|
|
# If content is not available in the error, try to retrieve it from the context/interaction
|
|
if not content or len(content) < 10: # If content is missing or too short to be the actual message
|
|
# Try to get the original content using our utility function
|
|
content = extract_message_content(ctx_or_interaction)
|
|
|
|
# If we still don't have content, use a generic message
|
|
if not content:
|
|
content = "The original message content could not be retrieved. This is likely due to a message that exceeded Discord's length limits."
|
|
|
|
# Try to send as a file first
|
|
try:
|
|
# Create a text file with the content
|
|
with open(file_name, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
|
|
# Send the file instead
|
|
message = f"The message was too long. Here's the content as a file:\nError details logged to: {log_file}"
|
|
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
await ctx_or_interaction.send(
|
|
message,
|
|
file=discord.File(file_name)
|
|
)
|
|
else:
|
|
if not ctx_or_interaction.response.is_done():
|
|
await ctx_or_interaction.response.send_message(
|
|
message,
|
|
file=discord.File(file_name)
|
|
)
|
|
else:
|
|
await ctx_or_interaction.followup.send(
|
|
message,
|
|
file=discord.File(file_name)
|
|
)
|
|
except discord.HTTPException as e:
|
|
# If sending as a file also fails (e.g., file too large), split into multiple messages
|
|
if e.code == 40005 or "Request entity too large" in str(e):
|
|
# Split the content into chunks of 1900 characters (Discord limit is 2000)
|
|
chunks = [content[i:i+1900] for i in range(0, len(content), 1900)]
|
|
|
|
# Send a notification about splitting the message
|
|
intro_message = f"The message was too long to send as a file. Splitting into {len(chunks)} parts.\nError details logged to: {log_file}"
|
|
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
await ctx_or_interaction.send(intro_message)
|
|
for i, chunk in enumerate(chunks):
|
|
await ctx_or_interaction.send(f"Part {i+1}/{len(chunks)}:\n```\n{chunk}\n```")
|
|
else:
|
|
if not ctx_or_interaction.response.is_done():
|
|
await ctx_or_interaction.response.send_message(intro_message)
|
|
for i, chunk in enumerate(chunks):
|
|
await ctx_or_interaction.followup.send(f"Part {i+1}/{len(chunks)}:\n```\n{chunk}\n```")
|
|
else:
|
|
await ctx_or_interaction.followup.send(intro_message)
|
|
for i, chunk in enumerate(chunks):
|
|
await ctx_or_interaction.followup.send(f"Part {i+1}/{len(chunks)}:\n```\n{chunk}\n```")
|
|
else:
|
|
# If it's a different error, re-raise it
|
|
raise
|
|
return
|
|
|
|
# Original error handling logic
|
|
if isinstance(ctx_or_interaction, commands.Context):
|
|
if ctx_or_interaction.author.id == user_id:
|
|
try:
|
|
await ctx_or_interaction.send(content=error_message)
|
|
except discord.Forbidden:
|
|
await ctx_or_interaction.send("Unable to send you a DM with the error details.")
|
|
else:
|
|
await ctx_or_interaction.send("An error occurred while processing your command.")
|
|
else:
|
|
if ctx_or_interaction.user.id == user_id:
|
|
await ctx_or_interaction.response.send_message(content=error_message, ephemeral=True)
|
|
else:
|
|
await ctx_or_interaction.response.send_message("An error occurred while processing your command.")
|
|
|
|
|
|
|