Files
meshcore-bot/modules/commands/cmd_command.py
T
agessaman ba8e3f0229 fix(refactor): restore 24 module-level names the moves stopped binding, and guard all of them
Codex's final review found that moving code out of modules dropped imports that local plugins could import from them, for example `from modules.service_plugins.discord_bridge_service import EventType`, which would make such a plugin fail to load. Comparing every top-level name bound on dev against the branch (both trees imported, so names under TYPE_CHECKING are excluded) found 24 in 11 modules: EventType, copy and contextlib in both bridges; sha256, Iterable and hmac_mod in message_handler; RotatingFileHandler, colorlog, contextlib, contextvars and functools in core; hashlib and re in utils; contextlib in feed_manager, earthquake_service and weather_service; Any in cmd and help; time in path. Each is re-imported exactly as dev imported it.

The public-import guard only covered `from modules... import` statements, so it missed these. It now records every unconditional top-level import and assignment, regenerated from dev's tree (461 → 1,597 names), and fails if the discord bridge stops exposing EventType.
2026-10-03 22:13:19 -07:00

181 lines
7.2 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, # noqa: F401 importable from this module on dev
Optional,
)
from ..models import MeshMessage
from .base_command import BaseCommand
class CmdCommand(BaseCommand):
"""Handles the cmd command"""
# Plugin metadata
name = "cmd"
honors_skip_channel_check = False
enabled_attr = "cmd_enabled"
keywords = ['cmd', 'cmds', 'command', '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 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 _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:
section_name = cmd_instance._derive_config_section_name()
if not cmd_instance.get_config_value(section_name, 'enabled', fallback=True, value_type='bool'):
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)