added channels command to share common channels, updated the satpass command to have common shortcuts

This commit is contained in:
agessaman
2025-10-01 18:43:00 -07:00
parent cf46a7b1de
commit cdcbf7a25b
4 changed files with 401 additions and 5 deletions
BIN
View File
Binary file not shown.
+41
View File
@@ -231,3 +231,44 @@ url_timeout = 10
# true: Use 24-hour UTC format
# false: Use 12-hour local format
use_zulu_time = false
[Channels_List]
# Common hashtag channels for the region
# Format: channel_name = description
# These channels will be listed when users use the 'channels' command
# The bot will automatically add the '#' prefix when displaying channels
# General channels (no category prefix)
general = General discussion and chat
weather = Weather updates and conditions
emergency = Emergency communications and alerts
# Sub-command channels (format: subcommand.channel_name = description)
# Example: channels sports -> sports.sounders = Seattle Sounders FC
# Sports-focused channels
sports.sounders = Seattle Sounders FC
sports.kraken = Seattle Kraken
sports.mariners = Seattle Mariners
sports.seahawks = Seattle Seahawks
sports.reign = OL Reign
sports.storm = Seattle Storm
sports.huskies = Washington Huskies
# Local area channels
local.capitolhill = Capitol Hill neighborhood
local.ballard = Ballard neighborhood
local.fremont = Fremont neighborhood
local.queenanne = Queen Anne neighborhood
# Technology channels
tech.mesh = Mesh networking and technical discussions
tech.hamradio = Amateur radio and ham radio topics
tech.programming = Programming and development
tech.iot = Internet of Things projects
# Emergency-focused channels
emergency.emergency = Emergency communications and alerts
emergency.weather = Weather updates and conditions
emergency.traffic = Traffic updates and road conditions
emergency.hamradio = Amateur radio emergency net
+304
View File
@@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""
Channels command for the MeshCore Bot
Lists common hashtag channels for the region with multi-message support
"""
from .base_command import BaseCommand
from ..models import MeshMessage
import asyncio
class ChannelsCommand(BaseCommand):
"""Handles the channels command"""
# Plugin metadata
name = "channels"
keywords = ['channels', 'channel']
description = "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels <category>' for specific categories, 'channels #channel' for specific channel info."
category = "basic"
def get_help_text(self) -> str:
return "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels <category>' for specific categories, 'channels #channel' for specific channel info."
async def execute(self, message: MeshMessage) -> bool:
"""Execute the channels command"""
try:
# Parse the command to check for sub-commands
content = message.content.strip()
if content.startswith('!'):
content = content[1:].strip()
# Check for sub-command (e.g., "channels seattle", "channel seahawks", "channels list", "channels #bot")
sub_command = None
specific_channel = None
if content.lower().startswith('channels ') or content.lower().startswith('channel '):
parts = content.split(' ', 1)
if len(parts) > 1:
sub_command = parts[1].strip().lower()
# Handle special "list" command to show all categories
if sub_command == 'list':
await self._show_all_categories(message)
return True
# Check if user is asking for a specific channel (starts with #)
if sub_command.startswith('#'):
specific_channel = sub_command
sub_command = None
else:
# Check if this might be a channel search (not a category)
# Try to find a channel that matches this name across all categories
found_channel = self._find_channel_by_name(sub_command)
if found_channel:
specific_channel = '#' + found_channel
sub_command = None
# Handle specific channel request
if specific_channel:
await self._show_specific_channel(message, specific_channel)
return True
# Load channels from config (with sub-command support)
channels = self._load_channels_from_config(sub_command)
if not channels:
if sub_command:
await self.send_response(message, f"No channels configured for '{sub_command}'. Use 'channels' for general channels.")
else:
await self.send_response(message, "No channels configured. Contact admin to add channels.")
return True
# Build channel list (names only, no descriptions)
channel_list = []
for channel_name, description in channels.items():
channel_list.append(channel_name) # Just the channel name
# Split into multiple messages if needed (130 character limit)
messages = self._split_into_messages(channel_list, sub_command)
# Send each message with a small delay between them
for i, msg_content in enumerate(messages):
if i > 0:
# Small delay between messages to prevent overwhelming the network
await asyncio.sleep(0.5)
await self.send_response(message, msg_content)
return True
except Exception as e:
self.logger.error(f"Error in channels command: {e}")
await self.send_response(message, f"Error retrieving channels: {e}")
return False
def _load_channels_from_config(self, sub_command: str = None) -> dict:
"""Load channels from the Channels_List config section with optional sub-command filtering"""
channels = {}
if self.bot.config.has_section('Channels_List'):
for channel_name, description in self.bot.config.items('Channels_List'):
# Skip empty or commented lines
if channel_name.strip() and not channel_name.startswith('#'):
# Strip quotes if present
if description.startswith('"') and description.endswith('"'):
description = description[1:-1]
# Handle sub-command filtering
if sub_command:
# Check if this channel belongs to the sub-command
if not channel_name.startswith(f'{sub_command}.'):
continue
# Remove the sub-command prefix for display
display_name = channel_name[len(sub_command) + 1:] # Remove 'subcommand.'
else:
# For general channels, only show channels that don't have sub-command prefixes
if '.' in channel_name:
continue
display_name = channel_name
# Add # prefix if not already present
if not display_name.startswith('#'):
display_name = '#' + display_name
channels[display_name] = description
return channels
async def _show_all_categories(self, message: MeshMessage):
"""Show all available channel categories"""
try:
categories = self._get_all_categories()
if not categories:
await self.send_response(message, "No channel categories configured.")
return
# Build category list
category_list = []
for category, count in categories.items():
category_list.append(f"{category} ({count} channels)")
# Split into multiple messages if needed
messages = self._split_into_messages(category_list, "Available categories")
# Send each message with a small delay between them
for i, msg_content in enumerate(messages):
if i > 0:
await asyncio.sleep(0.5)
await self.send_response(message, msg_content)
except Exception as e:
self.logger.error(f"Error showing categories: {e}")
await self.send_response(message, f"Error retrieving categories: {e}")
def _get_all_categories(self) -> dict:
"""Get all available channel categories and their channel counts"""
categories = {}
if self.bot.config.has_section('Channels_List'):
for channel_name, description in self.bot.config.items('Channels_List'):
# Skip empty or commented lines
if channel_name.strip() and not channel_name.startswith('#'):
# Check if this is a sub-command channel (has a dot)
if '.' in channel_name:
category = channel_name.split('.')[0]
if category not in categories:
categories[category] = 0
categories[category] += 1
else:
# General channels (no category)
if 'general' not in categories:
categories['general'] = 0
categories['general'] += 1
return categories
def _find_channel_by_name(self, search_name: str) -> str:
"""Find a channel by partial name match across all categories"""
search_name_lower = search_name.lower()
if self.bot.config.has_section('Channels_List'):
for config_name, description in self.bot.config.items('Channels_List'):
# Skip empty or commented lines
if config_name.strip() and not config_name.startswith('#'):
# Handle sub-command channels
if '.' in config_name:
category, name = config_name.split('.', 1)
# Check if the channel name matches (case insensitive)
if name.lower() == search_name_lower:
return name
else:
# Check general channels
if config_name.lower() == search_name_lower:
return config_name
return None
async def _show_specific_channel(self, message: MeshMessage, channel_name: str):
"""Show description for a specific channel"""
try:
# Search for the channel in all categories
found_channel = None
found_category = None
if self.bot.config.has_section('Channels_List'):
for config_name, description in self.bot.config.items('Channels_List'):
# Skip empty or commented lines
if config_name.strip() and not config_name.startswith('#'):
# Handle sub-command channels
if '.' in config_name:
category, name = config_name.split('.', 1)
display_name = '#' + name
else:
display_name = '#' + config_name
# Check if this matches the requested channel
if display_name.lower() == channel_name.lower():
found_channel = display_name
found_category = category if '.' in config_name else 'general'
break
if found_channel:
# Get the description
if found_category == 'general':
config_key = found_channel[1:] # Remove #
else:
config_key = f"{found_category}.{found_channel[1:]}" # Remove #
description = self.bot.config.get('Channels_List', config_key, fallback='No description available')
# Strip quotes if present
if description.startswith('"') and description.endswith('"'):
description = description[1:-1]
response = f"{found_channel}: {description}"
await self.send_response(message, response)
else:
await self.send_response(message, f"Channel {channel_name} not found. Use 'channels list' to see available channels.")
except Exception as e:
self.logger.error(f"Error showing specific channel: {e}")
await self.send_response(message, f"Error retrieving channel info: {e}")
def _split_into_messages(self, channel_list: list, sub_command: str = None) -> list:
"""Split channel list into multiple messages if they exceed 130 characters"""
messages = []
# Set appropriate header based on sub-command
if sub_command == "Available categories":
current_message = "Available Categories: "
elif sub_command:
current_message = f"{sub_command.title()}: "
else:
current_message = "Common channels: "
current_length = len(current_message)
for channel in channel_list:
# Check if adding this channel would exceed the limit
if current_length + len(channel) + 2 > 130: # +2 for ", " separator
# Start a new message
if sub_command == "Available categories":
expected_header = "Available Categories: "
else:
expected_header = f"{sub_command.title()} channels: " if sub_command else "Common channels: "
if current_message != expected_header:
messages.append(current_message.rstrip(", "))
current_message = "Channels (cont): "
current_length = len(current_message)
else:
# If even the first channel is too long, just send it alone
messages.append(f"{expected_header}{channel}")
current_message = "Channels (cont): "
current_length = len(current_message)
continue
# Add channel to current message
if sub_command == "Available categories":
header = "Available Categories: "
else:
header = f"{sub_command.title()} channels: " if sub_command else "Common channels: "
if current_message == header or current_message == "Channels (cont): ":
current_message += channel
else:
current_message += f", {channel}"
current_length = len(current_message)
# Add the last message if it has content
if sub_command == "Available categories":
header = "Available Categories: "
else:
header = f"{sub_command.title()} channels: " if sub_command else "Common channels: "
if current_message != header and current_message != "Channels (cont): ":
messages.append(current_message)
# If no messages were created, send a default message
if not messages:
if sub_command:
messages.append(f"No {sub_command} channels configured")
else:
messages.append("No channels configured")
return messages
+56 -5
View File
@@ -14,9 +14,19 @@ class SatpassCommand(BaseCommand):
# Plugin metadata
name = "satpass"
keywords = ['satpass']
description = "Get satellite pass info: satpass <NORAD_number>"
description = "Get satellite pass info: satpass <NORAD_number_or_shortcut>"
category = "solar"
# Common satellite shortcuts
SATELLITE_SHORTCUTS = {
'iss': '25544',
'hst': '20580', # Hubble Space Telescope
'hubble': '20580',
'starlink': '44294', # Example Starlink satellite
'tiangong': '48274', # Tiangong space station
'goes18': '51850', # GOES-18 weather satellite
}
def __init__(self, bot):
super().__init__(bot)
@@ -27,18 +37,25 @@ class SatpassCommand(BaseCommand):
content = message.content.strip()
if content == 'satpass':
# No satellite specified, show help
help_text = "🛰️ Satellite Pass Info\nUsage: satpass <NORAD_number>\nExamples:\n• satpass 25544 (ISS)\n• satpass 33591 (NOAA-15)"
help_text = self._get_help_text()
await self.send_response(message, help_text)
return True
# Extract satellite number from command
# Extract satellite identifier from command
parts = content.split()
if len(parts) < 2:
error_msg = "Please provide a satellite NORAD number. Example: satpass 25544"
error_msg = "Please provide a satellite NORAD number or shortcut. Example: satpass iss"
await self.send_response(message, error_msg)
return True
satellite = parts[1]
satellite_input = parts[1].lower()
# Check if it's a shortcut first
if satellite_input in self.SATELLITE_SHORTCUTS:
satellite = self.SATELLITE_SHORTCUTS[satellite_input]
else:
# Assume it's a NORAD number
satellite = satellite_input
# Get satellite pass information
pass_info = get_next_satellite_pass(satellite)
@@ -53,6 +70,40 @@ class SatpassCommand(BaseCommand):
await self.send_response(message, error_msg)
return False
def _get_help_text(self):
"""Get detailed help text with shortcuts"""
shortcuts_text = "🛰️ Satellite Pass Info\n\nUsage: satpass <NORAD_number_or_shortcut>\n\nShortcuts:\n"
# Group shortcuts by category for better organization
weather_sats = ['noaa15', 'noaa18', 'noaa19', 'metop-a', 'metop-b', 'metop-c', 'goes16', 'goes17', 'goes18']
space_stations = ['iss', 'tiangong', 'tiangong1', 'tiangong2']
telescopes = ['hst', 'hubble']
other = ['starlink']
# Add weather satellites
shortcuts_text += "🌤️ Weather: "
weather_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in weather_sats if name in self.SATELLITE_SHORTCUTS]
shortcuts_text += ", ".join(weather_list) + "\n"
# Add space stations
shortcuts_text += "🚀 Stations: "
station_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in space_stations if name in self.SATELLITE_SHORTCUTS]
shortcuts_text += ", ".join(station_list) + "\n"
# Add telescopes
shortcuts_text += "🔭 Telescopes: "
telescope_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in telescopes if name in self.SATELLITE_SHORTCUTS]
shortcuts_text += ", ".join(telescope_list) + "\n"
# Add other satellites
shortcuts_text += "🛰️ Other: "
other_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in other if name in self.SATELLITE_SHORTCUTS]
shortcuts_text += ", ".join(other_list) + "\n"
shortcuts_text += "\nExamples:\n• satpass iss\n• satpass noaa15\n• satpass 25544"
return shortcuts_text
def get_help_text(self):
"""Get help text for this command"""
return self.description