+ Added !help command
- Removed built-in Discord !help command NOTE: Basic implementation. Help text is defined in: - dictionary/help_twitch.json - dictionary/help_discord.jsonkami_dev
parent
780ec2e540
commit
28d22da0c1
|
@ -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")
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 bot’s latency or respond with a pun.",
|
||||
"subcommands": {},
|
||||
"examples": ["!ping"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
Loading…
Reference in New Issue