Improved !help

- Ensured it fetches the correct commands and help configuration depending on platform.
- Removed certain duplicate checks that intiated functions twice or repeated value asignments.
- Added the @monitor_cmd flag to commands, allowing easy command runtime diagnostics, including execution time.
kami_dev
Kami 2025-02-03 22:02:56 +01:00
parent 28d22da0c1
commit 5730840209
7 changed files with 149 additions and 47 deletions

View File

@ -17,15 +17,16 @@ class DiscordBot(commands.Bot):
self.help_data = None # We'll set this later self.help_data = None # We'll set this later
self.load_commands() 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): def set_db_connection(self, db_conn):
""" """
Store the DB connection in the bot so commands can use it. Store the DB connection in the bot so commands can use it.
""" """
self.db_conn = db_conn 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): def load_commands(self):
""" """

View File

@ -29,6 +29,8 @@ class TwitchBot(commands.Bot):
self.log("Twitch bot initiated", "INFO") self.log("Twitch bot initiated", "INFO")
log_func(f"TwitchBot._commands type: {type(self._commands)}", "DEBUG")
# 2) Then load commands # 2) Then load commands
self.load_commands() self.load_commands()
@ -37,10 +39,6 @@ class TwitchBot(commands.Bot):
Store the DB connection so that commands can use it. Store the DB connection so that commands can use it.
""" """
self.db_conn = db_conn 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): async def event_message(self, message):
"""Logs and processes incoming Twitch messages.""" """Logs and processes incoming Twitch messages."""
@ -127,7 +125,6 @@ class TwitchBot(commands.Bot):
Load all commands from cmd_twitch.py Load all commands from cmd_twitch.py
""" """
try: try:
importlib.reload(cmd_twitch)
cmd_twitch.setup(self) cmd_twitch.setup(self)
self.log("Twitch commands loaded successfully.", "INFO") self.log("Twitch commands loaded successfully.", "INFO")

View File

@ -14,6 +14,7 @@ from bot_discord import DiscordBot
from bot_twitch import TwitchBot from bot_twitch import TwitchBot
from modules.db import init_db_connection, run_db_operation from modules.db import init_db_connection, run_db_operation
from modules.db import ensure_quotes_table
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
@ -79,6 +80,13 @@ async def main():
log("Terminating bot due to no DB connection.", "FATAL") log("Terminating bot due to no DB connection.", "FATAL")
sys.exit(1) 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") log("Initializing bots...", "INFO")
# Create both bots # Create both bots

View File

@ -1,39 +1,36 @@
# cmd_discord.py # cmd_discord.py
from discord.ext import commands from discord.ext import commands
from cmd_common import common_commands as cc from cmd_common import common_commands as cc
from modules.permissions import has_permission from modules.permissions import has_permission
from modules.utility import handle_help_command from modules.utility import handle_help_command
from modules.utility import monitor_cmds
def setup(bot, db_conn=None, log=None): def setup(bot, db_conn=None, log=None):
""" """
Attach commands to the Discord bot, store references to db/log. 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") @bot.command(name="greet")
@monitor_cmds(bot.log)
async def cmd_greet(ctx): async def cmd_greet(ctx):
result = cc.greet(ctx.author.display_name, "Discord") result = cc.greet(ctx.author.display_name, "Discord")
await ctx.send(result) await ctx.send(result)
@bot.command(name="ping") @bot.command(name="ping")
@monitor_cmds(bot.log)
async def cmd_ping(ctx): async def cmd_ping(ctx):
result = cc.ping() result = cc.ping()
await ctx.send(result) await ctx.send(result)
@bot.command(name="howl") @bot.command(name="howl")
@monitor_cmds(bot.log)
async def cmd_howl(ctx): async def cmd_howl(ctx):
"""Calls the shared !howl logic.""" """Calls the shared !howl logic."""
result = cc.howl(ctx.author.display_name) result = cc.howl(ctx.author.display_name)
await ctx.send(result) await ctx.send(result)
@bot.command(name="reload") @bot.command(name="reload")
@monitor_cmds(bot.log)
async def cmd_reload(ctx): async def cmd_reload(ctx):
""" Dynamically reloads Discord commands. """ """ Dynamically reloads Discord commands. """
try: try:
@ -46,6 +43,7 @@ def setup(bot, db_conn=None, log=None):
await ctx.send(f"Error reloading commands: {e}") await ctx.send(f"Error reloading commands: {e}")
@bot.command(name="hi") @bot.command(name="hi")
@monitor_cmds(bot.log)
async def cmd_hi(ctx): async def cmd_hi(ctx):
user_id = str(ctx.author.id) user_id = str(ctx.author.id)
user_roles = [role.name.lower() for role in ctx.author.roles] # Normalize to lowercase 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!") await ctx.send("Hello there!")
@bot.command(name="quote") @bot.command(name="quote")
@monitor_cmds(bot.log)
async def cmd_quote(ctx, *args): async def cmd_quote(ctx, *args):
""" """
!quote !quote
@ -78,9 +77,19 @@ def setup(bot, db_conn=None, log=None):
) )
@bot.command(name="help") @bot.command(name="help")
@monitor_cmds(bot.log)
async def cmd_help(ctx, cmd_name: str = None): async def cmd_help(ctx, cmd_name: str = None):
""" """
e.g. !help e.g. !help
!help quote !help quote
""" """
await handle_help_command(ctx, cmd_name, bot, is_discord=True, log_func=bot.log) 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")

View File

@ -1,36 +1,37 @@
# cmd_twitch.py # cmd_twitch.py
from twitchio.ext import commands from twitchio.ext import commands
from cmd_common import common_commands as cc from cmd_common import common_commands as cc
from modules.permissions import has_permission from modules.permissions import has_permission
from modules.utility import handle_help_command from modules.utility import handle_help_command
from modules.utility import monitor_cmds
def setup(bot, db_conn=None, log=None): def setup(bot, db_conn=None, log=None):
""" """
This function is called to load/attach commands to the `bot`. 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. 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") @bot.command(name="greet")
@monitor_cmds(bot.log)
async def cmd_greet(ctx): async def cmd_greet(ctx):
result = cc.greet(ctx.author.display_name, "Twitch") result = cc.greet(ctx.author.display_name, "Twitch")
await ctx.send(result) await ctx.send(result)
@bot.command(name="ping") @bot.command(name="ping")
@monitor_cmds(bot.log)
async def cmd_ping(ctx): async def cmd_ping(ctx):
result = cc.ping() result = cc.ping()
await ctx.send(result) await ctx.send(result)
@bot.command(name="howl") @bot.command(name="howl")
@monitor_cmds(bot.log)
async def cmd_howl(ctx): async def cmd_howl(ctx):
result = cc.howl(ctx.author.display_name) result = cc.howl(ctx.author.display_name)
await ctx.send(result) await ctx.send(result)
@bot.command(name="hi") @bot.command(name="hi")
@monitor_cmds(bot.log)
async def cmd_hi(ctx): async def cmd_hi(ctx):
user_id = str(ctx.author.id) # Twitch user ID user_id = str(ctx.author.id) # Twitch user ID
user_roles = [role.lower() for role in ctx.author.badges.keys()] # "roles" from Twitch badges 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!") await ctx.send("Hello there!")
@bot.command(name="quote") @bot.command(name="quote")
@monitor_cmds(bot.log)
async def cmd_quote(ctx: commands.Context): async def cmd_quote(ctx: commands.Context):
if not bot.db_conn: if not bot.db_conn:
return await ctx.send("Database is unavailable, sorry.") return await ctx.send("Database is unavailable, sorry.")
@ -62,7 +64,17 @@ def setup(bot, db_conn=None, log=None):
) )
@bot.command(name="help") @bot.command(name="help")
async def help_command(ctx): @monitor_cmds(bot.log)
async def cmd_help(ctx):
parts = ctx.message.content.strip().split() parts = ctx.message.content.strip().split()
cmd_name = parts[1] if len(parts) > 1 else None 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) 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")

View File

@ -3,7 +3,10 @@
"help": { "help": {
"description": "Show information about available commands.", "description": "Show information about available commands.",
"subcommands": {}, "subcommands": {},
"examples": ["!help", "!help quote"] "examples": [
"!help",
"!help quote"
]
}, },
"quote": { "quote": {
"description": "Manage quotes (add, remove, fetch).", "description": "Manage quotes (add, remove, fetch).",
@ -33,7 +36,9 @@
"ping": { "ping": {
"description": "Check my uptime.", "description": "Check my uptime.",
"subcommands": {}, "subcommands": {},
"examples": ["!ping"] "examples": [
"!ping"
]
}, },
"howl": { "howl": {
"description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)", "description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)",

View File

@ -3,6 +3,8 @@ import os
import random import random
import json import json
import re import re
import functools
try: try:
# 'regex' on PyPI supports `\p{L}`, `\p{N}`, etc. # 'regex' on PyPI supports `\p{L}`, `\p{N}`, etc.
@ -14,6 +16,36 @@ except ImportError:
DICTIONARY_PATH = "dictionary/" # Path to dictionary files 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]: def format_uptime(seconds: float) -> tuple[str, int]:
""" """
Convert seconds into a human-readable string: 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: if not command_name:
# User typed just "!help" => list all known commands from this bot # 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: if not loaded_cmds:
return await send_message(ctx, "I have no commands loaded.") return await send_message(ctx, "I have no commands loaded.")
else: else:
short_list = ", ".join(loaded_cmds) if is_discord:
# We can also mention "Use !help [command] for more info." help_str = f"I currently offer these commands:"
return await send_message( for cmd in loaded_cmds:
ctx, help_str += f"\n- !{cmd}"
f"I currently offer these commands: {short_list}\nUse '!help <command>' for details." help_str += f"\n*Use '!help <command>' 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 <command>' for details."
)
# 1) Check if the command is loaded # 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 # 2) Check if it has help info in the JSON
cmd_help = help_data["commands"].get(command_name, None) 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 bot.help_data = data
# Now cross-check the loaded commands vs. the 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: if "commands" not in data:
log_func(f"No 'commands' key in {help_json_path}, skipping checks.", "ERROR") log_func(f"No 'commands' key in {help_json_path}, skipping checks.", "ERROR")
return 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") 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 = [] commands_list = []
# For Discord.py try:
if hasattr(bot, "commands"): _bot_type = str(type(bot)).split("_")[1].split(".")[0]
for c_obj in bot.commands: log_func(f"Currently processing commands for {_bot_type} ...", "DEBUG")
commands_list.append(c_obj.name) except Exception as e:
log_func(f"Unable to determine current bot type: {e}", "WARNING")
# For TwitchIO # For Discord
if hasattr(bot, "all_commands"): if is_discord:
# each item is (command_name_str, command_object) #if isinstance(bot, discord_commands.Bot):
for cmd_name, cmd_obj in bot.all_commands.items(): try:
commands_list.append(cmd_name) # '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) return sorted(commands_list)