mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-06 18:59:37 +00:00
added channels command to share common channels, updated the satpass command to have common shortcuts
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user