209 lines
8.7 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():
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.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)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter(debug_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():
# 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
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
await bot.add_cog(MediaCommands(bot))
await bot.add_cog(RequestCommands(bot))
await bot.add_cog(UtilityCommands(bot))
logger.info("Loaded all extensions")
@bot.event
async def on_ready():
logger.info(f'Logged in as {bot.user} (ID: {bot.user.id})')
logger.info(f'Connected to {len(bot.guilds)} guilds')
for guild in bot.guilds:
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")
@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):
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
error_details = f"Command error in {ctx.command} 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):
await ctx.send(f"Jellyseerr API error: {str(original).split('API Error')[1].strip()}")
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():
try:
async with bot:
logger.info("Starting Discord Jellyseerr Bot")
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()
await bot.start(config.BOT_TOKEN)
except KeyboardInterrupt:
logger.info("Bot shutting down by keyboard interrupt")
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 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)