mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-04-27 19:35:27 +00:00
90fdd0c77a
Introduced a new configuration option `cmd_reference_url` in the Cmd_Command section, allowing users to override the default `cmd` output with a link to a full documentation page. Updated the CmdCommand class to utilize this new setting and modified related documentation and tests to ensure proper functionality.
198 lines
8.0 KiB
Python
198 lines
8.0 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 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)
|