mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-09-01 16:48:26 +00:00
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:
+1
-4
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user