Refactor command syntax and enhance test command functionality

- Updated the test command response format to include optional phrases.
- Removed the at_phrase command and its references from the command manager and help command.
- Adjusted configuration examples to reflect the new syntax for message acknowledgments.
- Enhanced the sports command with additional WNBA team support and improved city mappings for team searches.
This commit is contained in:
agessaman
2025-09-17 21:12:43 -07:00
parent 9aaf29a988
commit 025d3d539f
9 changed files with 461 additions and 125 deletions
+1 -4
View File
@@ -113,7 +113,7 @@ auto_manage_contacts = false
# {timestamp}: Message timestamp in HH:MM:SS format
# {path}: Message routing path (e.g., "01,5f (2 hops)")
# {rssi}: Received Signal Strength Indicator in dBm
test = "Message received from {sender} | {connection_info} | Received at: {timestamp}"
test = "ack {sender}{phrase_part} | {connection_info} | Received at: {timestamp}"
ping = "Pong!"
pong = "Ping!"
help = "Bot Help: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix | Use 'help <command>' for details"
@@ -177,9 +177,6 @@ meshcore_log_level = INFO
# Example: "t hello world" -> "ack {sender}: hello world | {connection_info}"
t_phrase = "ack {sender}: {phrase} | {connection_info}"
# Special syntax: Messages starting with "@" followed by a phrase (DM only)
# Example: "@hello world" -> "ack {sender}: hello world | {connection_info}"
@_phrase = "ack {sender}: {phrase} | {connection_info}"
[External_Data]
# Weather API key (future feature)
+1 -3
View File
@@ -347,7 +347,7 @@ class CommandManager:
# Group commands by category
basic_commands = ['test', 'ping', 'help', 'cmd']
custom_syntax = ['t_phrase', 'at_phrase'] # Use the actual command key
custom_syntax = ['t_phrase'] # Use the actual command key
special_commands = ['advert']
weather_commands = ['wx', 'aqi']
solar_commands = ['sun', 'moon', 'solar', 'hfcond', 'satpass']
@@ -366,8 +366,6 @@ class CommandManager:
# Add user-friendly aliases
if cmd == 't_phrase':
commands_list += f"• `t phrase` - {help_text}\n"
elif cmd == 'at_phrase':
commands_list += f"• `@{{string}}` - {help_text}\n"
else:
commands_list += f"• `{cmd}` - {help_text}\n"
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env python3
"""
At-Phrase command for the MeshCore Bot
Handles the '@{string}' syntax for acknowledgments (DM only)
"""
from .base_command import BaseCommand
from ..models import MeshMessage
class AtPhraseCommand(BaseCommand):
"""Handles the @{string} command (DM and non-Public channels)"""
# Plugin metadata
name = "at_phrase"
keywords = [] # No keywords - only use custom syntax matching
description = "Responds to '@{string}' with ack + connection info (DM and non-Public channels)"
category = "custom_syntax"
requires_dm = False # Allow in channels, but restrict to non-Public channels in matches_custom_syntax
def get_help_text(self) -> str:
return "Responds to '@{string}' with ack + connection info (DM and non-Public channels)."
def matches_custom_syntax(self, message: MeshMessage) -> bool:
"""Check if message matches @_phrase syntax"""
# Strip exclamation mark if present (for command-style messages)
content = message.content.strip()
if content.startswith('!'):
content = content[1:].strip()
# Handle "@{string}" phrase syntax (DM and non-Public channels only)
if content.startswith('@') and len(content) > 1:
# Check if this is a DM or a non-Public channel
is_allowed = message.is_dm
if not message.is_dm and message.channel:
# Allow in all channels except "Public"
is_allowed = message.channel.lower() != "public"
if is_allowed:
phrase = content[1:].strip() # Get everything after "@" and strip whitespace
return bool(phrase) # Make sure there's actually a phrase
return False
def get_response_format(self) -> str:
"""Get the response format from config"""
if self.bot.config.has_section('Custom_Syntax'):
format_str = self.bot.config.get('Custom_Syntax', '@_phrase', fallback=None)
return self._strip_quotes_from_config(format_str) if format_str else None
return None
def format_response(self, message: MeshMessage, response_format: str) -> str:
"""Override to handle phrase extraction"""
# Strip exclamation mark if present (for command-style messages)
content = message.content.strip()
if content.startswith('!'):
content = content[1:].strip()
phrase = content[1:].strip() # Get everything after "@"
try:
connection_info = self.build_enhanced_connection_info(message)
timestamp = self.format_timestamp(message)
return response_format.format(
sender=message.sender_id or "Unknown",
phrase=phrase,
connection_info=connection_info,
path=message.path or "Unknown",
timestamp=timestamp,
snr=message.snr or "Unknown"
)
except (KeyError, ValueError) as e:
self.logger.warning(f"Error formatting @_phrase response: {e}")
return response_format
async def execute(self, message: MeshMessage) -> bool:
"""Execute the @{string} command"""
return await self.handle_keyword_match(message)
+1 -4
View File
@@ -31,7 +31,6 @@ class HelpCommand(BaseCommand):
"""Get help text for a specific command"""
# Map command aliases to their actual command names
command_aliases = {
'@': 'at_phrase',
't': 't_phrase',
'advert': 'advert',
'test': 'test',
@@ -83,7 +82,7 @@ class HelpCommand(BaseCommand):
# Group commands by category
basic_commands = ['test', 'ping', 'help']
custom_syntax = ['t_phrase', 'at_phrase'] # Use the actual command key
custom_syntax = ['t_phrase'] # Use the actual command key
special_commands = ['advert']
commands_list += "**Basic Commands:**\n"
@@ -99,8 +98,6 @@ class HelpCommand(BaseCommand):
# Add user-friendly aliases
if cmd == 't_phrase':
commands_list += f"• `t phrase` - {help_text}\n"
elif cmd == 'at_phrase':
commands_list += f"• `@{{string}}` - {help_text}\n"
else:
commands_list += f"• `{cmd}` - {help_text}\n"
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env python3
"""
Path Decode Command for the MeshCore Bot
Decodes hex path data to show which repeaters were involved in message routing
"""
import re
import time
from typing import List, Optional, Dict, Any
from .base_command import BaseCommand
from ..models import MeshMessage
class PathCommand(BaseCommand):
"""Command for decoding path data to repeater names"""
# Plugin metadata
name = "path"
keywords = ["path", "decode", "route"]
description = "Decode hex path data to show which repeaters were involved in message routing"
requires_dm = False
cooldown_seconds = 1
category = "meshcore_info"
def __init__(self, bot):
super().__init__(bot)
def matches_keyword(self, message: MeshMessage) -> bool:
"""Check if message starts with 'path' keyword"""
content = message.content.strip()
# Handle exclamation prefix
if content.startswith('!'):
content = content[1:].strip()
# Check if message starts with any of our keywords
content_lower = content.lower()
for keyword in self.keywords:
if content_lower.startswith(keyword + ' '):
return True
return False
async def execute(self, message: MeshMessage) -> bool:
"""Execute path decode command"""
self.logger.info(f"Path command executed with content: {message.content}")
# Parse the message content to extract path data
content = message.content.strip()
parts = content.split()
if len(parts) < 2:
response = self.get_help()
else:
# Extract path data from the command
path_input = " ".join(parts[1:])
response = await self._decode_path(path_input)
# Send the response
await self.bot.command_manager.send_response(message, response)
return True
async def _decode_path(self, path_input: str) -> str:
"""Decode hex path data to repeater names"""
try:
# Parse the path input - handle various formats
# Examples: "11,98,a4,49,cd,5f,01" or "11 98 a4 49 cd 5f 01" or "1198a449cd5f01"
path_input = path_input.replace(',', ' ').replace(':', ' ')
# Extract hex values using regex
hex_pattern = r'[0-9a-fA-F]{2}'
hex_matches = re.findall(hex_pattern, path_input)
if not hex_matches:
return "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01"
# Convert to uppercase for consistency
node_ids = [match.upper() for match in hex_matches]
self.logger.info(f"Decoding path with {len(node_ids)} nodes: {','.join(node_ids)}")
# Look up repeater names for each node ID
repeater_info = await self._lookup_repeater_names(node_ids)
# Format the response
return self._format_path_response(node_ids, repeater_info)
except Exception as e:
self.logger.error(f"Error decoding path: {e}")
return f"❌ Error decoding path: {e}"
async def _lookup_repeater_names(self, node_ids: List[str]) -> Dict[str, Dict[str, Any]]:
"""Look up repeater names for given node IDs"""
repeater_info = {}
try:
# First try to get data from API cache (like prefix command does)
api_data = await self._get_api_cache_data()
# Query the database for repeaters with matching prefixes
# Node IDs are typically the first 2 characters of the public key
for node_id in node_ids:
# Check API cache first
if api_data and node_id in api_data:
api_prefix_data = api_data[node_id]
if api_prefix_data['node_names']:
# Use API data
if len(api_prefix_data['node_names']) > 1:
# Multiple matches - show collision warning
repeater_info[node_id] = {
'found': True,
'collision': True,
'matches': len(api_prefix_data['node_names']),
'node_id': node_id,
'repeaters': [
{
'name': name,
'public_key': f"{node_id}...",
'device_type': 'Unknown',
'last_seen': 'API',
'is_active': True,
'source': 'api'
} for name in api_prefix_data['node_names']
]
}
else:
# Single match
repeater_info[node_id] = {
'name': api_prefix_data['node_names'][0],
'public_key': f"{node_id}...",
'device_type': 'Unknown',
'last_seen': 'API',
'is_active': True,
'found': True,
'collision': False,
'source': 'api'
}
continue
# Fallback to database if API cache doesn't have this prefix
query = '''
SELECT name, public_key, device_type, last_seen, is_active
FROM repeater_contacts
WHERE public_key LIKE ?
ORDER BY is_active DESC, last_seen DESC
'''
prefix_pattern = f"{node_id}%"
results = self.bot.db_manager.execute_query(query, (prefix_pattern,))
if results:
# Check for ID collisions (multiple repeaters with same prefix)
if len(results) > 1:
# Multiple matches - show collision warning
repeater_info[node_id] = {
'found': True,
'collision': True,
'matches': len(results),
'node_id': node_id,
'repeaters': [
{
'name': row['name'],
'public_key': row['public_key'],
'device_type': row['device_type'],
'last_seen': row['last_seen'],
'is_active': row['is_active']
} for row in results
]
}
else:
# Single match
row = results[0]
repeater_info[node_id] = {
'name': row['name'],
'public_key': row['public_key'],
'device_type': row['device_type'],
'last_seen': row['last_seen'],
'is_active': row['is_active'],
'found': True,
'collision': False
}
else:
# Also check device contacts for active repeaters
device_matches = []
if hasattr(self.bot.meshcore, 'contacts'):
for contact_key, contact_data in self.bot.meshcore.contacts.items():
public_key = contact_data.get('public_key', contact_key)
if public_key.startswith(node_id):
# Check if this is a repeater
if hasattr(self.bot, 'repeater_manager') and self.bot.repeater_manager._is_repeater_device(contact_data):
name = contact_data.get('adv_name', contact_data.get('name', 'Unknown'))
device_matches.append({
'name': name,
'public_key': public_key,
'device_type': contact_data.get('type', 'Unknown'),
'last_seen': 'Active',
'is_active': True,
'source': 'device'
})
if device_matches:
if len(device_matches) > 1:
# Multiple device matches - show collision warning
repeater_info[node_id] = {
'found': True,
'collision': True,
'matches': len(device_matches),
'node_id': node_id,
'repeaters': device_matches
}
else:
# Single device match
match = device_matches[0]
repeater_info[node_id] = {
'name': match['name'],
'public_key': match['public_key'],
'device_type': match['device_type'],
'last_seen': match['last_seen'],
'is_active': match['is_active'],
'found': True,
'collision': False,
'source': 'device'
}
else:
repeater_info[node_id] = {
'found': False,
'node_id': node_id
}
except Exception as e:
self.logger.error(f"Error looking up repeater names: {e}")
# Return basic info for all nodes
for node_id in node_ids:
repeater_info[node_id] = {
'found': False,
'node_id': node_id,
'error': str(e)
}
return repeater_info
async def _get_api_cache_data(self) -> Optional[Dict[str, Dict[str, Any]]]:
"""Get API cache data from the prefix command if available"""
try:
# Try to get the prefix command instance and its cache data
if hasattr(self.bot, 'command_manager'):
prefix_cmd = self.bot.command_manager.commands.get('prefix')
if prefix_cmd and hasattr(prefix_cmd, 'cache_data'):
# Check if cache is valid
current_time = time.time()
if current_time - prefix_cmd.cache_timestamp > prefix_cmd.cache_duration:
await prefix_cmd.refresh_cache()
return prefix_cmd.cache_data
except Exception as e:
self.logger.warning(f"Could not get API cache data: {e}")
return None
def _format_path_response(self, node_ids: List[str], repeater_info: Dict[str, Dict[str, Any]]) -> str:
"""Format the path decode response (max 130 chars per line)"""
# Group results by found/not found/collision
found_repeaters = []
collision_nodes = []
unknown_nodes = []
for node_id in node_ids:
info = repeater_info.get(node_id, {})
if info.get('found', False):
if info.get('collision', False):
collision_nodes.append((node_id, info))
else:
found_repeaters.append((node_id, info))
else:
unknown_nodes.append(node_id)
# Build response lines (each max 130 chars)
lines = []
# Show found repeaters (compact format)
if found_repeaters:
for node_id, info in found_repeaters:
name = info['name']
# Truncate name if too long
if len(name) > 27:
name = name[:24] + "..."
line = f"{node_id}: {name}"
if len(line) > 130:
line = line[:127] + "..."
lines.append(line)
# Show collision nodes (compact format)
if collision_nodes:
for node_id, info in collision_nodes:
matches = info.get('matches', 0)
line = f"{node_id}: {matches} repeaters"
if len(line) > 130:
line = line[:127] + "..."
lines.append(line)
# Show unknown nodes (compact format)
if unknown_nodes:
unknown_str = f"Unknown: {','.join(unknown_nodes)}"
if len(unknown_str) > 130:
# Split if too long
for node_id in unknown_nodes:
lines.append(f"Unknown: {node_id}")
else:
lines.append(unknown_str)
return "\n".join(lines)
def get_help(self) -> str:
"""Get help text for the path command"""
return """Path Decode: !path <hex_data>
Decode hex path to show repeaters involved in routing.
Examples:
• !path 11,98,a4,49,cd,5f,01
• !path 11 98 a4 49 cd 5f 01
• !path 1198a449cd5f01
Shows repeater names, collisions, and unknown nodes.
Uses local database - run !repeater scan to update."""
+7 -6
View File
@@ -37,7 +37,7 @@ class PrefixCommand(BaseCommand):
self.session = None
def get_help_text(self) -> str:
return "Look up repeaters by two-character prefix. Uses API data with local database fallback. Usage: 'prefix 1A', 'prefix free' (list available prefixes), or 'prefix refresh'."
return "Look up repeaters by two-character prefix. Usage: 'prefix 1A', 'prefix free' (list available prefixes), or 'prefix refresh'."
def matches_keyword(self, message: MeshMessage) -> bool:
"""Check if message starts with 'prefix' keyword"""
@@ -47,8 +47,9 @@ class PrefixCommand(BaseCommand):
if content.startswith('!'):
content = content[1:].strip()
# Check if message starts with 'prefix'
return content.lower().startswith('prefix ')
# Check if message starts with 'prefix' (with or without space)
content_lower = content.lower()
return content_lower == 'prefix' or content_lower.startswith('prefix ')
async def execute(self, message: MeshMessage) -> bool:
"""Execute the prefix command"""
@@ -61,7 +62,7 @@ class PrefixCommand(BaseCommand):
# Parse the command
parts = content.split()
if len(parts) < 2:
response = "Usage: prefix <two-character-prefix> (e.g., 'prefix 1A'), 'prefix free', or 'prefix refresh'"
response = self.get_help_text()
return await self.send_response(message, response)
command = parts[1].upper()
@@ -266,7 +267,7 @@ class PrefixCommand(BaseCommand):
if not free_prefixes:
return "❌ No free prefixes found (all 254 valid prefixes are in use)"
response = f"🆓 Available Prefixes ({len(free_prefixes)} of {total_free} free shown):\n"
response = f"Available Prefixes ({len(free_prefixes)} of {total_free} free):\n"
# Format as a grid for better readability
for i, prefix in enumerate(free_prefixes, 1):
@@ -280,7 +281,7 @@ class PrefixCommand(BaseCommand):
if len(free_prefixes) % 5 != 0:
response += "\n"
response += f"\n💡 Generate a key for a specific prefix at https://gessaman.com/mc-keygen"
response += f"\n💡 Generate a custom key: https://gessaman.com/mc-keygen"
return response
+82 -20
View File
@@ -39,8 +39,34 @@ class SportsCommand(BaseCommand):
# Custom team abbreviations to distinguish between leagues
TEAM_ABBREVIATIONS = {
# NWSL teams - use custom abbreviations to distinguish from MLS
'15363': 'SEA-W', # Seattle Reign (Women's)
'20905': 'LOU-W', # Racing Louisville (Women's)
'21422': 'LA-W', # Angel City FC (Women's)
'22187': 'BAY-W', # Bay FC (Women's)
'15360': 'CHI-W', # Chicago Stars FC (Women's)
'15364': 'GFC-W', # Gotham FC (Women's)
'17346': 'HOU-W', # Houston Dash (Women's)
'20907': 'KC-W', # Kansas City Current (Women's)
'15366': 'NC-W', # North Carolina Courage (Women's)
'18206': 'ORL-W', # Orlando Pride (Women's)
'15362': 'POR-W', # Portland Thorns FC (Women's)
'20905': 'LOU-W', # Racing Louisville FC (Women's)
'21423': 'SD-W', # San Diego Wave FC (Women's)
'15363': 'SEA-W', # Seattle Reign FC (Women's)
'19141': 'UTA-W', # Utah Royals (Women's)
'15365': 'WAS-W', # Washington Spirit (Women's)
# WNBA teams - use custom abbreviations to distinguish from NBA
'14': 'SEA-W', # Seattle Storm (Women's)
'9': 'NY-W', # New York Liberty (Women's)
'6': 'LA-W', # Los Angeles Sparks (Women's)
'19': 'CHI-W', # Chicago Sky (Women's)
'20': 'ATL-W', # Atlanta Dream (Women's)
'18': 'CON-W', # Connecticut Sun (Women's)
'3': 'DAL-W', # Dallas Wings (Women's)
'129689': 'GS-W', # Golden State Valkyries (Women's)
'5': 'IND-W', # Indiana Fever (Women's)
'17': 'LV-W', # Las Vegas Aces (Women's)
'8': 'MIN-W', # Minnesota Lynx (Women's)
'11': 'PHX-W', # Phoenix Mercury (Women's)
'16': 'WSH-W', # Washington Mystics (Women's)
}
# Team mappings for common searches
@@ -236,6 +262,34 @@ class SportsCommand(BaseCommand):
'knicks': {'sport': 'basketball', 'league': 'nba', 'team_id': '18'},
'pelicans': {'sport': 'basketball', 'league': 'nba', 'team_id': '3'},
# WNBA Teams
'storm': {'sport': 'basketball', 'league': 'wnba', 'team_id': '14'},
'seattle storm': {'sport': 'basketball', 'league': 'wnba', 'team_id': '14'},
'liberty': {'sport': 'basketball', 'league': 'wnba', 'team_id': '9'},
'new york liberty': {'sport': 'basketball', 'league': 'wnba', 'team_id': '9'},
'sparks': {'sport': 'basketball', 'league': 'wnba', 'team_id': '6'},
'los angeles sparks': {'sport': 'basketball', 'league': 'wnba', 'team_id': '6'},
'sky': {'sport': 'basketball', 'league': 'wnba', 'team_id': '19'},
'chicago sky': {'sport': 'basketball', 'league': 'wnba', 'team_id': '19'},
'dream': {'sport': 'basketball', 'league': 'wnba', 'team_id': '20'},
'atlanta dream': {'sport': 'basketball', 'league': 'wnba', 'team_id': '20'},
'sun': {'sport': 'basketball', 'league': 'wnba', 'team_id': '18'},
'connecticut sun': {'sport': 'basketball', 'league': 'wnba', 'team_id': '18'},
'wings': {'sport': 'basketball', 'league': 'wnba', 'team_id': '3'},
'dallas wings': {'sport': 'basketball', 'league': 'wnba', 'team_id': '3'},
'valkyries': {'sport': 'basketball', 'league': 'wnba', 'team_id': '129689'},
'golden state valkyries': {'sport': 'basketball', 'league': 'wnba', 'team_id': '129689'},
'fever': {'sport': 'basketball', 'league': 'wnba', 'team_id': '5'},
'indiana fever': {'sport': 'basketball', 'league': 'wnba', 'team_id': '5'},
'aces': {'sport': 'basketball', 'league': 'wnba', 'team_id': '17'},
'las vegas aces': {'sport': 'basketball', 'league': 'wnba', 'team_id': '17'},
'lynx': {'sport': 'basketball', 'league': 'wnba', 'team_id': '8'},
'minnesota lynx': {'sport': 'basketball', 'league': 'wnba', 'team_id': '8'},
'mercury': {'sport': 'basketball', 'league': 'wnba', 'team_id': '11'},
'phoenix mercury': {'sport': 'basketball', 'league': 'wnba', 'team_id': '11'},
'mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
'washington mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
# NHL Teams (limited data available from API)
'kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '58'},
'seattle kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '58'},
@@ -505,6 +559,11 @@ class SportsCommand(BaseCommand):
'nba': {'sport': 'basketball', 'league': 'nba'},
'basketball': {'sport': 'basketball', 'league': 'nba'},
# WNBA
'wnba': {'sport': 'basketball', 'league': 'wnba'},
'womens basketball': {'sport': 'basketball', 'league': 'wnba'},
'womens': {'sport': 'basketball', 'league': 'wnba'},
# NHL
'nhl': {'sport': 'hockey', 'league': 'nhl'},
'hockey': {'sport': 'hockey', 'league': 'nhl'},
@@ -532,23 +591,23 @@ class SportsCommand(BaseCommand):
# Define city mappings to team names
city_mappings = {
'seattle': ['seahawks', 'mariners', 'sounders', 'kraken', 'reign'],
'chicago': ['bears', 'cubs', 'white sox', 'fire'],
'new york': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls'],
'ny': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls'],
'los angeles': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc'],
'la': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc'],
'seattle': ['seahawks', 'mariners', 'sounders', 'kraken', 'reign', 'storm'],
'chicago': ['bears', 'cubs', 'white sox', 'fire', 'sky'],
'new york': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty'],
'ny': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty'],
'los angeles': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc', 'sparks'],
'la': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc', 'sparks'],
'miami': ['dolphins', 'marlins', 'heat', 'inter miami'],
'boston': ['patriots', 'red sox', 'celtics', 'revolution'],
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
'atlanta': ['falcons', 'braves', 'hawks', 'atlanta united'],
'atlanta': ['falcons', 'braves', 'hawks', 'atlanta united', 'dream'],
'houston': ['texans', 'astros', 'dynamo'],
'dallas': ['cowboys', 'rangers', 'stars', 'fc dallas'],
'dallas': ['cowboys', 'rangers', 'stars', 'fc dallas', 'wings'],
'denver': ['broncos', 'rockies', 'rapids'],
'detroit': ['lions', 'tigers', 'pistons'],
'minnesota': ['vikings', 'twins', 'timberwolves', 'minnesota united'],
'minneapolis': ['vikings', 'twins', 'timberwolves', 'minnesota united'],
'minnesota': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx'],
'minneapolis': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx'],
'cleveland': ['browns', 'guardians', 'cavaliers'],
'cincinnati': ['bengals', 'reds', 'fc cincinnati'],
'pittsburgh': ['steelers', 'pirates', 'penguins'],
@@ -557,20 +616,23 @@ class SportsCommand(BaseCommand):
'tampa bay': ['buccaneers', 'rays', 'lightning'],
'kansas city': ['chiefs', 'royals', 'sporting kc'],
'kc': ['chiefs', 'royals', 'sporting kc'],
'washington': ['commanders', 'nationals', 'wizards', 'dc united', 'mystics'],
'dc': ['commanders', 'nationals', 'wizards', 'dc united', 'mystics'],
'phoenix': ['cardinals', 'diamondbacks', 'suns', 'mercury'],
'indiana': ['colts', 'pacers', 'fever'],
'indianapolis': ['colts', 'pacers', 'fever'],
'las vegas': ['raiders', 'aces', 'golden knights'],
'connecticut': ['sun'],
'arizona': ['cardinals', 'diamondbacks', 'coyotes'],
'phoenix': ['cardinals', 'diamondbacks', 'coyotes'],
'san francisco': ['49ers', 'giants', 'warriors', 'earthquakes'],
'sf': ['49ers', 'giants', 'warriors', 'earthquakes'],
'golden state': ['warriors', 'valkyries'],
'san francisco': ['49ers', 'giants', 'warriors', 'earthquakes', 'valkyries'],
'sf': ['49ers', 'giants', 'warriors', 'earthquakes', 'valkyries'],
'san diego': ['chargers', 'padres', 'san diego fc'],
'sd': ['chargers', 'padres', 'san diego fc'],
'washington': ['commanders', 'nationals', 'wizards', 'dc united'],
'dc': ['commanders', 'nationals', 'wizards', 'dc united'],
'indianapolis': ['colts', 'pacers'],
'ind': ['colts', 'pacers'],
'nashville': ['titans', 'predators', 'nashville sc'],
'tennessee': ['titans', 'predators', 'nashville sc'],
'ten': ['titans', 'predators', 'nashville sc'],
'las vegas': ['raiders', 'golden knights'],
'lv': ['raiders', 'golden knights'],
'louisville': ['racing'],
'carolina': ['panthers', 'hornets'],
@@ -869,7 +931,7 @@ class SportsCommand(BaseCommand):
# Otherwise, treat as single team query
team_info = self.TEAM_MAPPINGS.get(team_name)
if not team_info:
return f"Team/League '{team_name}' not found. Try: seahawks, mariners, sounders, kraken, chiefs, lfc, mlb, nfl, mls, epl, etc."
return f"Team/League '{team_name}' not found. Try: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc."
try:
score_info = await self.fetch_team_score(team_info)
+44 -7
View File
@@ -22,14 +22,18 @@ class TestCommand(BaseCommand):
return self.description
def matches_keyword(self, message: MeshMessage) -> bool:
"""Override to implement special test keyword matching"""
content_lower = message.content.lower().strip()
"""Override to implement special test keyword matching with optional phrase"""
# Strip exclamation mark if present (for command-style messages)
content = message.content.strip()
if content.startswith('!'):
content = content[1:].strip()
# For "test", only match if it's the first word or its own word
# Split by whitespace and clean up punctuation
words = re.findall(r'\b\w+\b', content_lower)
if words and (words[0] == "test" or "test" in words):
return True
# Handle "test" alone or "test " with phrase
if content.lower() == "test":
return True # Just "test" by itself
elif (content.startswith('test ') or content.startswith('Test ')) and len(content) > 5:
phrase = content[5:].strip() # Get everything after "test " and strip whitespace
return bool(phrase) # Make sure there's actually a phrase
return False
@@ -40,6 +44,39 @@ class TestCommand(BaseCommand):
return self._strip_quotes_from_config(format_str) if format_str else None
return None
def format_response(self, message: MeshMessage, response_format: str) -> str:
"""Override to handle phrase extraction"""
# Strip exclamation mark if present (for command-style messages)
content = message.content.strip()
if content.startswith('!'):
content = content[1:].strip()
# Extract phrase if present, otherwise use empty string
if content.lower() == "test":
phrase = ""
else:
phrase = content[5:].strip() # Get everything after "test "
try:
connection_info = self.build_enhanced_connection_info(message)
timestamp = self.format_timestamp(message)
# Format phrase part - add colon and space if phrase exists
phrase_part = f": {phrase}" if phrase else ""
return response_format.format(
sender=message.sender_id or "Unknown",
phrase=phrase,
phrase_part=phrase_part,
connection_info=connection_info,
path=message.path or "Unknown",
timestamp=timestamp,
snr=message.snr or "Unknown"
)
except (KeyError, ValueError) as e:
self.logger.warning(f"Error formatting test response: {e}")
return response_format
async def execute(self, message: MeshMessage) -> bool:
"""Execute the test command"""
return await self.handle_keyword_match(message)
+1 -4
View File
@@ -211,7 +211,7 @@ auto_manage_contacts = false
# {snr}: Signal-to-noise ratio in dB
# {timestamp}: Message timestamp in HH:MM:SS format
# {path}: Message routing path (e.g., "01,5f (2 hops)")
test = "Message received from {sender} | {connection_info} | Received at: {timestamp}"
test = "ack {sender}{phrase_part} | {connection_info} | Received at: {timestamp}"
ping = "Pong!"
pong = "Ping!"
help = "Bot Help: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass | Use 'help <command>' for details"
@@ -274,9 +274,6 @@ meshcore_log_level = INFO
# Example: "t hello world" -> "ack {sender}: hello world | {connection_info}"
t_phrase = "ack {sender}: {phrase} | {connection_info}"
# Special syntax: Messages starting with "@" followed by a phrase (DM only)
# Example: "@hello world" -> "ack {sender}: hello world | {connection_info}"
@_phrase = "ack {sender}: {phrase} | {connection_info}"
[External_Data]
# Weather API key (future feature)