+ Added !help command

- Removed built-in Discord !help command

NOTE:
Basic implementation.
Help text is defined in:
  - dictionary/help_twitch.json
  - dictionary/help_discord.json
kami_dev
Kami 2025-02-03 14:14:30 +01:00
parent 780ec2e540
commit 28d22da0c1
7 changed files with 351 additions and 25 deletions

View File

@ -4,14 +4,17 @@ from discord.ext import commands
import importlib
import cmd_discord
from modules import db
import modules
import modules.utility
class DiscordBot(commands.Bot):
def __init__(self, config, log_func):
super().__init__(command_prefix="!", intents=discord.Intents.all())
self.remove_command("help") # Remove built-in help function
self.config = config
self.log = log_func # Use the logging function from bots.py
self.db_conn = None # We'll set this later
self.help_data = None # We'll set this later
self.load_commands()
def set_db_connection(self, db_conn):
@ -20,18 +23,29 @@ class DiscordBot(commands.Bot):
"""
self.db_conn = db_conn
try:
db.ensure_quotes_table(self.db_conn, self.log)
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):
"""
Load all commands dynamically from cmd_discord.py.
Load all commands from cmd_discord.py
"""
try:
importlib.reload(cmd_discord)
cmd_discord.setup(self)
self.log("Discord commands loaded successfully.", "INFO")
# Now load the help info from dictionary/help_discord.json
help_json_path = "dictionary/help_discord.json"
modules.utility.initialize_help_data(
bot=self,
help_json_path=help_json_path,
is_discord=True,
log_func=self.log
)
except Exception as e:
self.log(f"Error loading Discord commands: {e}", "ERROR")

View File

@ -6,7 +6,8 @@ from twitchio.ext import commands
import importlib
import cmd_twitch
from modules import db
import modules
import modules.utility
class TwitchBot(commands.Bot):
def __init__(self, config, log_func):
@ -16,7 +17,8 @@ class TwitchBot(commands.Bot):
self.refresh_token = os.getenv("TWITCH_REFRESH_TOKEN")
self.log = log_func # Use the logging function from bots.py
self.config = config
self.db_conn = None # We'll set this later
self.db_conn = None # We'll set this
self.help_data = None # We'll set this later
# 1) Initialize the parent Bot FIRST
super().__init__(
@ -36,7 +38,7 @@ class TwitchBot(commands.Bot):
"""
self.db_conn = db_conn
try:
db.ensure_quotes_table(self.db_conn, self.log)
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")
@ -122,12 +124,22 @@ class TwitchBot(commands.Bot):
def load_commands(self):
"""
Load all commands dynamically from cmd_twitch.py.
Load all commands from cmd_twitch.py
"""
try:
importlib.reload(cmd_twitch)
cmd_twitch.setup(self)
self.log("Twitch commands loaded successfully.", "INFO")
# Now load the help info from dictionary/help_twitch.json
help_json_path = "dictionary/help_twitch.json"
modules.utility.initialize_help_data(
bot=self,
help_json_path=help_json_path,
is_discord=False, # Twitch
log_func=self.log
)
except Exception as e:
self.log(f"Error loading Twitch commands: {e}", "ERROR")

View File

@ -2,6 +2,7 @@
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
def setup(bot, db_conn=None, log=None):
@ -16,24 +17,24 @@ def setup(bot, db_conn=None, log=None):
# Auto-create the quotes table if desired
if db_conn and log:
cc.create_quotes_table(db_conn, log)
@bot.command()
async def greet(ctx):
@bot.command(name="greet")
async def cmd_greet(ctx):
result = cc.greet(ctx.author.display_name, "Discord")
await ctx.send(result)
@bot.command()
async def ping(ctx):
@bot.command(name="ping")
async def cmd_ping(ctx):
result = cc.ping()
await ctx.send(result)
@bot.command()
async def howl(ctx):
@bot.command(name="howl")
async def cmd_howl(ctx):
"""Calls the shared !howl logic."""
result = cc.howl(ctx.author.display_name)
await ctx.send(result)
@bot.command()
async def reload(ctx):
@bot.command(name="reload")
async def cmd_reload(ctx):
""" Dynamically reloads Discord commands. """
try:
import cmd_discord
@ -44,8 +45,8 @@ def setup(bot, db_conn=None, log=None):
except Exception as e:
await ctx.send(f"Error reloading commands: {e}")
@bot.command()
async def hi(ctx):
@bot.command(name="hi")
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
@ -56,7 +57,7 @@ def setup(bot, db_conn=None, log=None):
await ctx.send("Hello there!")
@bot.command(name="quote")
async def quote_command(ctx, *args):
async def cmd_quote(ctx, *args):
"""
!quote
!quote add <text>
@ -74,4 +75,12 @@ def setup(bot, db_conn=None, log=None):
ctx=ctx,
args=list(args),
get_twitch_game_for_channel=None # None for Discord
)
)
@bot.command(name="help")
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)

View File

@ -3,6 +3,7 @@
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
def setup(bot, db_conn=None, log=None):
"""
@ -15,22 +16,22 @@ def setup(bot, db_conn=None, log=None):
cc.create_quotes_table(bot.db_conn, bot.log)
@bot.command(name="greet")
async def greet(ctx):
async def cmd_greet(ctx):
result = cc.greet(ctx.author.display_name, "Twitch")
await ctx.send(result)
@bot.command(name="ping")
async def ping(ctx):
async def cmd_ping(ctx):
result = cc.ping()
await ctx.send(result)
@bot.command(name="howl")
async def howl(ctx):
async def cmd_howl(ctx):
result = cc.howl(ctx.author.display_name)
await ctx.send(result)
@bot.command(name="hi")
async def hi_command(ctx):
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
@ -40,7 +41,7 @@ def setup(bot, db_conn=None, log=None):
await ctx.send("Hello there!")
@bot.command(name="quote")
async def quote(ctx: commands.Context):
async def cmd_quote(ctx: commands.Context):
if not bot.db_conn:
return await ctx.send("Database is unavailable, sorry.")
@ -58,4 +59,10 @@ def setup(bot, db_conn=None, log=None):
ctx=ctx,
args=args,
get_twitch_game_for_channel=get_twitch_game_for_channel
)
)
@bot.command(name="help")
async def help_command(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)

View File

@ -0,0 +1,68 @@
{
"commands": {
"help": {
"description": "Show information about available commands.",
"subcommands": {},
"examples": ["!help", "!help quote"]
},
"quote": {
"description": "Manage quotes (add, remove, fetch).",
"subcommands": {
"add": {
"args": "[quote_text]",
"desc": "Adds a new quote."
},
"remove": {
"args": "[quote_number]",
"desc": "Removes the specified quote by ID."
},
"[quote_number]": {
"desc": "Fetch a specific quote by ID."
},
"no subcommand": {
"desc": "Fetch a random quote."
}
},
"examples": [
"!quote add This is my new quote : Add a new quote",
"!quote remove 3 : Remove quote # 3",
"!quote 5 : Fetch quote # 5",
"!quote : Fetch a random quote"
]
},
"ping": {
"description": "Check my uptime.",
"subcommands": {},
"examples": ["!ping"]
},
"howl": {
"description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)",
"subcommands": {},
"examples": [
"!howl"
]
},
"hi": {
"description": "Hello there.",
"subcommands": {},
"examples": [
"!hi"
]
},
"greet": {
"description": "Make me greet you to Discord!",
"subcommands": {},
"examples": [
"!greet"
]
},
"reload": {
"description": "Reload Discord commands dynamically. TODO.",
"subcommands": {},
"examples": [
"!reload"
]
}
}
}

View File

@ -0,0 +1,40 @@
{
"commands": {
"help": {
"description": "Show info about available commands in Twitch chat.",
"subcommands": {},
"examples": ["!help", "!help quote"]
},
"quote": {
"description": "Manage quotes (add, remove, fetch).",
"subcommands": {
"add": {
"args": "[quote_text]",
"desc": "Adds a new quote."
},
"remove": {
"args": "[quote_number]",
"desc": "Removes the specified quote by ID."
},
"[quote_number]": {
"desc": "Fetch a specific quote by ID."
},
"no subcommand": {
"desc": "Fetch a random quote."
}
},
"examples": [
"!quote add This is my new quote",
"!quote remove 3",
"!quote 5",
"!quote"
]
},
"ping": {
"description": "Check bots latency or respond with a pun.",
"subcommands": {},
"examples": ["!ping"]
}
}
}

View File

@ -171,3 +171,179 @@ def sanitize_user_input(
reason_string = "; ".join(reasons)
return (sanitized, sanitization_applied, reason_string, original_string)
#####################
# Help command logic
#####################
async def handle_help_command(ctx, command_name, bot, is_discord, log_func):
"""
Called by the platform-specific help commands to provide the help text.
:param ctx: discord.py or twitchio context
:param command_name: e.g. "quote" or None if user typed just "!help"
:param bot: The current bot instance
:param is_discord: True for Discord, False for Twitch
:param log_func: The logging function
"""
# If there's no loaded help_data, we can't do much
if not hasattr(bot, "help_data") or not bot.help_data:
return await send_message(ctx, "No help data found.")
help_data = bot.help_data # The parsed JSON from e.g. help_discord.json
if "commands" not in help_data:
return await send_message(ctx, "Invalid help data structure (no 'commands' key).")
if not command_name:
# User typed just "!help" => list all known commands from this bot
loaded_cmds = get_loaded_commands(bot)
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."
)
# 1) Check if the command is loaded
loaded = (command_name in get_loaded_commands(bot))
# 2) Check if it has help info in the JSON
cmd_help = help_data["commands"].get(command_name, None)
if loaded and cmd_help:
# The command is loaded, and we have help info => show it
if is_discord:
msg = build_discord_help_message(command_name, cmd_help)
else:
msg = build_twitch_help_message(command_name, cmd_help)
await send_message(ctx, msg)
elif loaded and not cmd_help:
# The command is loaded but no help info => mention that
await send_message(ctx, f"The '{command_name}' command is loaded but has no help info yet.")
elif (not loaded) and cmd_help:
# The command is not loaded, but we have an entry => mention it's unloaded/deprecated
await send_message(ctx, f"The '{command_name}' command is not currently loaded (deprecated or unavailable).")
else:
# Not loaded, no help info => not found at all
await send_message(ctx, f"I'm sorry, I don't offer a command named '{command_name}'.")
def initialize_help_data(bot, help_json_path, is_discord, log_func):
"""
Loads help data from a JSON file, stores it in bot.help_data,
then verifies each loaded command vs. the help_data.
Logs any mismatches:
- Commands in help file but not loaded => "deprecated"
- Loaded commands not in help file => "missing help"
"""
if not os.path.exists(help_json_path):
log_func(f"Help file '{help_json_path}' not found. No help data loaded.", "WARNING")
bot.help_data = {}
return
# Load the JSON
try:
with open(help_json_path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
log_func(f"Error parsing help JSON '{help_json_path}': {e}", "ERROR")
data = {}
bot.help_data = data
# Now cross-check the loaded commands vs. the data
loaded_cmds = set(get_loaded_commands(bot))
if "commands" not in data:
log_func(f"No 'commands' key in {help_json_path}, skipping checks.", "ERROR")
return
file_cmds = set(data["commands"].keys())
# 1) Commands in file but not loaded
missing_cmds = file_cmds - loaded_cmds
for cmd in missing_cmds:
log_func(f"Help file has '{cmd}', but it's not loaded on this {'Discord' if is_discord else 'Twitch'} bot (deprecated?).", "WARNING")
# 2) Commands loaded but not in file
needed_cmds = loaded_cmds - file_cmds
for cmd in needed_cmds:
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):
commands_list = []
# For Discord.py
if hasattr(bot, "commands"):
for c_obj in bot.commands:
commands_list.append(c_obj.name)
# 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)
return sorted(commands_list)
def build_discord_help_message(cmd_name, cmd_help_dict):
"""
A verbose multi-line string for Discord.
"""
description = cmd_help_dict.get("description", "No description available.\n")
subcommands = cmd_help_dict.get("subcommands", {})
examples = cmd_help_dict.get("examples", [])
lines = [f"**Help for `!{cmd_name}`**:",
f"Description: {description}"]
if subcommands:
lines.append("\n**Subcommands / Arguments:**")
for sub, detail in subcommands.items():
arg_part = detail.get("args", "")
desc_part = detail.get("desc", "")
lines.append(f"• **{sub}** {arg_part} **->** {desc_part}")
else:
lines.append("\n*No subcommands defined.*")
if examples:
lines.append("\nExample usage:")
for ex in examples:
ex_cmd = ex.split(" : ")[0]
ex_note = ex.split(" : ")[1]
lines.append(f"- `{ex_cmd}`\n {ex_note}")
return "\n".join(lines)
def build_twitch_help_message(cmd_name, cmd_help_dict):
"""
A concise, possibly single-line help for Twitch.
"""
description = cmd_help_dict.get("description", "No description available.")
subcommands = cmd_help_dict.get("subcommands", {})
sub_line_parts = []
for sub, detail in subcommands.items():
if sub == "no subcommand":
sub_line_parts.append(f"!{cmd_name}")
else:
arg_part = detail.get("args", "")
if arg_part:
sub_line_parts.append(f"!{cmd_name} {sub} {arg_part}".strip())
else:
sub_line_parts.append(f"!{cmd_name} {sub}".strip())
usage_line = " | ".join(sub_line_parts) if sub_line_parts else f"!{cmd_name}"
return f"Help for !{cmd_name}: {description}. Usage: {usage_line}"
async def send_message(ctx, text):
"""
Minimal helper to send a message to either Discord or Twitch.
"""
await ctx.send(text)