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.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):
"""

View File

@ -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")

View File

@ -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

View File

@ -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)
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
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)
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": {
"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*)",

View File

@ -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 <command>' 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 <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
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)