mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
- Added a new FAQ section addressing common issues when moving a database to a new install, including schema mismatches, stale operations, and timeout errors. - Updated error logging in `feed_manager.py` and `scheduler.py` to use `logger.exception` for better traceback visibility during message queue and channel operation errors. - Improved command help text generation in `command_manager.py` to ensure proper formatting and context filtering based on message length. - Enhanced command availability checks in `cmd_command.py` and `help_command.py` to respect channel-specific overrides and improve user experience.
194 lines
7.9 KiB
Python
194 lines
7.9 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 .base_command import BaseCommand
|
|
from ..models import MeshMessage
|
|
|
|
|
|
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')
|
|
|
|
def can_execute(self, message: MeshMessage) -> 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.keys():
|
|
# 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 = []
|
|
prefix = "Available commands: "
|
|
current_length = len(prefix)
|
|
|
|
for cmd in command_names:
|
|
# Calculate length if we add this command: ", cmd" or "cmd" (first one)
|
|
if result:
|
|
test_length = current_length + len(', ') + len(cmd)
|
|
else:
|
|
test_length = 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:
|
|
# 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)
|