mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-31 10:02:38 +00:00
A codex review of the branch found seven places where earlier refactor commits changed behavior. Each is restored here with a test: - base_service imported flood_scope relatively; the local-service loader also runs that file as local_services.base_service, where the import cannot resolve. Now absolute. - Radio-health fields were initialized after commands and services were built, so a plugin constructor reading is_radio_offline raised. They are now set before any plugin. - Removing the per-command can_execute overrides made 31 commands honor skip_channel_check, which their overrides had dropped. BaseCommand.honors_skip_channel_check (False on exactly those commands) keeps them enforcing their channel list. - CommandManager.get_plugin_metadata, WxCommand.get_weather_for_zipcode and PluginLoader.validate_plugin were public compatibility entry points; they are back. - feed_manager stopped exporting DEFAULT_MAX_FEED_RESPONSE_BYTES and DEFAULT_MAX_PARSED_FEED_ITEMS despite the commit saying it re-exported them; it does now. - wx and gwx logged companion-location lookup failures at WARNING; the shared helper logged at debug. It now takes the level. - The INFO 'MESSAGE ROUTING' / 'DIRECT MESSAGE' lines for pubkey-correlated DMs were removed with the dead path_info computation; they are logged again by a log-only helper.
178 lines
7.2 KiB
Python
178 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 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)
|