Files
meshcore-bot/modules/commands/cmd_command.py
agessaman 97acab182e feat(commands): implement command enablement checks in cmd and help commands
Added an `is_enabled` method to the BaseCommand class to determine if commands are enabled based on configuration. Updated CmdCommand and HelpCommand to skip disabled commands when listing available commands. Updated tests to verify that disabled commands are omitted from command lists.
2026-08-07 21:10:12 -07:00

200 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""
Cmd command for the MeshCore Bot
Lists available commands in a compact, comma-separated format for LoRa
"""
from typing import Any, Optional
from ..models import MeshMessage
from .base_command import BaseCommand
class CmdCommand(BaseCommand):
"""Handles the cmd command"""
# Plugin metadata
name = "cmd"
keywords = ['cmd', 'commands']
description = "Lists available commands in compact format"
category = "basic"
# Documentation
short_description = "Lists available commands"
usage = "cmd"
examples = ["cmd"]
def __init__(self, bot):
"""Initialize the cmd command.
Args:
bot: The bot instance.
"""
super().__init__(bot)
self.cmd_enabled = self.get_config_value('Cmd_Command', 'enabled', fallback=True, value_type='bool')
self.cmd_reference_url = self.get_config_value(
'Cmd_Command', 'cmd_reference_url', fallback='', value_type='str'
).strip()
def can_execute(self, message: MeshMessage, skip_channel_check: bool = False) -> bool:
"""Check if this command can be executed with the given message.
Args:
message: The message triggering the command.
Returns:
bool: True if command is enabled and checks pass, False otherwise.
"""
if not self.cmd_enabled:
return False
return super().can_execute(message)
def get_help_text(self) -> str:
"""Get help text for the cmd command.
Returns:
str: The help text for this command.
"""
return "Lists commands in compact format."
def _is_command_valid_for_channel(self, cmd_name: str, cmd_instance: Any, message: Optional[MeshMessage]) -> bool:
"""Return True if this command is valid in the message's channel context."""
if message is None:
return True
if hasattr(cmd_instance, 'is_channel_allowed') and callable(cmd_instance.is_channel_allowed):
if not cmd_instance.is_channel_allowed(message):
return False
if hasattr(self.bot.command_manager, '_is_channel_trigger_allowed'):
if not self.bot.command_manager._is_channel_trigger_allowed(cmd_name, message):
return False
return True
def _get_commands_list(self, message: Optional[MeshMessage] = None, max_length: Optional[int] = None) -> str:
"""Get a compact list of available commands, prioritizing important ones.
When message is provided, only includes commands that can execute in this
channel (respects per-command channel overrides and channel_keywords).
Args:
message: Optional message for context filtering. When provided, only
commands valid for this channel are included.
max_length: Maximum length for the command list (None = no limit).
Returns:
str: Comma-separated list of commands, truncated if necessary.
"""
# Define priority order - most important/commonly used commands first
priority_commands = [
'test', 'ping', 'help', 'hello', 'cmd', 'advert',
'wx', 'aqi', 'sun', 'moon', 'solar', 'hfcond', 'satpass',
'prefix', 'path', 'sports', 'dice', 'roll', 'stats'
]
# Get all command names (only those available in this context)
all_commands = []
# Include plugin commands that are valid for this channel
for cmd_name, cmd_instance in self.bot.command_manager.commands.items():
# Skip system commands without keywords (like greeter)
if hasattr(cmd_instance, 'keywords') and cmd_instance.keywords:
if hasattr(cmd_instance, 'is_enabled') and not cmd_instance.is_enabled():
continue
if not self._is_command_valid_for_channel(cmd_name, cmd_instance, message):
continue
all_commands.append(cmd_name)
# Include config keywords that aren't handled by plugins (and are allowed in channel)
for keyword in self.bot.command_manager.keywords:
# Check if this keyword is already handled by a plugin
is_plugin_keyword = any(
keyword.lower() in [k.lower() for k in cmd.keywords]
for cmd in self.bot.command_manager.commands.values()
)
if not is_plugin_keyword:
if message is not None and hasattr(self.bot.command_manager, '_is_channel_trigger_allowed'):
if not self.bot.command_manager._is_channel_trigger_allowed(keyword, message):
continue
all_commands.append(keyword)
# Remove duplicates and sort
all_commands = sorted(set(all_commands))
# Prioritize: put priority commands first, then others
prioritized = []
remaining = []
for cmd in all_commands:
if cmd in priority_commands:
prioritized.append(cmd)
else:
remaining.append(cmd)
# Sort priority commands by their order in priority_commands list
prioritized = sorted(prioritized, key=lambda x: priority_commands.index(x) if x in priority_commands else 999)
# Combine: priority first, then others
command_names = prioritized + sorted(remaining)
# Build the list, respecting max_length if provided
if max_length is None:
return ', '.join(command_names)
# Build list within length limit
result: list[str] = []
prefix = "Available commands: "
current_length = len(prefix)
for cmd in command_names:
# Calculate length if we add this command: ", cmd" or "cmd" (first one)
test_length = current_length + len(', ') + len(cmd) if result else current_length + len(cmd)
if test_length <= max_length:
if result:
result.append(cmd)
current_length += len(', ') + len(cmd)
else:
result.append(cmd)
current_length += len(cmd)
else:
# Can't fit this command, add count of remaining
remaining_count = len(command_names) - len(result)
if remaining_count > 0:
suffix = f" ({remaining_count} more)"
if current_length + len(suffix) <= max_length:
result.append(suffix)
break
return prefix + ', '.join(result)
async def execute(self, message: MeshMessage) -> bool:
"""Execute the cmd command.
Args:
message: The message triggering the command.
Returns:
bool: True if executed successfully, False otherwise.
"""
try:
if self.cmd_reference_url:
return await self.send_response(message, f"Full command reference: {self.cmd_reference_url}")
# Check if user has defined a custom cmd keyword response in config
# Use the already-loaded keywords dict (quotes are already stripped)
cmd_keyword = self.bot.command_manager.keywords.get('cmd')
if cmd_keyword:
# User has defined a custom response, use it with formatting
response = self.bot.command_manager.format_keyword_response(cmd_keyword, message)
return await self.send_response(message, response)
# Fallback to dynamic command list if no custom keyword is defined
# Get max message length to ensure we fit within limits
max_length = self.get_max_message_length(message)
# _get_commands_list handles the prefix internally; pass message for context filtering
response = self._get_commands_list(message=message, max_length=max_length)
return await self.send_response(message, response)
except Exception as e:
self.logger.error(f"Error executing cmd command: {e}")
error_msg = self.translate('errors.execution_error', command='cmd', error=str(e))
return await self.send_response(message, error_msg)