2025-05-25 16:42:08 +02:00

273 lines
11 KiB
Python

import os
import logging
from logging.handlers import RotatingFileHandler
import discord
from discord.ext import commands
import asyncio
import config
import sys
import traceback
# Set up logging
def setup_logging():
"""Configure and set up the logging system for the bot.
Returns:
logging.Logger: The configured logger for the bot
"""
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
detailed_log_format = '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
log_level = logging.DEBUG if config.DEBUG_MODE else logging.INFO
# Create logs directory if it doesn't exist
os.makedirs('logs', exist_ok=True)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# Clear existing handlers to avoid duplication
if root_logger.handlers:
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter(detailed_log_format if config.DEBUG_MODE else log_format))
console_handler.setLevel(log_level)
# File handler with rotation (5 files, 5MB each)
file_handler = RotatingFileHandler(
filename='logs/bot.log',
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=5,
encoding='utf-8',
mode='a' # Append mode instead of overwrite
)
file_handler.setFormatter(logging.Formatter(detailed_log_format))
file_handler.setLevel(logging.DEBUG if config.DEBUG_MODE else log_level)
# Error file handler with rotation (5 files, 5MB each)
error_file_handler = RotatingFileHandler(
filename='logs/error.log',
maxBytes=5 * 1024 * 1024, # 5MB
backupCount=5,
encoding='utf-8',
mode='a' # Append mode instead of overwrite
)
error_file_handler.setFormatter(logging.Formatter(detailed_log_format))
error_file_handler.setLevel(logging.ERROR)
# Add handlers
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(error_file_handler)
return logging.getLogger('bot')
logger = setup_logging()
# Initialize the bot with intents
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
# Create the bot instance
bot = commands.Bot(
command_prefix=config.BOT_PREFIX,
intents=intents,
help_command=None # Disable default help command
)
# Create API client variable (will be initialized later)
jellyseerr_api = None
# Load all cogs
async def load_extensions():
"""Load all command extensions and initialize the API client."""
# Import here to avoid circular imports
from commands import MediaCommands, RequestCommands
from utility_commands import UtilityCommands
from jellyseerr_api import JellyseerrAPI
# Initialize the API client
global jellyseerr_api
try:
jellyseerr_api = JellyseerrAPI(config.JELLYSEERR_URL, config.JELLYSEERR_EMAIL, config.JELLYSEERR_PASSWORD)
# Make API client available to command modules
import sys
sys.modules['jellyseerr_api'].jellyseerr_api = jellyseerr_api
# Add all cogs
logger.info("Loading command extensions...")
await bot.add_cog(MediaCommands(bot))
await bot.add_cog(RequestCommands(bot))
await bot.add_cog(UtilityCommands(bot))
logger.info("Loaded all extensions")
except Exception as e:
logger.error(f"Failed to initialize extensions: {str(e)}", exc_info=True)
raise
@bot.event
async def on_ready():
"""Handler for when the bot has connected to Discord and is ready."""
logger.info(f'Logged in as {bot.user} (ID: {bot.user.id})')
logger.info(f'Connected to {len(bot.guilds)} guilds')
# List connected guilds
guild_list = []
for guild in bot.guilds:
guild_list.append(f' - {guild.name} (ID: {guild.id})')
logger.info(f' - {guild.name} (ID: {guild.id})')
logger.info(f'Using prefix: {config.BOT_PREFIX}')
logger.info(f'Jellyseerr URL: {config.JELLYSEERR_URL}')
# Test Jellyseerr authentication
if config.JELLYSEERR_LOCAL_LOGIN:
try:
logger.info(f'Authenticating with Jellyseerr using email: {config.JELLYSEERR_EMAIL}')
if config.DEBUG_MODE:
logger.debug(f'Jellyseerr URL: {config.JELLYSEERR_URL}')
logger.debug(f'Using local login with email: {config.JELLYSEERR_EMAIL}')
await jellyseerr_api.login()
logger.info('Successfully authenticated with Jellyseerr')
except Exception as e:
logger.error(f'Failed to authenticate with Jellyseerr: {str(e)}', exc_info=True)
logger.warning('The bot will attempt to reconnect when commands are used')
if config.DEBUG_MODE:
logger.debug(f'Authentication error details: {traceback.format_exc()}')
logger.debug('Check that:')
logger.debug('1. Your Jellyseerr URL is correct')
logger.debug('2. Local login is enabled in Jellyseerr settings')
logger.debug('3. The email and password are correct')
logger.debug('4. The user account has not been disabled')
else:
logger.info('Local login disabled, proceeding without authentication')
# Set bot activity
activity = discord.Activity(
type=discord.ActivityType.watching,
name=f"{config.BOT_PREFIX}help | Jellyseerr"
)
await bot.change_presence(activity=activity)
logger.info("Bot is ready and fully operational")
# Print a nice summary to the log
bot_info = [
"=====================================",
"Discord Jellyseerr Bot Ready",
"=====================================",
f"Bot User: {bot.user}",
f"Bot ID: {bot.user.id}",
f"Prefix: {config.BOT_PREFIX}",
f"Guilds: {len(bot.guilds)}",
f"API URL: {config.JELLYSEERR_URL}",
"====================================="
]
logger.info("\n".join(bot_info))
@bot.event
async def on_command(ctx):
logger.info(f"Command '{ctx.command}' invoked by {ctx.author} in {ctx.guild}/{ctx.channel}")
@bot.event
async def on_command_completion(ctx):
logger.info(f"Command '{ctx.command}' completed successfully for {ctx.author}")
@bot.event
async def on_command_error(ctx, error):
"""Handle command errors and provide user-friendly responses.
Args:
ctx: The context of the command
error: The error that occurred
"""
if isinstance(error, commands.CommandNotFound):
logger.debug(f"Command not found: {ctx.message.content}")
return
elif isinstance(error, commands.MissingRequiredArgument):
logger.info(f"Missing required argument: {error.param.name} for command {ctx.command} by user {ctx.author}")
await ctx.send(f"Missing required argument: `{error.param.name}`. Type `{config.BOT_PREFIX}help {ctx.command}` for correct usage.")
elif isinstance(error, commands.BadArgument):
logger.info(f"Bad argument for command {ctx.command} by user {ctx.author}: {str(error)}")
await ctx.send(f"Invalid argument: `{str(error)}`. Please check your input and try again.")
elif isinstance(error, commands.MissingPermissions):
logger.info(f"Permission denied for {ctx.author} trying to use {ctx.command}")
await ctx.send("You don't have permission to use this command.")
elif isinstance(error, commands.CommandOnCooldown):
logger.info(f"Command on cooldown for {ctx.author} trying to use {ctx.command}")
await ctx.send(f"This command is on cooldown. Please try again in {error.retry_after:.1f} seconds.")
elif isinstance(error, commands.DisabledCommand):
logger.info(f"Disabled command {ctx.command} attempted by {ctx.author}")
await ctx.send("This command is currently disabled.")
elif isinstance(error, commands.NoPrivateMessage):
logger.info(f"Command {ctx.command} not allowed in DMs, attempted by {ctx.author}")
await ctx.send("This command cannot be used in private messages.")
else:
# Get original error if it's wrapped in CommandInvokeError
original = error.original if hasattr(error, 'original') else error
# Create a detailed error message for logging
command_name = ctx.command.name if ctx.command else "unknown"
error_details = f"Command error in {command_name} invoked by {ctx.author}: {str(original)}"
logger.error(error_details, exc_info=True)
# Create a user-friendly error message
if "400" in str(original) and "must be url encoded" in str(original).lower():
await ctx.send("Error processing your request: Your input contains special characters that couldn't be processed. Please try again with simpler text.")
elif "API Error" in str(original):
try:
error_msg = str(original).split('API Error')[1].strip()
await ctx.send(f"Jellyseerr API error: {error_msg}")
except:
await ctx.send(f"Jellyseerr API error: {str(original)}")
elif "Connection error" in str(original):
await ctx.send(f"Connection error: Cannot connect to Jellyseerr. Please check if the server is running and try again later.")
elif "timeout" in str(original).lower():
await ctx.send(f"The request timed out. The Jellyseerr server might be slow or unreachable. Please try again later.")
else:
await ctx.send(f"An error occurred while processing your command. Please try again later or check the syntax with `{config.BOT_PREFIX}help {ctx.command}`.")
async def main():
"""Main function to initialize and start the bot."""
try:
async with bot:
logger.info("Starting Discord Jellyseerr Bot")
logger.info(f"Bot version: 1.0.0")
logger.info(f"Python version: {sys.version}")
logger.info(f"Discord.py version: {discord.__version__}")
if config.JELLYSEERR_LOCAL_LOGIN:
logger.info(f"Using Jellyseerr authentication with account: {config.JELLYSEERR_EMAIL}")
else:
logger.info("Local login disabled, some functionality may be limited")
if config.DEBUG_MODE:
logger.debug("DEBUG MODE ENABLED - verbose logging activated")
await load_extensions()
logger.info("Connecting to Discord...")
await bot.start(config.BOT_TOKEN)
except KeyboardInterrupt:
logger.info("Bot shutting down by keyboard interrupt")
except discord.LoginFailure:
logger.critical("Invalid Discord token. Please check your DISCORD_BOT_TOKEN in the .env file.", exc_info=True)
sys.exit(1)
except Exception as e:
logger.critical(f"Fatal error: {str(e)}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Bot shut down by keyboard interrupt")
sys.exit(0)
except Exception as e:
print(f"Fatal error occurred: {str(e)}")
logging.critical(f"Fatal error in main process: {str(e)}", exc_info=True)
sys.exit(1)