From e2c8b7a941bb69c5aed8e098f76d6ed2b6247b33 Mon Sep 17 00:00:00 2001 From: Zach Lagden Date: Mon, 2 Sep 2024 20:37:10 +0000 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20to=20match=20la?= =?UTF-8?q?gden.dev=20comment=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 70 +++++-- cogs/rickbot/cmds_botinfo.py | 87 ++++++--- cogs/rickbot/cmds_botutils.py | 120 +++++++++--- cogs/rickbot/slashcmds_botinfo.py | 80 ++++++-- cogs/rickbot/slashcmds_botutils.py | 83 ++++++--- config.py | 49 ++++- db.py | 68 +++++-- helpers/__init__.py | 28 ++- helpers/colors.py | 15 +- helpers/custom/__init__.py | 28 ++- helpers/errors.py | 105 ++++++----- helpers/logs.py | 136 ++++++++++---- helpers/rickbot.py | 56 ++++-- rickbot/__init__.py | 16 +- rickbot/main.py | 285 ++++++++++++++++++++--------- 15 files changed, 892 insertions(+), 334 deletions(-) diff --git a/app.py b/app.py index c943feb..d9b140e 100644 --- a/app.py +++ b/app.py @@ -1,36 +1,84 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -Runs the bot. +This script is responsible for running the bot. It sets up the necessary environment, handles signals for +graceful shutdowns, and initiates the bot's main loop. The bot's core functionality is encapsulated in the +RickBot class, which is imported and used here. """ -import asyncio -import signal -import os +# Import the required modules -from rickbot.main import RickBot +# Python Standard Library +# ----------------------- +from typing import ( + NoReturn, +) # The build in typing module, used for type hints and annotations. +import asyncio # For handling asynchronous operations, which are crucial for the bot's functionality. +import signal # To handle signals like SIGTERM and SIGINT, allowing for graceful shutdowns. +import os # Provides a way to interact with the operating system, specifically for handling file paths. -abspath = os.path.abspath(__file__) -dname = os.path.dirname(abspath) -os.chdir(dname) +# Internal Modules +# ------------------------ +from rickbot.main import ( + RickBot, +) # Importing the RickBot class, which contains the core logic and behavior of the bot. + +# Adjust the current working directory +# ------------------------------------ +# This ensures that the script's directory is the working directory, no matter where it's run from. +# It's a safeguard to ensure that all relative paths within the bot's operations are correctly resolved. +abspath = os.path.abspath(__file__) # Get the absolute path of this file. +dname = os.path.dirname(abspath) # Determine the directory name of this file. +os.chdir(dname) # Change the current working directory to this file's directory. + +# Initialize the bot +# ------------------ +# Create an instance of RickBot, which will be used to run and manage the bot's operations. +# This instance encapsulates all the methods and properties necessary for the bot to function. bot = RickBot() -async def main(): +async def main() -> NoReturn: + """ + The main function responsible for starting and managing the bot. + + This function sets up signal handlers for graceful shutdowns and then starts the bot. + The signal handlers listen for SIGTERM and SIGINT, which are signals for termination + and interruption (e.g., Ctrl+C), respectively. When such a signal is received, the bot's + shutdown procedure is initiated. + + Returns: + NoReturn: This function does not return anything. + """ + # Set up signal handlers for graceful shutdown + # -------------------------------------------- + # This loop iterates over the signals SIGTERM and SIGINT, adding a handler for each. + # The handler triggers the bot's shutdown method, allowing for a clean exit. for s in [signal.SIGTERM, signal.SIGINT]: asyncio.get_event_loop().add_signal_handler( s, lambda s=s: asyncio.create_task(bot.shutdown(s)) ) + # Start the bot + # ------------- + # This line initiates the bot's main functionality, entering its event loop until shutdown is triggered. await bot.start_bot() +# Entry point for the script +# -------------------------- +# This block ensures that the script can be run as a standalone program. +# It tries to run the main asynchronous function, handling common exit scenarios gracefully. if __name__ == "__main__": try: + # Run the main asynchronous function asyncio.run(main()) except (KeyboardInterrupt, SystemExit): + # Handle common exit scenarios like keyboard interruption (Ctrl+C) or system exit. print("Bot shutdown requested, exiting...") except Exception as e: + # Catch any other exceptions that might occur and log them. print(f"Unhandled exception: {e}") diff --git a/cogs/rickbot/cmds_botinfo.py b/cogs/rickbot/cmds_botinfo.py index 23c9f4a..c7fa052 100644 --- a/cogs/rickbot/cmds_botinfo.py +++ b/cogs/rickbot/cmds_botinfo.py @@ -1,29 +1,49 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -This cog provides commands to get information about the bot, this is a part of the RickBot default cog set. +This cog provides commands to get information about the bot, and it is part of the RickBot default cog set. """ -# Python standard library -from datetime import datetime - -# Third-party libraries -from discord_timestamps import format_timestamp, TimestampType -from discord.ext import commands -import discord -import requests - -# Helper functions -from helpers.colors import MAIN_EMBED_COLOR, ERROR_EMBED_COLOR +# Python Standard Library +# ------------------------ +from datetime import ( + datetime, +) # Used for parsing and formatting date and time information + +# Third Party Libraries +# --------------------- +from discord_timestamps import ( + format_timestamp, + TimestampType, +) # Helps format timestamps for Discord messages +from discord.ext import commands # Used for defining Discord bot commands and cogs +import discord # Core library for interacting with Discord's API +import requests # Handles HTTP requests, used here for interacting with the GitHub API + +# Internal Modules +# ---------------- +from helpers.colors import ( + MAIN_EMBED_COLOR, + ERROR_EMBED_COLOR, +) # Predefined color constants for Discord embeds # Config -from config import CONFIG +# ------ +from config import CONFIG # Imports the bot's configuration settings # Custom Exceptions class InvalidGitHubURL(Exception): - def __init__(self, message="Invalid GitHub URL"): + """ + Exception raised when an invalid GitHub URL is encountered. + + Attributes: + message (str): The error message explaining the issue. + """ + + def __init__(self, message: str = "Invalid GitHub URL"): self.message = message super().__init__(self.message) @@ -38,6 +58,9 @@ def convert_repo_url_to_api(url: str) -> str: Returns: str: The corresponding GitHub API URL for commits. + + Raises: + ValueError: If the provided URL is invalid. """ # Split the URL by slashes parts = url.rstrip("/").split("/") @@ -57,7 +80,16 @@ def convert_repo_url_to_api(url: str) -> str: # Cog class RickBot_BotInfoCommands(commands.Cog): - def __init__(self, bot): + """ + Cog for RickBot that provides commands to retrieve information about the bot, including recent GitHub updates. + + Attributes: + bot (commands.Bot): The instance of the bot. + GITHUB_REPO (str): The GitHub repository URL configured for the bot. + GITHUB_API (str): The GitHub API URL derived from the repository URL. + """ + + def __init__(self, bot: commands.Bot): self.bot = bot self.GITHUB_REPO = CONFIG["REPO"]["url"] @@ -68,12 +100,14 @@ def __init__(self, bot): self.GITHUB_API = None @commands.command(name="updates") - async def _updates(self, ctx): - """ - Check GitHub for the latest commits, provides the last 5 along with other relevant information. + async def _updates(self, ctx: commands.Context): """ + Check GitHub for the latest commits and provide details about the last 5 commits. - if self.GITHUB_API is None: + Args: + ctx (commands.Context): The command context. + """ + if self.GITHUB_API in [None, ""]: embed = discord.Embed( title="Sorry!", description="This command is disabled.", @@ -164,9 +198,12 @@ async def _updates(self, ctx): await ctx.message.reply(embed=embed, mention_author=False) @commands.command(name="ping") - async def _ping(self, ctx): + async def _ping(self, ctx: commands.Context): """ Check the bot's latency. + + Args: + ctx (commands.Context): The command context. """ embed = discord.Embed( title="Pong!", @@ -178,4 +215,10 @@ async def _ping(self, ctx): async def setup(bot: commands.Bot): + """ + Setup function to add this cog to the bot. + + Args: + bot (commands.Bot): The instance of the bot. + """ await bot.add_cog(RickBot_BotInfoCommands(bot)) diff --git a/cogs/rickbot/cmds_botutils.py b/cogs/rickbot/cmds_botutils.py index 4669d82..f926e7f 100644 --- a/cogs/rickbot/cmds_botutils.py +++ b/cogs/rickbot/cmds_botutils.py @@ -1,38 +1,69 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -This cog counts the number of times a user has said the n-word in the server and provides a command to check the records, -as well as a leaderboard command that shows the top 10 users with the most n-word records. +This cog provides utility commands for RickBot, allowing the bot owner to execute, evaluate, and manage code and commands directly through Discord. +It also includes error handling and a test error command. """ -# Python standard library -import subprocess +# Python Standard Library +# ------------------------ +import subprocess # Used for running shell commands from within Python code -# Third-party libraries -from discord.ext import commands -import discord +# Third Party Libraries +# --------------------- +from discord.ext import commands # Used for defining Discord bot commands and cogs +import discord # Core library for interacting with Discord's API -# Helper functions -from helpers.colors import MAIN_EMBED_COLOR, ERROR_EMBED_COLOR -from helpers.errors import handle_error +# Internal Modules +# ---------------- +from helpers.colors import ( + MAIN_EMBED_COLOR, + ERROR_EMBED_COLOR, +) # Predefined color constants for Discord embeds +from helpers.errors import handle_error # Custom error handling function # Config -from config import CONFIG +# ------s +from config import CONFIG # Imports the bot's configuration settings +# Cog class RickBot_BotUtilsCommands(commands.Cog): - def __init__(self, bot): + """ + Cog for RickBot that provides utility commands for the bot owner. + + This includes commands for evaluating code, executing code, running shell commands, and a command for testing error handling. + + Attributes: + bot (commands.Bot): The instance of the bot. + """ + + def __init__(self, bot: commands.Bot): self.bot = bot - def botownercheck(ctx): + def botownercheck(ctx: commands.Context) -> bool: + """ + Check if the user is the bot owner. + + Args: + ctx (commands.Context): The command context. + + Returns: + bool: True if the user is the bot owner, False otherwise. + """ return ctx.author.id == int(CONFIG["MAIN"]["dev"]) @commands.command() @commands.check(botownercheck) async def eval(self, ctx: commands.Context, *, code: str): """ - Evaluate code. + Evaluate a string of Python code. + + Args: + ctx (commands.Context): The command context. + code (str): The Python code to evaluate. """ try: str_output = str(eval(code)) @@ -45,7 +76,14 @@ async def eval(self, ctx: commands.Context, *, code: str): await ctx.reply(embed=embed, mention_author=False) @eval.error - async def eval_error(self, ctx, error): + async def eval_error(self, ctx: commands.Context, error: commands.CommandError): + """ + Error handler for the eval command. + + Args: + ctx (commands.Context): The command context. + error (commands.CommandError): The exception raised during command execution. + """ if isinstance(error, commands.CheckFailure): embed = discord.Embed( title="Error", @@ -53,7 +91,6 @@ async def eval_error(self, ctx, error): color=ERROR_EMBED_COLOR, ) await ctx.reply(embed=embed, mention_author=False) - else: await handle_error(ctx, error) @@ -61,7 +98,11 @@ async def eval_error(self, ctx, error): @commands.check(botownercheck) async def exec(self, ctx: commands.Context, *, code: str): """ - Execute code. + Execute a string of Python code. + + Args: + ctx (commands.Context): The command context. + code (str): The Python code to execute. """ try: exec(code) @@ -75,7 +116,14 @@ async def exec(self, ctx: commands.Context, *, code: str): await ctx.reply(embed=embed, mention_author=False) @exec.error - async def exec_error(self, ctx, error): + async def exec_error(self, ctx: commands.Context, error: commands.CommandError): + """ + Error handler for the exec command. + + Args: + ctx (commands.Context): The command context. + error (commands.CommandError): The exception raised during command execution. + """ if isinstance(error, commands.CheckFailure): embed = discord.Embed( title="Error", @@ -83,7 +131,6 @@ async def exec_error(self, ctx, error): color=ERROR_EMBED_COLOR, ) await ctx.reply(embed=embed, mention_author=False) - else: await handle_error(ctx, error) @@ -91,9 +138,12 @@ async def exec_error(self, ctx, error): @commands.check(botownercheck) async def cmd(self, ctx: commands.Context, *, cmd: str): """ - Run a command. - """ + Run a shell command. + Args: + ctx (commands.Context): The command context. + cmd (str): The shell command to run. + """ try: str_output = subprocess.check_output(cmd, shell=True, text=True) except subprocess.CalledProcessError as e: @@ -102,11 +152,17 @@ async def cmd(self, ctx: commands.Context, *, cmd: str): embed = discord.Embed( title="Command", description=f"```{str_output}```", color=MAIN_EMBED_COLOR ) - await ctx.reply(embed=embed, mention_author=False) @cmd.error - async def cmd_error(self, ctx, error): + async def cmd_error(self, ctx: commands.Context, error: commands.CommandError): + """ + Error handler for the cmd command. + + Args: + ctx (commands.Context): The command context. + error (commands.CommandError): The exception raised during command execution. + """ if isinstance(error, commands.CheckFailure): embed = discord.Embed( title="Error", @@ -114,7 +170,6 @@ async def cmd_error(self, ctx, error): color=ERROR_EMBED_COLOR, ) await ctx.reply(embed=embed, mention_author=False) - else: await handle_error(ctx, error) @@ -122,13 +177,20 @@ async def cmd_error(self, ctx, error): @commands.check(botownercheck) async def testerror(self, ctx: commands.Context): """ - Cause an error. - """ + Trigger an error for testing purposes. + Args: + ctx (commands.Context): The command context. + """ await ctx.message.add_reaction("👌") - raise Exception("Test error.") async def setup(bot: commands.Bot): + """ + Setup function to add this cog to the bot. + + Args: + bot (commands.Bot): The instance of the bot. + """ await bot.add_cog(RickBot_BotUtilsCommands(bot)) diff --git a/cogs/rickbot/slashcmds_botinfo.py b/cogs/rickbot/slashcmds_botinfo.py index 817dce7..5c9bdd1 100644 --- a/cogs/rickbot/slashcmds_botinfo.py +++ b/cogs/rickbot/slashcmds_botinfo.py @@ -1,30 +1,50 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -This cog provides commands to get information about the bot, this is a part of the RickBot default cog set. +This cog provides commands to get information about the bot, which is a part of the RickBot default cog set. """ -# Python standard library -from datetime import datetime - -# Third-party libraries -from discord_timestamps import format_timestamp, TimestampType -from discord.ext import commands -from discord import app_commands -import discord -import requests - -# Helper functions -from helpers.colors import MAIN_EMBED_COLOR, ERROR_EMBED_COLOR +# Python Standard Library +# ----------------------- +from datetime import ( + datetime, +) # Used for parsing and formatting date and time information + +# Third Party Libraries +# --------------------- +from discord_timestamps import ( + format_timestamp, + TimestampType, +) # Helps format timestamps for Discord messages +from discord.ext import commands # Used for defining Discord bot commands and cogs +from discord import app_commands # Supports slash commands in Discord bots +import discord # Core library for interacting with Discord's API +import requests # Handles HTTP requests, used here for interacting with the GitHub API + +# Internal Modules +# ---------------- +from helpers.colors import ( + MAIN_EMBED_COLOR, + ERROR_EMBED_COLOR, +) # Predefined color constants for Discord embeds # Config -from config import CONFIG +# ------ +from config import CONFIG # Imports the bot's configuration settings # Custom Exceptions class InvalidGitHubURL(Exception): - def __init__(self, message="Invalid GitHub URL"): + """ + Exception raised when an invalid GitHub URL is encountered. + + Attributes: + message (str): The error message explaining the issue. + """ + + def __init__(self, message: str = "Invalid GitHub URL"): self.message = message super().__init__(self.message) @@ -39,6 +59,9 @@ def convert_repo_url_to_api(url: str) -> str: Returns: str: The corresponding GitHub API URL for commits. + + Raises: + ValueError: If the provided URL is invalid. """ # Split the URL by slashes parts = url.rstrip("/").split("/") @@ -58,7 +81,16 @@ def convert_repo_url_to_api(url: str) -> str: # Cog class RickBot_BotInfoSlashCommands(commands.Cog): - def __init__(self, bot): + """ + Cog for RickBot that provides slash commands to retrieve bot information. + + Attributes: + bot (commands.Bot): The instance of the bot. + GITHUB_REPO (str): The GitHub repository URL configured for the bot. + GITHUB_API (str): The GitHub API URL derived from the repository URL. + """ + + def __init__(self, bot: commands.Bot): self.bot = bot self.GITHUB_REPO = CONFIG["REPO"]["url"] @@ -68,7 +100,9 @@ def __init__(self, bot): else: self.GITHUB_API = None - async def _send_embed(self, interaction, title, description, color): + async def _send_embed( + self, interaction: discord.Interaction, title: str, description: str, color: int + ): """Helper to send formatted Discord embeds.""" embed = discord.Embed(title=title, description=description, color=color) await interaction.response.send_message(embed=embed, ephemeral=True) @@ -81,7 +115,7 @@ async def _updates(self, interaction: discord.Interaction): Check GitHub for the latest commits, provides the last 5 along with other relevant information. """ - if self.GITHUB_API is None: + if self.GITHUB_API in [None, ""]: await self._send_embed( interaction, "Sorry!", @@ -183,4 +217,10 @@ async def _ping(self, interaction: discord.Interaction): async def setup(bot: commands.Bot): + """ + Setup function to add this cog to the bot. + + Args: + bot (commands.Bot): The instance of the bot. + """ await bot.add_cog(RickBot_BotInfoSlashCommands(bot)) diff --git a/cogs/rickbot/slashcmds_botutils.py b/cogs/rickbot/slashcmds_botutils.py index ce67fd2..e123618 100644 --- a/cogs/rickbot/slashcmds_botutils.py +++ b/cogs/rickbot/slashcmds_botutils.py @@ -1,35 +1,52 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. This cog provides utility commands for bot developers, such as evaluating code, executing commands, and testing errors. These commands are restricted to bot developers only for security purposes. """ -# Python standard library -import os -import subprocess - -# Third-party libraries -from discord.ext import commands -from discord import app_commands -import discord - -# Helper functions -from helpers.colors import MAIN_EMBED_COLOR, ERROR_EMBED_COLOR -from helpers.errors import handle_error +# Python Standard Library +# ----------------------- +import os # os is used for interacting with the operating system, particularly to execute system commands and manage processes. +import subprocess # subprocess is used for running system commands and capturing their output. + +# Third Party Libraries +# --------------------- +from discord.ext import ( + commands, +) # commands is used for creating bot commands and cogs within the Discord bot framework. +from discord import ( + app_commands, +) # app_commands is used for defining and managing slash commands in Discord. +import discord # discord is the main library used for interacting with the Discord API. + +# Internal Modules +# ---------------- +from helpers.colors import ( + MAIN_EMBED_COLOR, + ERROR_EMBED_COLOR, +) # MAIN_EMBED_COLOR and ERROR_EMBED_COLOR are color codes used in embeds to maintain consistency in the bot's UI. +from helpers.errors import ( + handle_error, +) # handle_error is a custom function used to manage errors within commands, providing a standardized response. # Config -from config import CONFIG +# ------ +from config import ( + CONFIG, +) # CONFIG is a configuration object used to access settings and constants across the bot. +# Cog class RickBot_BotUtilsSlashCommands(commands.Cog): """ This cog contains utility commands intended for bot developers, allowing them to evaluate Python code, execute system commands, and test error handling. """ - def __init__(self, bot): + def __init__(self, bot: commands.Bot) -> None: """ Initializes the cog with the bot instance. @@ -57,7 +74,9 @@ def botownercheck(interaction: discord.Interaction) -> bool: """ return interaction.user.id == int(CONFIG["MAIN"]["dev"]) - async def _send_embed(self, interaction, title, description, color): + async def _send_embed( + self, interaction: discord.Interaction, title: str, description: str, color: int + ) -> None: """ Helper function to send formatted Discord embeds. @@ -74,7 +93,7 @@ async def _send_embed(self, interaction, title, description, color): name="eval", description="Evaluate Python code. Restricted to bot developers." ) @app_commands.check(botownercheck) - async def eval(self, interaction: discord.Interaction, *, code: str): + async def eval(self, interaction: discord.Interaction, *, code: str) -> None: """ Evaluates the provided Python code and returns the result. @@ -95,7 +114,9 @@ async def eval(self, interaction: discord.Interaction, *, code: str): ) @eval.error - async def eval_error(self, interaction: discord.Interaction, error): + async def eval_error( + self, interaction: discord.Interaction, error: commands.CommandError + ) -> None: """ Handles errors for the eval command. @@ -119,7 +140,7 @@ async def eval_error(self, interaction: discord.Interaction, error): name="exec", description="Execute Python code. Restricted to bot developers." ) @app_commands.check(botownercheck) - async def exec(self, interaction: discord.Interaction, *, code: str): + async def exec(self, interaction: discord.Interaction, *, code: str) -> None: """ Executes the provided Python code. @@ -141,7 +162,9 @@ async def exec(self, interaction: discord.Interaction, *, code: str): ) @exec.error - async def exec_error(self, interaction: discord.Interaction, error): + async def exec_error( + self, interaction: discord.Interaction, error: commands.CommandError + ) -> None: """ Handles errors for the exec command. @@ -165,7 +188,7 @@ async def exec_error(self, interaction: discord.Interaction, error): name="cmd", description="Run a system command. Restricted to bot developers." ) @app_commands.check(botownercheck) - async def cmd(self, interaction: discord.Interaction, *, cmd: str): + async def cmd(self, interaction: discord.Interaction, *, cmd: str) -> None: """ Runs the specified system command and returns the output. @@ -186,7 +209,9 @@ async def cmd(self, interaction: discord.Interaction, *, cmd: str): ) @cmd.error - async def cmd_error(self, interaction: discord.Interaction, error): + async def cmd_error( + self, interaction: discord.Interaction, error: commands.CommandError + ) -> None: """ Handles errors for the cmd command. @@ -211,7 +236,7 @@ async def cmd_error(self, interaction: discord.Interaction, error): description="Test error handling. Restricted to bot developers.", ) @app_commands.check(botownercheck) - async def testerror(self, interaction: discord.Interaction): + async def testerror(self, interaction: discord.Interaction) -> None: """ Raises a test error to verify error handling. @@ -223,7 +248,9 @@ async def testerror(self, interaction: discord.Interaction): raise Exception("Test error raised.") @testerror.error - async def testerror_error(self, interaction: discord.Interaction, error): + async def testerror_error( + self, interaction: discord.Interaction, error: commands.CommandError + ) -> None: """ Handles errors for the testerror command. @@ -247,12 +274,12 @@ async def testerror_error(self, interaction: discord.Interaction, error): name="restart", description="Restart the bot. Restricted to bot developers." ) @commands.check(botownercheck) - async def restart(self, interaction: discord.Interaction): + async def restart(self, interaction: discord.Interaction) -> None: """ Restarts the bot. Args: - ctx (commands.Context): The context of the command. + interaction (discord.Interaction): The interaction that triggered the command. """ if not CONFIG["advanced"]["linux_service_name"]: @@ -268,7 +295,7 @@ async def restart(self, interaction: discord.Interaction): ) -async def setup(bot: commands.Bot): +async def setup(bot: commands.Bot) -> None: """ Sets up the cog by adding it to the bot. diff --git a/config.py b/config.py index 65d98df..63c9f43 100644 --- a/config.py +++ b/config.py @@ -1,24 +1,63 @@ -import os -import configparser +""" +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -from helpers.logs import RICKLOG_MAIN +This script handles the configuration setup for the bot, ensuring that both the standard and custom config files +are available and correctly loaded. It uses the `configparser` module to manage these configurations. +The script checks for the existence of main and custom configuration files, reads them, and initializes parsers +to manage configuration data. If any configuration files are missing, it either logs a warning or creates a basic +custom configuration file. +""" + +# Import the required modules + +# Python Standard Library +import os # os is used for interacting with the file system, checking the existence of files, and handling file operations. +import configparser # configparser is used to parse and manage configuration files in the INI format. + +# Internal Helpers +from helpers.logs import ( + RICKLOG_MAIN, +) # Importing a logging utility from the helpers.logs module to log messages regarding the configuration process. + +# Initialize config parsers +# ------------------------- +# CONFIG will handle the main configuration, while CUSTOM_CONFIG will manage any custom settings. +# These ConfigParser instances will store and manage the configuration data read from the files. CONFIG = configparser.ConfigParser() CUSTOM_CONFIG = configparser.ConfigParser() # Ensure the config file exists +# ----------------------------- +# Check if the main configuration file "config.ini" exists in the current directory. +# If it doesn't exist, the script logs a warning and exits, as the bot cannot function without this core configuration. if not os.path.exists("config.ini"): RICKLOG_MAIN.warning("Config file not found, exiting.") - exit(1) + exit(1) # Exit the script with a status code of 1 to indicate an error condition. # Read the config file +# -------------------- +# Load the configuration from "config.ini" into the CONFIG parser. +# The read method loads the contents of the file, allowing subsequent code to access the configuration settings. CONFIG.read("config.ini") # Ensure the custom config file exists +# ------------------------------------ +# Check if the custom configuration file "custom_config.ini" exists. If it doesn't, log a warning and create a basic one. +# This ensures that the bot has a place to store user-defined overrides or additional settings, which are crucial for customization. if not os.path.exists("custom_config.ini"): RICKLOG_MAIN.warning("Custom config file not found, creating one.") + # Open "custom_config.ini" in write mode. If it doesn't exist, it will be created. + # The file is initialized with a [DEFAULT] section to ensure it's a valid INI file structure. with open("custom_config.ini", "w+") as f: - f.write("[DEFAULT]\n") + f.write("[DEFAULT]\n") # Write a default section header to the file. # Read the custom config file +# --------------------------- +# Load the custom configuration from "custom_config.ini" into the CUSTOM_CONFIG parser. +# This allows the script to merge or override the main configuration with user-provided settings. CUSTOM_CONFIG.read("custom_config.ini") + +print("PREFIX: `" + CONFIG["BOT"]["prefix"] + "`") diff --git a/db.py b/db.py index 11d48da..d5e9c3a 100644 --- a/db.py +++ b/db.py @@ -1,26 +1,66 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -This is the database file for the AR15 website. It contains the database connection logic. +This module handles the database interactions for the bot, specifically focusing on MongoDB operations. +It sets up the connection to the database, provides access to various collections, and offers utility +functions for obtaining the MongoDB client. """ -# Import the required modules - # Third Party Modules -from pymongo.mongo_client import MongoClient -from pymongo.server_api import ServerApi +# ------------------- +from pymongo.mongo_client import ( + MongoClient, +) # The official MongoDB driver for Python, used to interact with the database. +from pymongo.server_api import ( + ServerApi, +) # Allows locking the API version to ensure compatibility. -# Import configuration +# Internal Modules +# ---------------- from config import CONFIG -client = MongoClient(CONFIG["mongo"]["uri"], server_api=ServerApi("1")) -bot_db = client["bot"] -messages_collection = bot_db["messages"] -money_collection = bot_db["money"] -invites_collection = bot_db["invites"] +# Initialize the MongoDB client +# ----------------------------- +# Here we're creating a MongoClient instance using the URI specified in the CONFIG. +# The ServerApi parameter is used to lock the API version to "1", ensuring compatibility and stability. +client = MongoClient(CONFIG["DB"]["mongo_uri"], server_api=ServerApi("1")) + +# Database Access +# ------------------------------ +# Selecting the database to use with the bot. +# The database name is fetched from the configuration file, allowing easy switching of databases by +# simply modifying the configuration, without needing to alter the codebase. +bot_db = client[CONFIG["DB"]["bot_db"]] + +# Collection Access +# ------------------------------ +# Here you can define access to various collections within the bot's database. +# Each collection represents a distinct type of data. +# Example: +# logs_collection = bot_db["logs"] # The 'logs' collection, used for storing log data + + +def get_mongo_client() -> MongoClient: + """ + Returns the MongoClient instance for interacting with MongoDB. + + This function provides access to the MongoDB client that was initialized + at the start of the module. It's useful when you need to perform operations + with the database from other parts of your codebase without directly + instantiating the client again. + + Returns: + MongoClient: The MongoClient instance initialized with the URI and server API version. + Example: + To retrieve the client and access the database: -def get_mongo_client(): + ``` + client = get_mongo_client() + database = client["my_database"] + ``` + """ return client diff --git a/helpers/__init__.py b/helpers/__init__.py index 5d0b710..95477a4 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py @@ -1,18 +1,30 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -The helpers module contains all the helper functions and classes used in rickbot. +The helpers module contains all the helper functions and classes used in RickBot. """ -"""Import all modules that exist in the current directory.""" -# Ref https://stackoverflow.com/a/60861023/ +# Import necessary modules for dynamic import of helper modules + +# Import 'import_module' to programmatically import other modules. from importlib import import_module + +# Import 'Path' to interact with the file system and locate Python files in the current directory. from pathlib import Path +# Dynamically import all Python modules in the current directory. +# This loop will go through each Python file in the same directory as this script, +# and import it unless it is a special module (like __init__.py) or has already been imported. for f in Path(__file__).parent.glob("*.py"): - module_name = f.stem + module_name = f.stem # Get the module name by removing the .py extension if (not module_name.startswith("_")) and (module_name not in globals()): - import_module(f".{module_name}", __package__) + import_module( + f".{module_name}", __package__ + ) # Import the module using its relative name + # Clean up loop variables to avoid polluting the global namespace del f, module_name -del (import_module,) + +# Clean up imported functions to avoid leaving them in the global namespace +del import_module diff --git a/helpers/colors.py b/helpers/colors.py index 59ab10e..1d0a6b1 100644 --- a/helpers/colors.py +++ b/helpers/colors.py @@ -1,12 +1,17 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. This is a helper for defining all the colors used in the bot. """ -MAIN_EMBED_COLOR = 0x6D28D9 +# Define color constants for the bot's embeds. -ERROR_EMBED_COLOR = 0x7C1719 +MAIN_EMBED_COLOR = 0x6D28D9 # A rich purple color used as the primary color for general-purpose embeds. -SUCCESS_EMBED_COLOR = 0x1E8449 +ERROR_EMBED_COLOR = ( + 0x7C1719 # A deep red color used to indicate errors or issues in the bot. +) + +SUCCESS_EMBED_COLOR = 0x1E8449 # A vibrant green color used to signify successful operations or confirmations. diff --git a/helpers/custom/__init__.py b/helpers/custom/__init__.py index 98e18b8..95477a4 100644 --- a/helpers/custom/__init__.py +++ b/helpers/custom/__init__.py @@ -1,18 +1,30 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -The helpers module contains all the custom helper functions and classes used custom cog files. +The helpers module contains all the helper functions and classes used in RickBot. """ -"""Import all modules that exist in the current directory.""" -# Ref https://stackoverflow.com/a/60861023/ +# Import necessary modules for dynamic import of helper modules + +# Import 'import_module' to programmatically import other modules. from importlib import import_module + +# Import 'Path' to interact with the file system and locate Python files in the current directory. from pathlib import Path +# Dynamically import all Python modules in the current directory. +# This loop will go through each Python file in the same directory as this script, +# and import it unless it is a special module (like __init__.py) or has already been imported. for f in Path(__file__).parent.glob("*.py"): - module_name = f.stem + module_name = f.stem # Get the module name by removing the .py extension if (not module_name.startswith("_")) and (module_name not in globals()): - import_module(f".{module_name}", __package__) + import_module( + f".{module_name}", __package__ + ) # Import the module using its relative name + # Clean up loop variables to avoid polluting the global namespace del f, module_name -del (import_module,) + +# Clean up imported functions to avoid leaving them in the global namespace +del import_module diff --git a/helpers/errors.py b/helpers/errors.py index 91abfc8..a379cde 100644 --- a/helpers/errors.py +++ b/helpers/errors.py @@ -1,46 +1,57 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. This is a helper for handling all discord.py related errors. """ # Import the required modules -# Python standard library -from datetime import datetime -import os -import random -import string -import traceback - -# Third-party libraries -import discord -from discord.ext import commands - -# Helper functions -from helpers.colors import ERROR_EMBED_COLOR -from helpers.logs import RICKLOG_MAIN - -# Config -from config import CUSTOM_CONFIG - - -async def handle_error(ctx: commands.Context, error: discord.DiscordException): +# Python Standard Library +# ------------------------ +from datetime import ( + datetime, +) # Used for timestamping error logs and embeds with the current date and time. +import os # Interacts with the operating system, here to check and create directories. +import random # Used to generate random values, specifically for creating unique error IDs. +import string # Provides a set of characters used in the creation of random error IDs. +import traceback # Captures and formats stack traces, useful for detailed error logs. + +# Third Party Libraries +# --------------------- +import discord # The primary library for interacting with the Discord API. +from discord.ext import ( + commands, +) # Provides the Command extension for building Discord bots. + +# Internal Modules +# ---------------- +from helpers.colors import ERROR_EMBED_COLOR # Custom color code for error embeds. +from helpers.logs import ( + RICKLOG_MAIN, +) # The main logger for RickBot, used to log error details. + + +async def handle_error(ctx: commands.Context, error: discord.DiscordException) -> None: """ - Handle errors that occur in the bot. + Handles errors that occur within the bot by sending an appropriate response to the user + and logging the error if necessary. - :param ctx: The context in which the error occurred. - :param error: The error that occurred. - """ + Args: + ctx (commands.Context): The context in which the error occurred. + error (discord.DiscordException): The error that occurred. + Returns: + None + """ + # Check for specific command-related errors and handle them accordingly if isinstance(error, commands.CommandNotFound): embed = discord.Embed( title="Error", description="Command not found.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.MissingRequiredArgument): @@ -49,7 +60,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="Missing required argument.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.BadArgument): @@ -58,7 +68,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="Bad argument.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.MissingPermissions): @@ -67,7 +76,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="You do not have the required permissions to run this command.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.BotMissingPermissions): @@ -76,7 +84,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="The bot does not have the required permissions to run this command.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.CommandOnCooldown): @@ -85,7 +92,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description=f"This command is on cooldown. Please try again in {error.retry_after:.2f} seconds.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, commands.CheckFailure): @@ -94,7 +100,6 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="You do not have the required roles to run this command.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) elif isinstance(error, OverflowError): @@ -103,30 +108,30 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): description="The number you entered is too large.", color=ERROR_EMBED_COLOR, ) - await ctx.reply(embed=embed, mention_author=False) else: - # Generate a random error ID + # Handle unexpected errors by logging and informing the user + # Generate a random error ID to track this specific error instance error_id = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) embed = discord.Embed( title="An Unexpected Error has occurred", - description="You should feel special, this doesn't often happen.\n\nThe developer has been notified, " - "and a fix should be out soon.\nIf no fix has been released after a while please contact the " - "developer and provide the below Error ID.", + description=( + "You should feel special, this doesn't often happen.\n\n" + "The developer has been notified, and a fix should be out soon.\n" + "If no fix has been released after a while, please contact the " + "developer and provide the below Error ID." + ), timestamp=datetime.now(), color=ERROR_EMBED_COLOR, ) embed.add_field(name="Error", value=f"```{error}```", inline=False) embed.add_field(name="Error ID", value=f"```{error_id}```", inline=False) - embed.set_footer(text="RickBot Error Logging") - # This is a serious error, log it in the errors directory - - # Ensure the errors directory exists + # Ensure the errors directory exists, create it if it doesn't if not os.path.exists("errors"): RICKLOG_MAIN.warning( "The errors directory does not exist; creating it now." @@ -139,21 +144,23 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): ) return - # Log the error + # Log the error details to a file in the errors directory error_file = ( f"errors/{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}-{error_id}.txt" ) - orignal_error = error.original + # Capture the original error's traceback for detailed logging + original_error = error.original traceback_error = traceback.format_exception( - type(orignal_error), orignal_error, orignal_error.__traceback__ + type(original_error), original_error, original_error.__traceback__ ) with open(error_file, "w+") as f: f.write( - "Hello! An error occurred during the running of RickBot.\nThis is most likely a serious error, " - "so please investigate it.\nIf you find this errors has occurred due to an issue with the original " - "code, please contact the developer.\nOtherwise, you're on your own. Good luck!\n\n" + "Hello! An error occurred during the running of RickBot.\n" + "This is most likely a serious error, so please investigate it.\n" + "If you find this error has occurred due to an issue with the original code, " + "please contact the developer.\nOtherwise, you're on your own. Good luck!\n\n" ) f.write(f"Error: {error}\n") f.write(f"Error ID: {error_id}\n") @@ -169,7 +176,7 @@ async def handle_error(ctx: commands.Context, error: discord.DiscordException): ) f.write("".join(traceback_error)) + # Inform the user of the error and log it await ctx.reply(embed=embed, mention_author=False) - RICKLOG_MAIN.error(f"An error occurred while running the command: {error}") RICKLOG_MAIN.error(f"Error log created at {error_file}") diff --git a/helpers/logs.py b/helpers/logs.py index db83013..2a1a2ff 100644 --- a/helpers/logs.py +++ b/helpers/logs.py @@ -1,20 +1,32 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. -This is a helper for logging. +This script provides a custom logging setup for RickBot, including formatters with +ANSI color support for console output, and a cleaner formatter for log files. """ # Import the required modules -# Python standard library -import logging -import re +# Python Standard Library +# ----------------------- +import logging # Handles the logging operations, allowing the output of messages to different destinations. +import re # Provides regular expression matching operations for strings. + # Helper functions +def remove_ansi_escape_sequences(s: str) -> str: + """ + Removes ANSI escape sequences from a string. This is useful for cleaning up + log messages before saving them to a file, ensuring no color codes are stored. + Args: + s (str): The string from which to remove ANSI escape sequences. -def remove_ansi_escape_sequences(s: str) -> str: + Returns: + str: The cleaned string with no ANSI escape sequences. + """ # ANSI escape sequences regex pattern ansi_escape_pattern = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") return ansi_escape_pattern.sub("", s) @@ -22,6 +34,15 @@ def remove_ansi_escape_sequences(s: str) -> str: # Custom formatter class with colors class CustomFormatter(logging.Formatter): + """ + A custom logging formatter that adds color to log messages using ANSI escape sequences. + This is particularly useful for enhancing readability when viewing logs in the console. + + Attributes: + COLORS (dict): A dictionary mapping log levels to their respective ANSI color codes. + RESET (str): The ANSI code to reset the color back to the default. + """ + # ANSI escape sequences for colors COLORS = { "DEBUG": "\033[1;97m", # Bold White @@ -34,51 +55,102 @@ class CustomFormatter(logging.Formatter): } RESET = "\033[0m" - def format(self, record): - log_fmt = f'{self.COLORS.get("DATE")}%(asctime)s{self.RESET} {self.COLORS.get(record.levelname, "")}%(levelname)s{self.RESET} {self.COLORS.get("NAME")}%(name)s{self.RESET} %(message)s' + def format(self, record: logging.LogRecord) -> str: + """ + Formats a log record with colors for different log levels. + + Args: + record (logging.LogRecord): The log record to format. + + Returns: + str: The formatted log message with ANSI color codes. + """ + log_fmt = ( + f'{self.COLORS.get("DATE")}%(asctime)s{self.RESET} ' + f'{self.COLORS.get(record.levelname, "")}%(levelname)s{self.RESET} ' + f'{self.COLORS.get("NAME")}%(name)s{self.RESET} %(message)s' + ) formatter = logging.Formatter(log_fmt, "%Y-%m-%d %H:%M:%S") return formatter.format(record) # Custom formatter that removes ANSI colors class CustomFileFormatter(logging.Formatter): - def format(self, record): + """ + A custom logging formatter that strips ANSI color codes from log messages. + This is used for log files to ensure that no unwanted color codes are saved. + + Methods: + format(record): Strips ANSI codes from the formatted log message. + """ + + def format(self, record: logging.LogRecord) -> str: + """ + Formats a log record by removing any ANSI color codes. + + Args: + record (logging.LogRecord): The log record to format. + + Returns: + str: The formatted log message without ANSI color codes. + """ original_format = super().format(record) return remove_ansi_escape_sequences(original_format) # Define the RickBot logger -RICKLOG = logging.getLogger("rickbot") -RICKLOG.setLevel(logging.DEBUG) +RICKLOG = logging.getLogger("rickbot") # The main logger for RickBot. +RICKLOG.setLevel( + logging.DEBUG +) # Set the default logging level to DEBUG for comprehensive logging. # Create a file and console handler -file_handler = logging.FileHandler(filename="rickbot.log", mode="w") -console_handler = logging.StreamHandler() +file_handler = logging.FileHandler( + filename="rickbot.log", mode="w" +) # File handler to write logs to a file. +console_handler = ( + logging.StreamHandler() +) # Console handler to output logs to the console. # Create formatters file_formatter = CustomFileFormatter( "%(asctime)s %(levelname)s %(name)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S" -) -console_formatter = CustomFormatter() +) # Formatter for the file handler, no colors. +console_formatter = CustomFormatter() # Formatter for the console handler, with colors. # Set formatters to handlers -file_handler.setFormatter(file_formatter) -console_handler.setFormatter(console_formatter) +file_handler.setFormatter( + file_formatter +) # Attach the file formatter to the file handler. +console_handler.setFormatter( + console_formatter +) # Attach the console formatter to the console handler. # Add the handlers to the logger -RICKLOG.addHandler(file_handler) -RICKLOG.addHandler(console_handler) +RICKLOG.addHandler(file_handler) # Add the file handler to the main RickBot logger. +RICKLOG.addHandler( + console_handler +) # Add the console handler to the main RickBot logger. # Define sub-loggers as constants -RICKLOG_CMDS = logging.getLogger("rickbot.cmds") -RICKLOG_DISCORD = logging.getLogger("rickbot.discord") -RICKLOG_MAIN = logging.getLogger("rickbot.main") -RICKLOG_WEBHOOK = logging.getLogger("rickbot.webhook") -RICKLOG_BG = logging.getLogger("rickbot.bg") -RICKLOG_HELPERS = logging.getLogger("rickbot.helpers") +RICKLOG_CMDS = logging.getLogger("rickbot.cmds") # Sub-logger for commands. +RICKLOG_DISCORD = logging.getLogger( + "rickbot.discord" +) # Sub-logger for Discord-related logs. +RICKLOG_MAIN = logging.getLogger( + "rickbot.main" +) # Sub-logger for main application logs. +RICKLOG_WEBHOOK = logging.getLogger( + "rickbot.webhook" +) # Sub-logger for webhook-related logs. +RICKLOG_BG = logging.getLogger("rickbot.bg") # Sub-logger for background task logs. +RICKLOG_HELPERS = logging.getLogger( + "rickbot.helpers" +) # Sub-logger for helper function logs. # Add handlers to sub-loggers -# Currently not required as the handlers are added to the main logger +# Currently, this is not required as the handlers are added to the main logger, +# but it's here in case of future needs or changes in how logging is handled. """ RICKLOG_CMDS.addHandler(file_handler) RICKLOG_CMDS.addHandler(console_handler) @@ -95,17 +167,17 @@ def format(self, record): """ -def setup_discord_logging(level=logging.INFO) -> None: +def setup_discord_logging(level: int = logging.INFO) -> None: """ - Sets the levels for all discord.py related loggers. + Sets the logging levels for all discord.py related loggers. Args: - level (int): The logging level to set. + level (int): The logging level to set. Must be one of the standard logging levels: + DEBUG, INFO, WARNING, ERROR, or CRITICAL. Returns: None """ - if level not in { logging.DEBUG, logging.INFO, @@ -121,7 +193,7 @@ def setup_discord_logging(level=logging.INFO) -> None: logging.getLogger("discord.http").setLevel(level) -# Example usage +# Example usage of the logging setup if __name__ == "__main__": RICKLOG.info("RickBot logging setup complete.") RICKLOG_CMDS.debug("This is a debug message from the cmds sub-logger.") diff --git a/helpers/rickbot.py b/helpers/rickbot.py index 37498af..1ef74fc 100644 --- a/helpers/rickbot.py +++ b/helpers/rickbot.py @@ -1,6 +1,7 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. This is a helper file full of RickBot specific constants and functions. @@ -10,28 +11,50 @@ # Import the required modules -# Python standard library -import logging +# Third-party Modules +# ------------------- +from termcolor import ( + colored, +) # Termcolor is used to add color to terminal text, enhancing the readability of console outputs. -# Third-party modules -from termcolor import colored - -# discord.py library -from discord.ext.commands import Bot +# discord.py Library +# ------------------ +from discord.ext.commands import ( + Bot, +) # Importing the Bot class from discord.ext.commands to type hint the bot object. # Helpers -from helpers.logs import RICKLOG +# ------- +from helpers.logs import ( + RICKLOG, +) # Importing the main logger from the helpers.logs module to log RickBot-specific events. # Functions +# --------- def rickbot_start_msg(bot: Bot) -> None: """ Print a message to the console when the bot is ready. + + This function is called when RickBot is fully initialized and ready to start interacting with Discord. + It prints a stylized message to the console, logging information about the bot, including the number + of guilds, users, commands, and cogs it has loaded. + + Args: + bot (Bot): The instance of RickBot that has just started. + + Returns: + None """ - print(START_SUCCESS_RICKBOT_ART) + print( + START_SUCCESS_RICKBOT_ART + ) # Print the ASCII art for RickBot's startup message. + + # Log the bot's login details with colored output for emphasis. RICKLOG.info(f'Logged in as {colored(bot.user.name, "light_cyan", attrs=["bold", "underline"])} with ID {colored(bot.user.id, "light_cyan", attrs=["bold", "underline"])}') # type: ignore - # Log extra information from the bot + + # Log extra information about the bot's connections and loaded components. RICKLOG.info( f'Connected to {colored(len(bot.guilds), "light_cyan", attrs=["bold", "underline"])} guilds.' ) @@ -47,7 +70,10 @@ def rickbot_start_msg(bot: Bot) -> None: # Constants +# --------- +# This constant contains the ASCII art that is printed to the console when RickBot starts. +# The art is a stylized version of RickBot's name, created using text with colored attributes. START_SUCCESS_RICKBOT_ART = ( str( """ @@ -61,11 +87,11 @@ def rickbot_start_msg(bot: Bot) -> None: colored("// ) )", "cyan", attrs=["bold"]), colored("//___/ / ( ) ___ //___", "magenta", attrs=["bold"]), colored("//___/ / ___ __ ___", "cyan", attrs=["bold"]), - colored("/ ___ ( / / // ) ) //\ \\", "magenta", attrs=["bold"]), # type: ignore + colored("/ ___ ( / / // ) ) //\\ \\", "magenta", attrs=["bold"]), colored("/ __ ( // ) ) / /", "cyan", attrs=["bold"]), - colored("// | | / / // // \ \\", "magenta", attrs=["bold"]), # type: ignore + colored("// | | / / // // \\ \\", "magenta", attrs=["bold"]), colored("// ) ) // / / / /", "cyan", attrs=["bold"]), - colored("// | | / / ((____ // \ \\", "magenta", attrs=["bold"]), # type: ignore + colored("// | | / / ((____ // \\ \\", "magenta", attrs=["bold"]), colored("//____/ / ((___/ / / /", "cyan", attrs=["bold"]), ) ) diff --git a/rickbot/__init__.py b/rickbot/__init__.py index 1bf7afd..fe02b30 100644 --- a/rickbot/__init__.py +++ b/rickbot/__init__.py @@ -1,9 +1,19 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. The rickbot package. This package contains the main application file(s) for RickBot. -Do not run this anything within this module directly. Instead, run app.py. +Do not run anything within this module directly. Instead, run app.py. """ + +# This file typically serves as the initialization file for the rickbot package. +# It can be used to initialize package-level variables or import certain +# functionalities when the package is imported elsewhere in the application. +# However, in this case, we are simply providing documentation to clarify the +# purpose of the package. + +# Since no code is required to be executed at the package level, this file is left empty. +# All the essential functionality should be defined and run through app.py. diff --git a/rickbot/main.py b/rickbot/main.py index 8939c3d..e6ab1dd 100644 --- a/rickbot/main.py +++ b/rickbot/main.py @@ -1,125 +1,198 @@ """ -(c) 2024 Zachariah Michael Lagden (All Rights Reserved) -You may not use, copy, distribute, modify, or sell this code without the express permission of the author. +(c) 2024 Lagden Development (All Rights Reserved) +Licensed for non-commercial use with attribution required; provided 'as is' without warranty. +See https://github.com/Lagden-Development/.github/blob/main/LICENSE for more information. This is the main application file for RickBot. """ -# Python standard library -from datetime import datetime -import asyncio -import logging -import glob - -# Third-party libraries -from termcolor import colored - -# discord.py library -from discord.ext import commands -import discord - -# Helper files +# Python Standard Library Imports +# ------------------------------- +from datetime import datetime # Used for logging timestamps and bot status updates. +import asyncio # Essential for managing asynchronous operations, which are critical for bot functionality. +import logging # Provides logging capabilities to track events and debug issues. +import glob # Used to find file paths matching a specified pattern, useful for loading cogs dynamically. + +# Third-party Libraries +# --------------------- +from discord.ext import ( + commands, +) # Imports the commands extension for creating and managing bot commands. +from termcolor import ( + colored, +) # Used to add color to the console output, enhancing readability. +import discord # Core library for interacting with the Discord API. + +# Internal Modules +# ------------ from helpers.logs import ( - setup_discord_logging, - RICKLOG, - RICKLOG_MAIN, - RICKLOG_DISCORD, - RICKLOG_WEBHOOK, + setup_discord_logging, # Function to set up logging specifically for Discord-related events. + RICKLOG, # Main logger for general bot-related logging. + RICKLOG_MAIN, # Logger for logging the main bot events. + RICKLOG_DISCORD, # Logger for Discord-specific events. + RICKLOG_WEBHOOK, # Logger for webhook-related events. ) -from helpers.rickbot import rickbot_start_msg -from helpers.errors import handle_error - -# Configuration file -from config import CONFIG - -# Configurations (Not usally changed, so not in the config file) - -COMMAND_ERRORS_TO_IGNORE = (commands.CommandNotFound,) - -# Custom exceptions - - +from helpers.rickbot import ( + rickbot_start_msg, +) # Function to print the bot start message. +from helpers.errors import ( + handle_error, +) # Function to handle errors in a standardized way. +from db import bot_db # Database connection for the bot. + +# Configuration +# ------------------ +from config import ( + CONFIG, +) # Configuration settings. + + +# Configurations (Not usually changed, so not in the config file) +# --------------------------------------------------------------- +COMMAND_ERRORS_TO_IGNORE = ( + commands.CommandNotFound, +) # A tuple of command errors that can be safely ignored. + + +# Custom Exceptions +# ----------------- class WebhookFailedError(Exception): """ Raised when a webhook fails to send a message. """ - pass + pass # This exception is used to signal a failure in sending a webhook message. # Functions +# --------- def get_prefix(bot, message): - return commands.when_mentioned_or(CONFIG["BOT"]["prefix"])(bot, message) + """ + Function to determine the command prefix for the bot. + Args: + bot: The instance of the bot. + message: The message object which triggered the command. -# Classes + Returns: + Callable: A function that returns either the mentioned prefix or the prefix defined in the configuration. + """ + return commands.when_mentioned_or(CONFIG["BOT"]["prefix"])(bot, message) -# Define the custom Context class +# Classes +# ------- class RickContext(commands.Context): - pass + """ + Custom context class for the bot, used to override or extend the default context behavior. + """ + + def __init__(self, **attrs): + super().__init__(db=bot_db, **attrs) -# Define the custom Bot class class RickBot(commands.Bot): + """ + Custom bot class that encapsulates the main functionality and behavior of RickBot. + """ + def __init__(self): super().__init__( - command_prefix=get_prefix, - case_insensitive=True, - strip_after_prefix=True, - allowed_mentions=discord.AllowedMentions(everyone=False, roles=False), - intents=discord.Intents.all(), + command_prefix=get_prefix, # Sets the prefix used to invoke commands. + case_insensitive=True, # Commands are case-insensitive, making them easier to use. + strip_after_prefix=True, # Strips whitespace after the prefix, ensuring clean command parsing. + allowed_mentions=discord.AllowedMentions( + everyone=False, roles=False + ), # Prevents mass mentions. + intents=discord.Intents.all(), # Sets the bot's intents, enabling access to all necessary events. ) - self.setup_logging() - self.load_config() + self.setup_logging() # Initialize logging setup. + self.load_config() # Load configuration settings. def setup_logging(self): - setup_discord_logging(logging.INFO) + """ + Sets up the logging configuration for the bot. + """ + setup_discord_logging( + logging.DEBUG + ) # Calls a helper function to configure Discord-specific logging. def load_config(self): + """ + Loads and applies the bot's configuration settings. + Adjusts logging levels based on the current mode (development or production). + """ if CONFIG["MAIN"]["mode"] == "dev": - RICKLOG.setLevel(logging.DEBUG) + RICKLOG.setLevel( + logging.DEBUG + ) # Enables debug logging in development mode. else: - RICKLOG.setLevel(logging.INFO) + RICKLOG.setLevel( + logging.INFO + ) # Uses standard logging levels in production. async def setup_hook(self): - await self.load_cogs() + """ + Runs after the bot has connected but before it has logged in. + Used here to load all the cogs. + """ + await self.load_cogs() # Calls a method to dynamically load all bot cogs. async def load_cogs(self): + """ + Loads all cogs (extensions) from the 'cogs' directory. + """ for cog_folder in glob.glob("cogs/*"): cogs_loaded_from_this_folder = 0 if not cog_folder.startswith("_"): for filename in glob.glob(f"{cog_folder}/*.py"): if not filename.startswith("_"): - cog_name = f"{filename[:-3].replace('/', '.')}" - await self.load_extension(cog_name) + cog_name = f"{filename[:-3].replace('/', '.')}" # Convert file path to module path. + await self.load_extension( + cog_name + ) # Load the cog as an extension. cogs_loaded_from_this_folder += 1 - - RICKLOG_MAIN.debug(f"Loaded cog: {cog_name}") + RICKLOG_MAIN.debug( + f"Loaded cog: {cog_name}" + ) # Log the cog loading. RICKLOG_MAIN.info( f"Loaded cog folder: {cog_folder} ({cogs_loaded_from_this_folder} cogs)" ) async def get_context(self, message, *, cls=None): + """ + Overrides the default context method to use RickContext. + + Args: + message: The message that triggered the context creation. + cls: The class to use for context creation (defaults to RickContext). + + Returns: + RickContext: The context object created for the message. + """ return await super().get_context(message, cls=RickContext) async def on_ready(self): + """ + Called when the bot is ready and fully connected to Discord. + Logs the ready state and sets the bot's status. + """ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") RICKLOG_MAIN.info( f"RickBot started at {colored(current_time, 'light_cyan', attrs=['bold', 'underline'])}" ) RICKLOG_DISCORD.info("RickBot's Connection to Discord initialized.") - await self.set_status() - rickbot_start_msg(self) - print() + await self.set_status() # Set the bot's status according to the configuration. + rickbot_start_msg(self) # Display the start message. + print() # Print an empty line for better console readability. RICKLOG_DISCORD.info("Syncing commands...") - await self.tree.sync() + await self.tree.sync() # Sync commands with Discord. RICKLOG_DISCORD.info("Commands synced.") async def set_status(self): @@ -154,79 +227,118 @@ async def set_status(self): ) async def on_message(self, message): - # Process commands and check for mentions - if ( - message.author == self.user - or message.author.bot - or message.guild is None - or message.webhook_id - ): + """ + Handles incoming messages and processes commands. + + Args: + message: The message object representing the received message. + """ + # Ignore messages from the bot itself, other bots, DMs, or webhooks. + if message.author == self.user or message.author.bot or message.webhook_id: return + # Check if the message mentions the bot and respond with a help message. if message.content.startswith( - f"<@!{self.user.id}>" # type: ignore - ) or message.content.startswith( - f"<@{self.user.id}>" # type: ignore - ): + f"<@!{self.user.id}>" + ) or message.content.startswith(f"<@{self.user.id}>"): await message.reply( f"Hey there, {message.author.mention}! Use `{CONFIG['BOT']['prefix']}help` to see what I can do.", mention_author=False, ) return - await self.process_commands(message) + await self.process_commands(message) # Process commands in the message. async def on_command_error(self, ctx, error): - # Error handling for commands without specific error handlers + """ + Global error handler for commands. + + Args: + ctx: The context in which the error occurred. + error: The error that occurred. + """ + # If the command has its own error handler or the error should be ignored, do nothing. if hasattr(ctx.command, "on_error") or isinstance( error, COMMAND_ERRORS_TO_IGNORE ): return - await handle_error(ctx, error) + await handle_error(ctx, error) # Handle the error using a custom handler. async def start_bot(self): + """ + Starts the bot and handles graceful shutdown. + + This method encapsulates the bot's startup process and ensures that + the bot shuts down gracefully when requested. + """ try: RICKLOG_MAIN.info("Starting RickBot...") - await self.start(CONFIG["BOT"]["token"]) + await self.start( + CONFIG["BOT"]["token"] + ) # Start the bot using the token from the config. finally: - RICKLOG_MAIN.info("RickBot has shut down gracefully.") + RICKLOG_MAIN.info( + "RickBot has shut down gracefully." + ) # Log that the bot has shut down. async def shutdown(self, signal): - """Gracefully shut down the bot.""" + """ + Gracefully shuts down the bot upon receiving a termination signal. + + Args: + signal: The signal that triggered the shutdown. + """ RICKLOG_MAIN.info( f"Received exit signal {signal.name} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}..." ) RICKLOG_DISCORD.info("Closing Discord connection...") - await self.close() + await self.close() # Close the connection to Discord. RICKLOG_DISCORD.info("Discord connection closed.") async def on_connect(self): + """ + Called when the bot successfully connects to Discord. + Logs the connection event. + """ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") RICKLOG_DISCORD.info(f"RickBot connected to Discord at {current_time}.") - RICKLOG_DISCORD.info(f"Session ID: {self.ws.session_id}") + RICKLOG_DISCORD.info(f"Session ID: {self.ws.session_id}") # Log the session ID. async def on_disconnect(self): + """ + Called when the bot disconnects from Discord. + Logs the disconnection event and attempts to reconnect. + """ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") RICKLOG_DISCORD.warning(f"RickBot disconnected from Discord at {current_time}.") RICKLOG_DISCORD.info("Attempting to reconnect...") async def on_resumed(self): + """ + Called when the bot successfully resumes a connection to Discord after a disconnect. + Logs the resume event. + """ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") RICKLOG_DISCORD.info( f"RickBot resumed connection to Discord at {current_time}." ) - RICKLOG_DISCORD.info(f"Resumed Session ID: {self.ws.session_id}") + RICKLOG_DISCORD.info( + f"Resumed Session ID: {self.ws.session_id}" + ) # Log the resumed session ID. async def grab_channel_webhook( self, channel: discord.TextChannel ) -> discord.Webhook: """ - Grabs the first webhook found in a channel. - For the webhooks it finds it has to ensure it was created by the bot. - Otherwise (or if not found) it creates a new webhook. - """ + Grabs or creates a webhook for the specified channel. + + Args: + channel (discord.TextChannel): The channel for which the webhook is being retrieved or created. + Returns: + discord.Webhook: The webhook object for the specified channel. + """ # Grab all webhooks in the channel webhooks = await channel.webhooks() @@ -257,8 +369,11 @@ async def send_to_channel( ): """ Sends a message to a channel using a webhook. - """ + Args: + channel (discord.TextChannel): The channel where the message will be sent. + **kwargs: Additional keyword arguments to be passed to the webhook's send method. + """ # Grab the webhook for the channel webhook = await self.grab_channel_webhook(channel) @@ -273,4 +388,4 @@ async def send_to_channel( RICKLOG_WEBHOOK.error( f"Failed to send webhook message to channel {channel} on attempt ({str(attempt)}): {e}" ) - await asyncio.sleep(1) + await asyncio.sleep(1) # Wait for a second before retrying.