diff --git a/bot_discord.py b/bot_discord.py index 7dfa1e4..c287b92 100644 --- a/bot_discord.py +++ b/bot_discord.py @@ -17,15 +17,16 @@ class DiscordBot(commands.Bot): self.help_data = None # We'll set this later self.load_commands() + self_log = self.log + + self.log("Discord bot initiated", "INFO") + log_func(f"DiscordBot.commands type: {type(self.commands)}", "DEBUG") + def set_db_connection(self, db_conn): """ Store the DB connection in the bot so commands can use it. """ self.db_conn = db_conn - try: - modules.db.ensure_quotes_table(self.db_conn, self.log) - except Exception as e: - self.log(f"Critical: unable to ensure quotes table: {e}", "FATAL") def load_commands(self): """ diff --git a/bot_twitch.py b/bot_twitch.py index c6d1843..0f8f258 100644 --- a/bot_twitch.py +++ b/bot_twitch.py @@ -29,6 +29,8 @@ class TwitchBot(commands.Bot): self.log("Twitch bot initiated", "INFO") + log_func(f"TwitchBot._commands type: {type(self._commands)}", "DEBUG") + # 2) Then load commands self.load_commands() @@ -37,10 +39,6 @@ class TwitchBot(commands.Bot): Store the DB connection so that commands can use it. """ self.db_conn = db_conn - try: - modules.db.ensure_quotes_table(self.db_conn, self.log) - except Exception as e: - self.log(f"Critical: unable to ensure quotes table: {e}", "FATAL") async def event_message(self, message): """Logs and processes incoming Twitch messages.""" @@ -127,7 +125,6 @@ class TwitchBot(commands.Bot): Load all commands from cmd_twitch.py """ try: - importlib.reload(cmd_twitch) cmd_twitch.setup(self) self.log("Twitch commands loaded successfully.", "INFO") diff --git a/bots.py b/bots.py index e42c280..fd332a5 100644 --- a/bots.py +++ b/bots.py @@ -14,6 +14,7 @@ from bot_discord import DiscordBot from bot_twitch import TwitchBot from modules.db import init_db_connection, run_db_operation +from modules.db import ensure_quotes_table # Load environment variables load_dotenv() @@ -79,6 +80,13 @@ async def main(): log("Terminating bot due to no DB connection.", "FATAL") sys.exit(1) + # auto-create the quotes table if it doesn't exist + try: + ensure_quotes_table(db_conn, log) + except Exception as e: + log(f"Critical: unable to ensure quotes table: {e}", "FATAL") + + log("Initializing bots...", "INFO") # Create both bots diff --git a/cmd_discord.py b/cmd_discord.py index 03ec943..552f5da 100644 --- a/cmd_discord.py +++ b/cmd_discord.py @@ -1,39 +1,36 @@ # cmd_discord.py from discord.ext import commands + from cmd_common import common_commands as cc from modules.permissions import has_permission from modules.utility import handle_help_command - +from modules.utility import monitor_cmds def setup(bot, db_conn=None, log=None): """ Attach commands to the Discord bot, store references to db/log. """ - - # auto-create the quotes table if it doesn't exist - if bot.db_conn and bot.log: - cc.create_quotes_table(bot.db_conn, bot.log) - - # Auto-create the quotes table if desired - if db_conn and log: - cc.create_quotes_table(db_conn, log) @bot.command(name="greet") + @monitor_cmds(bot.log) async def cmd_greet(ctx): result = cc.greet(ctx.author.display_name, "Discord") await ctx.send(result) @bot.command(name="ping") + @monitor_cmds(bot.log) async def cmd_ping(ctx): result = cc.ping() await ctx.send(result) @bot.command(name="howl") + @monitor_cmds(bot.log) async def cmd_howl(ctx): """Calls the shared !howl logic.""" result = cc.howl(ctx.author.display_name) await ctx.send(result) @bot.command(name="reload") + @monitor_cmds(bot.log) async def cmd_reload(ctx): """ Dynamically reloads Discord commands. """ try: @@ -46,6 +43,7 @@ def setup(bot, db_conn=None, log=None): await ctx.send(f"Error reloading commands: {e}") @bot.command(name="hi") + @monitor_cmds(bot.log) async def cmd_hi(ctx): user_id = str(ctx.author.id) user_roles = [role.name.lower() for role in ctx.author.roles] # Normalize to lowercase @@ -57,6 +55,7 @@ def setup(bot, db_conn=None, log=None): await ctx.send("Hello there!") @bot.command(name="quote") + @monitor_cmds(bot.log) async def cmd_quote(ctx, *args): """ !quote @@ -78,9 +77,19 @@ def setup(bot, db_conn=None, log=None): ) @bot.command(name="help") + @monitor_cmds(bot.log) async def cmd_help(ctx, cmd_name: str = None): """ e.g. !help !help quote """ - await handle_help_command(ctx, cmd_name, bot, is_discord=True, log_func=bot.log) \ No newline at end of file + await handle_help_command(ctx, cmd_name, bot, is_discord=True, log_func=bot.log) + + ###################### + # The following log entry must be last in the file to verify commands loading as they should + ###################### + # Debug: Print that commands are being registered + try: + bot.log(f"Registering commands for Discord: {list(bot.commands.keys())}", "DEBUG") + except Exception as e: + bot.log(f"An error occured while printing registered commands for Discord: {e}", "WARNING") \ No newline at end of file diff --git a/cmd_twitch.py b/cmd_twitch.py index 7451de7..1f1bc5f 100644 --- a/cmd_twitch.py +++ b/cmd_twitch.py @@ -1,36 +1,37 @@ # cmd_twitch.py from twitchio.ext import commands + from cmd_common import common_commands as cc from modules.permissions import has_permission from modules.utility import handle_help_command +from modules.utility import monitor_cmds def setup(bot, db_conn=None, log=None): """ This function is called to load/attach commands to the `bot`. We also attach the db_conn and log so the commands can use them. """ - - # auto-create the quotes table if it doesn't exist - if bot.db_conn and bot.log: - cc.create_quotes_table(bot.db_conn, bot.log) - @bot.command(name="greet") + @monitor_cmds(bot.log) async def cmd_greet(ctx): result = cc.greet(ctx.author.display_name, "Twitch") await ctx.send(result) @bot.command(name="ping") + @monitor_cmds(bot.log) async def cmd_ping(ctx): result = cc.ping() await ctx.send(result) @bot.command(name="howl") + @monitor_cmds(bot.log) async def cmd_howl(ctx): result = cc.howl(ctx.author.display_name) await ctx.send(result) @bot.command(name="hi") + @monitor_cmds(bot.log) async def cmd_hi(ctx): user_id = str(ctx.author.id) # Twitch user ID user_roles = [role.lower() for role in ctx.author.badges.keys()] # "roles" from Twitch badges @@ -41,6 +42,7 @@ def setup(bot, db_conn=None, log=None): await ctx.send("Hello there!") @bot.command(name="quote") + @monitor_cmds(bot.log) async def cmd_quote(ctx: commands.Context): if not bot.db_conn: return await ctx.send("Database is unavailable, sorry.") @@ -62,7 +64,17 @@ def setup(bot, db_conn=None, log=None): ) @bot.command(name="help") - async def help_command(ctx): + @monitor_cmds(bot.log) + async def cmd_help(ctx): parts = ctx.message.content.strip().split() cmd_name = parts[1] if len(parts) > 1 else None - await handle_help_command(ctx, cmd_name, bot, is_discord=False, log_func=bot.log) \ No newline at end of file + await handle_help_command(ctx, cmd_name, bot, is_discord=False, log_func=bot.log) + + ###################### + # The following log entry must be last in the file to verify commands loading as they should + ###################### + # Debug: Print that commands are being registered + try: + bot.log(f"Registering commands for Twitch: {list(bot.commands.keys())}", "DEBUG") + except Exception as e: + bot.log(f"An error occured while printing registered commands for Twitch: {e}", "WARNING") \ No newline at end of file diff --git a/dictionary/help_discord.json b/dictionary/help_discord.json index 85513b7..6996ac8 100644 --- a/dictionary/help_discord.json +++ b/dictionary/help_discord.json @@ -3,7 +3,10 @@ "help": { "description": "Show information about available commands.", "subcommands": {}, - "examples": ["!help", "!help quote"] + "examples": [ + "!help", + "!help quote" + ] }, "quote": { "description": "Manage quotes (add, remove, fetch).", @@ -33,7 +36,9 @@ "ping": { "description": "Check my uptime.", "subcommands": {}, - "examples": ["!ping"] + "examples": [ + "!ping" + ] }, "howl": { "description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)", diff --git a/modules/utility.py b/modules/utility.py index dfe8bfb..d1d202b 100644 --- a/modules/utility.py +++ b/modules/utility.py @@ -3,6 +3,8 @@ import os import random import json import re +import functools + try: # 'regex' on PyPI supports `\p{L}`, `\p{N}`, etc. @@ -14,6 +16,36 @@ except ImportError: DICTIONARY_PATH = "dictionary/" # Path to dictionary files +def monitor_cmds(log_func): + """ + Decorator that logs when a command starts and ends execution. + """ + def decorator(func): + @functools.wraps(func) # Preserve function metadata + async def wrapper(*args, **kwargs): + start_time = time.time() + try: + cmd_name = str(func.__name__).split("_")[1] + log_func(f"Command '{cmd_name}' started execution.", "DEBUG") + + # Await the actual function (since it's an async command) + result = await func(*args, **kwargs) + + end_time = time.time() + cmd_duration = end_time - start_time + cmd_duration = str(round(cmd_duration, 2)) + log_func(f"Command '{cmd_name}' finished execution after {cmd_duration}s.", "DEBUG") + return result # Return the result of the command + except Exception as e: + end_time = time.time() + cmd_duration = end_time - start_time + cmd_duration = str(round(cmd_duration, 2)) + log_func(f"Command '{cmd_name}' FAILED while executing after {cmd_duration}s: {e}", "CRITICAL") + + return wrapper # Return the wrapped function + + return decorator # Return the decorator itself + def format_uptime(seconds: float) -> tuple[str, int]: """ Convert seconds into a human-readable string: @@ -195,19 +227,29 @@ async def handle_help_command(ctx, command_name, bot, is_discord, log_func): if not command_name: # User typed just "!help" => list all known commands from this bot - loaded_cmds = get_loaded_commands(bot) + loaded_cmds = get_loaded_commands(bot, log_func, is_discord) if not loaded_cmds: return await send_message(ctx, "I have no commands loaded.") else: - short_list = ", ".join(loaded_cmds) - # We can also mention "Use !help [command] for more info." - return await send_message( - ctx, - f"I currently offer these commands: {short_list}\nUse '!help ' for details." - ) + if is_discord: + help_str = f"I currently offer these commands:" + for cmd in loaded_cmds: + help_str += f"\n- !{cmd}" + help_str += f"\n*Use '!help ' for more details.*" + return await send_message( + ctx, + help_str + ) + else: + short_list = ", ".join(loaded_cmds) + # We can also mention "Use !help [command] for more info." + return await send_message( + ctx, + f"I currently offer these commands:{short_list}. \nUse '!help ' for details." + ) # 1) Check if the command is loaded - loaded = (command_name in get_loaded_commands(bot)) + loaded = (command_name in get_loaded_commands(bot, log_func, is_discord)) # 2) Check if it has help info in the JSON cmd_help = help_data["commands"].get(command_name, None) @@ -255,7 +297,7 @@ def initialize_help_data(bot, help_json_path, is_discord, log_func): bot.help_data = data # Now cross-check the loaded commands vs. the data - loaded_cmds = set(get_loaded_commands(bot)) + loaded_cmds = set(get_loaded_commands(bot, log_func, is_discord)) if "commands" not in data: log_func(f"No 'commands' key in {help_json_path}, skipping checks.", "ERROR") return @@ -273,20 +315,48 @@ def initialize_help_data(bot, help_json_path, is_discord, log_func): log_func(f"Command '{cmd}' is loaded on {('Discord' if is_discord else 'Twitch')} but no help info is provided in {help_json_path}.", "WARNING") -def get_loaded_commands(bot): +def get_loaded_commands(bot, log_func, is_discord): + from discord.ext import commands as discord_commands + from twitchio.ext import commands as twitch_commands + commands_list = [] - # For Discord.py - if hasattr(bot, "commands"): - for c_obj in bot.commands: - commands_list.append(c_obj.name) + try: + _bot_type = str(type(bot)).split("_")[1].split(".")[0] + log_func(f"Currently processing commands for {_bot_type} ...", "DEBUG") + except Exception as e: + log_func(f"Unable to determine current bot type: {e}", "WARNING") - # For TwitchIO - if hasattr(bot, "all_commands"): - # each item is (command_name_str, command_object) - for cmd_name, cmd_obj in bot.all_commands.items(): - commands_list.append(cmd_name) + # For Discord + if is_discord: + #if isinstance(bot, discord_commands.Bot): + try: + # 'bot.commands' is a set of Command objects + for cmd_obj in bot.commands: + commands_list.append(cmd_obj.name) + log_func(f"Discord commands body: {commands_list}", "DEBUG") + except Exception as e: + log_func(f"Error retrieving Discord commands: {e}", "ERROR") + elif not is_discord: + # For TwitchIO + #if isinstance(bot.commands, set): + try: + #commands_attr = bot.commands + #log_func(f"Twitch type(bot.commands) => {type(commands_attr)}", "DEBUG") + # 'bot.all_commands' is a dict: { command_name: Command(...) } + #all_cmd_names = list(bot.all_commands.keys()) + #log_func(f"Twitch commands body: {all_cmd_names}", "DEBUG") + #commands_list.extend(all_cmd_names) + for cmd_obj in bot._commands: + commands_list.append(cmd_obj) + log_func(f"Twitch commands body: {commands_list}", "DEBUG") + except Exception as e: + log_func(f"Error retrieving Twitch commands: {e}", "ERROR") + else: + log_func(f"Unable to determine platform in 'get_loaded_commands()'!", "CRITICAL") + + log_func(f"... Finished processing commands for {_bot_type} ...", "DEBUG") return sorted(commands_list)