273 lines
11 KiB
Python
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) |