From 5c2af1a01403a512a82d258e9fcdffa1c5c99a72 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 25 Nov 2025 21:19:48 -0800 Subject: [PATCH] added generic per-command channel configuration, allows commands to be mapped to different channels or dms only --- config.ini.example | 60 ++++--- docs/MODERN_WEBVIEWER_README.md | 239 ---------------------------- modules/channel_manager.py | 4 +- modules/commands/base_command.py | 60 +++++++ modules/commands/greeter_command.py | 26 +-- modules/commands/sports_command.py | 13 +- modules/core.py | 36 +++-- modules/message_handler.py | 17 +- 8 files changed, 155 insertions(+), 300 deletions(-) delete mode 100644 docs/MODERN_WEBVIEWER_README.md diff --git a/config.ini.example b/config.ini.example index 4e9ac5b..ca17349 100644 --- a/config.ini.example +++ b/config.ini.example @@ -85,6 +85,29 @@ bot_latitude = 40.7128 # Example: -74.0060 for New York City, -123.00 for Victoria BC bot_longitude = -74.0060 +# Maximum number of channels to fetch from MeshCore node +# MeshCore supports up to 40 channels (default: 40) +# Set to a lower value if you want to limit channel fetching for performance +max_channels = 12 + +# Interval-based advertising settings +# Send periodic flood adverts at specified intervals +# 0: Disabled (default) +# >0: Send flood advert every N hours +advert_interval_hours = 0 + +# Send startup advert when bot finishes initializing +# false: No startup advert (default) +# zero-hop: Send local broadcast advert +# flood: Send network-wide flood advert +startup_advert = false + +# Auto-manage contact list when new contacts are discovered +# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) +# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) +# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) +auto_manage_contacts = false + [Localization] # Language code for bot responses (en, es, es-MX, es-ES, fr, de, ja, etc.) # Default: en (English) @@ -166,24 +189,6 @@ dadjoke_enabled = true # true: Split long jokes into multiple messages long_jokes = false -# Send startup advert when bot finishes initializing -# false: No startup advert (default) -# zero-hop: Send local broadcast advert -# flood: Send network-wide flood advert -startup_advert = false - -# Auto-manage contact list when new contacts are discovered -# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) -# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) -# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) -auto_manage_contacts = false - -# Interval-based advertising settings -# Send periodic flood adverts at specified intervals -# 0: Disabled (default) -# >0: Send flood advert every N hours -advert_interval_hours = 0 - [Keywords] # Keyword-response pairs (keyword = response format) # Available fields: {sender}, {connection_info}, {snr}, {timestamp}, {path}, {path_distance}, {firstlast_distance} @@ -198,7 +203,7 @@ advert_interval_hours = 0 test = "ack @[{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}" ping = "Pong!" pong = "Ping!" -help = "Bot Help: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats | Use 'help ' for details" +help = "Bot Help: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats | More: 'help '" cmd = "Available commands: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats" [Channels] @@ -256,10 +261,12 @@ meshcore_log_level = INFO enabled = false # Channels where greetings should occur (comma-separated) +# IMPORTANT: Leave commented out (or omit entirely) to use global monitor_channels (default behavior) +# If uncommented with empty value (channels = ), command will be DM-only +# Comma-separated list to restrict to specific channels (only greeter command works there) +# Example: channels = general,welcome,newbies # If not specified, uses the channels from [Channels] monitor_channels setting -# Example: general,welcome,newbies -# Leave empty to use monitor_channels from [Channels] section -channels = +# channels = # Greeting message template (default for all channels) # Available fields: {sender} - the user's name/ID @@ -484,9 +491,12 @@ sports_enabled = true # Comma-separated list of team names (use lowercase) teams = seahawks,mariners,sounders,kraken -# Channels where sports command is allowed (leave empty for all channels) -# Comma-separated list of channel names -channels = general,#bot,#sounders,#seahawks +# Channels where sports command is allowed +# IMPORTANT: Leave commented out (or omit entirely) to use global monitor_channels (default behavior) +# If uncommented with empty value (channels = ), command will be DM-only +# Comma-separated list to restrict to specific channels (only sports command works there) +# Example: channels = #sounders,#seahawks (only sports command works in these channels) +# channels = general,#bot,#sounders,#seahawks # Channel overrides for sports command # Format: channel_name = default_team diff --git a/docs/MODERN_WEBVIEWER_README.md b/docs/MODERN_WEBVIEWER_README.md deleted file mode 100644 index 4d049e6..0000000 --- a/docs/MODERN_WEBVIEWER_README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Modern MeshCore Bot Data Viewer v2.0 - -## 🚀 Complete Replacement Using Flask-SocketIO 5.x Best Practices - -This is a **complete ground-up rewrite** of the MeshCore Bot Data Viewer, designed to eliminate all the recurring issues with the original implementation. - -## 🔍 Why a Complete Rewrite? - -The original web viewer had **fundamental architectural issues**: - -- ❌ **Fighting Against Flask-SocketIO Design**: Manual background threads with complex timeout handling -- ❌ **Resource Leaks**: 170+ file descriptors, connection leaks, memory issues -- ❌ **Hanging Issues**: Background thread blocking, SocketIO emit timeouts -- ❌ **Circuit Breaker Problems**: Too aggressive settings, no auto-recovery -- ❌ **Poor Connection Management**: Manual tracking instead of built-in features - -## ✅ Modern Solution Benefits - -### **1. Event-Driven Architecture** -- **No Background Threads**: Uses Flask-SocketIO's built-in event system -- **Direct Broadcasting**: `socketio.emit()` instead of complex queue management -- **Automatic Connection Management**: Built-in client tracking and cleanup - -### **2. Flask-SocketIO 5.x Best Practices** -- **Proper Configuration**: Modern ping/pong, timeouts, and logging -- **Event Handlers**: Clean `@socketio.on()` decorators -- **Built-in Error Handling**: Proper SocketIO error management - -### **3. Performance Improvements** -- **90% Reduction in File Descriptors**: 170+ → <20 -- **No More Hanging**: Event-driven prevents thread blocking -- **Better Stability**: Built-in connection lifecycle management -- **Lower Resource Usage**: No background threads or complex timeouts - -## 📁 File Structure - -``` -modules/web_viewer/ -├── modern_viewer.py # Main modern web viewer -├── templates/ -│ ├── modern_base.html # Base template with navigation -│ ├── modern_index.html # Dashboard -│ ├── modern_realtime.html # Real-time monitoring -│ ├── modern_contacts.html # Contact management -│ ├── contacts.html # Contact tracking -│ ├── modern_cache.html # Cache management -│ ├── modern_purging.html # Purging log -│ └── modern_stats.html # Statistics -└── start_modern_viewer.sh # Startup script -``` - -## 🚀 Quick Start - -### **1. Start the Modern Web Viewer** -```bash -# Make executable and run -chmod +x start_modern_viewer.sh -./start_modern_viewer.sh -``` - -### **2. Access the Web Interface** -- **Dashboard**: http://127.0.0.1:8080/ -- **Real-time**: http://127.0.0.1:8080/realtime -- **Contacts**: http://127.0.0.1:8080/contacts -- **Tracking**: http://127.0.0.1:8080/tracking -- **Cache**: http://127.0.0.1:8080/cache -- **Purging**: http://127.0.0.1:8080/purging -- **Stats**: http://127.0.0.1:8080/stats - -### **3. API Endpoints** -- **Health Check**: `GET /api/health` -- **Statistics**: `GET /api/stats` -- **Contacts**: `GET /api/contacts` -- **Tracking**: `GET /api/tracking` -- **Cache**: `GET /api/cache` -- **Purging**: `GET /api/purging` -- **Stream Data**: `POST /api/stream_data` - -## 🔧 Features - -### **Complete Feature Parity** -- ✅ **Dashboard**: System overview and quick actions -- ✅ **Real-time Monitoring**: Command and packet streaming -- ✅ **Contact Management**: View, search, and manage contacts -- ✅ **Contact Tracking**: Monitor contact activity and routing -- ✅ **Cache Management**: Database cache and storage management -- ✅ **Purging Log**: Data cleanup and maintenance logs -- ✅ **Statistics**: Performance and usage analytics - -### **Modern Improvements** -- ✅ **Responsive Design**: Bootstrap 5.1.3 with modern UI -- ✅ **Real-time Updates**: WebSocket connections with auto-reconnection -- ✅ **Search & Filtering**: Advanced contact and data filtering -- ✅ **Export Functionality**: CSV export for all data types -- ✅ **Error Handling**: Comprehensive error management -- ✅ **Performance Monitoring**: Built-in health checks and metrics - -## 🛠️ Technical Architecture - -### **Backend (Flask-SocketIO 5.x)** -```python -# Modern SocketIO Configuration -self.socketio = SocketIO( - self.app, - cors_allowed_origins="*", - max_http_buffer_size=1000000, - ping_timeout=5, # Flask-SocketIO 5.x default - ping_interval=25, # Flask-SocketIO 5.x default - logger=True, # Proper logging - engineio_logger=True, # EngineIO logging - async_mode='threading' # Better stability -) -``` - -### **Frontend (Modern JavaScript)** -```javascript -// Modern Socket.IO client with proper error handling -class ModernConnectionManager { - constructor() { - this.socket = io({ - transports: ['websocket', 'polling'], - timeout: 5000, - forceNew: true - }); - - this.setupSocketEvents(); - this.startPingInterval(); - } -} -``` - -### **Database Integration** -- **Connection Pooling**: Thread-safe database connections -- **Automatic Cleanup**: Connection timeout and refresh -- **Error Handling**: Comprehensive database error management - -## 📊 Performance Comparison - -| Metric | Original | Modern v2.0 | Improvement | -|--------|----------|-------------|-------------| -| **File Descriptors** | 170+ | <20 | 90% reduction | -| **Hanging Issues** | Every 40 minutes | None | 100% eliminated | -| **Circuit Breaker Trips** | Frequent | Rare | 95% reduction | -| **Code Complexity** | High | Low | 80% reduction | -| **Maintainability** | Difficult | Easy | 90% improvement | - -## 🔄 Migration Strategy - -### **Phase 1: Parallel Deployment** -1. Deploy modern implementation alongside current one -2. Test with real bot data for 24+ hours -3. Compare performance and stability metrics - -### **Phase 2: Gradual Migration** -1. Switch bot integration to use modern web viewer -2. Monitor for extended periods (48+ hours) -3. Verify all functionality works correctly - -### **Phase 3: Full Replacement** -1. Replace current web viewer with modern implementation -2. Remove old code and dependencies -3. Update documentation and deployment scripts - -## 🐛 Troubleshooting - -### **Common Issues** - -**1. Port Already in Use** -```bash -# Check what's using port 8080 -lsof -i :8080 -# Kill the process if needed -kill -9 -``` - -**2. Database Connection Issues** -```bash -# Check database file permissions -ls -la meshcore_bot.db -# Ensure the file is readable/writable -``` - -**3. SocketIO Connection Issues** -- Check browser console for errors -- Verify WebSocket support in browser -- Check firewall settings - -### **Logs and Debugging** -- **Web Viewer Logs**: `logs/web_viewer_modern.log` -- **Bot Integration Logs**: `meshcore_bot.log` -- **Health Check**: `GET /api/health` - -## 🎯 Expected Results - -### **Immediate Benefits** -- ✅ **No More Hanging**: 40-minute hanging pattern eliminated -- ✅ **No More Circuit Breaker Trips**: Robust connection handling -- ✅ **No More Resource Leaks**: Proper connection lifecycle management -- ✅ **Better Performance**: Lower resource usage, faster response times - -### **Long-term Benefits** -- ✅ **Easier Maintenance**: Clean, modern codebase -- ✅ **Better Debugging**: Comprehensive logging and error handling -- ✅ **Future-proof**: Following Flask-SocketIO best practices -- ✅ **Scalable**: Event-driven architecture scales better - -## 📈 Monitoring - -### **Health Checks** -- **System Status**: Dashboard shows real-time system health -- **Connection Status**: WebSocket connection monitoring -- **Database Health**: Automatic database connection management -- **Performance Metrics**: Built-in performance monitoring - -### **Key Metrics to Monitor** -- **File Descriptors**: Should stay <20 (vs 170+ before) -- **Connection Stability**: No hanging or timeouts -- **Memory Usage**: Stable, no leaks -- **Response Times**: Fast, consistent performance - -## 🚀 Next Steps - -1. **Test the Modern Implementation**: Run for 24+ hours to verify stability -2. **Compare with Original**: Monitor resource usage and performance -3. **Migrate Bot Integration**: Update bot to use modern web viewer -4. **Full Deployment**: Replace original implementation completely - -## 📞 Support - -For issues or questions about the modern web viewer: - -1. **Check Logs**: Review `logs/web_viewer_modern.log` -2. **Health Check**: Visit `http://127.0.0.1:8080/api/health` -3. **Restart**: Use `./start_modern_viewer.sh` to restart -4. **Debug Mode**: Add `--debug` flag for detailed logging - ---- - -**The modern web viewer represents a complete architectural improvement, eliminating all the fundamental issues with the original implementation while providing better performance, stability, and maintainability.** diff --git a/modules/channel_manager.py b/modules/channel_manager.py index c96a6b7..ef0d3bb 100644 --- a/modules/channel_manager.py +++ b/modules/channel_manager.py @@ -14,13 +14,13 @@ from meshcore import EventType class ChannelManager: """Manages channel operations and information with enhanced concurrent fetching""" - def __init__(self, bot, max_channels: int = 8): + def __init__(self, bot, max_channels: int = 40): """ Initialize the channel manager Args: bot: The MeshCore bot instance - max_channels: Maximum number of channels to fetch (default 8) + max_channels: Maximum number of channels to fetch (default 40) """ self.bot = bot self.logger = bot.logger diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index 6dcf83a..d9efb1b 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -26,6 +26,9 @@ class BaseCommand(ABC): self.bot = bot self.logger = bot.logger self._last_execution_time = 0 + + # Load allowed channels from config (standardized channel override) + self.allowed_channels = self._load_allowed_channels() # Load translated keywords after initialization self._load_translated_keywords() @@ -130,8 +133,65 @@ class BaseCommand(ABC): """Get help text for this command""" return self.description or "No help available for this command." + def _load_allowed_channels(self) -> Optional[List[str]]: + """ + Load allowed channels from config. + + Config format: [CommandName_Command] + channels = channel1,channel2,channel3 + + Returns: + - None: Use global monitor_channels (default behavior) + - Empty list []: Command disabled for all channels (only DMs) + - List of channels: Command only works in these channels + """ + # Derive section name from command name + # Convert "sports" -> "Sports_Command", "greeter" -> "Greeter_Command", etc. + section_name = f"{self.name.title().replace('_', '_')}_Command" + + # Try to get channels config + channels_str = self.get_config_value(section_name, 'channels', fallback=None, value_type='str') + + if channels_str is None: + return None # Use global monitor_channels + + if channels_str.strip() == '': + return [] # Disabled for all channels (DM only) + + # Parse comma-separated list + channels = [ch.strip() for ch in channels_str.split(',') if ch.strip()] + return channels if channels else None + + def is_channel_allowed(self, message: MeshMessage) -> bool: + """ + Check if this command is allowed in the message's channel. + + Returns: + - True if DM and command allows DMs (unless requires_dm is False, but that's separate) + - True if channel is in allowed_channels (or None for global) + - False otherwise + """ + # DMs are always allowed (unless requires_dm is False, but that's checked separately) + if message.is_dm: + return True + + # If no channel override, use global monitor_channels + if self.allowed_channels is None: + return message.channel in self.bot.command_manager.monitor_channels + + # If empty list, command is disabled for channels (DM only) + if self.allowed_channels == []: + return False + + # Check if channel is in allowed list + return message.channel in self.allowed_channels + def can_execute(self, message: MeshMessage) -> bool: """Check if this command can be executed with the given message""" + # Check channel access (standardized channel override) + if not self.is_channel_allowed(message): + return False + # Check if command requires DM and message is not DM if self.requires_dm and not message.is_dm: return False diff --git a/modules/commands/greeter_command.py b/modules/commands/greeter_command.py index 42aa957..acc64d2 100644 --- a/modules/commands/greeter_command.py +++ b/modules/commands/greeter_command.py @@ -74,7 +74,8 @@ class GreeterCommand(BaseCommand): if self.backfill_lookback_days == 0: self.backfill_lookback_days = None - # Load greeter-specific channels (if not set, uses monitor_channels from [Channels] section) + # Note: allowed_channels is now loaded by BaseCommand from config + # Keep greeter_channels for backward compatibility and case-insensitive matching channels_str = self.get_config_value('Greeter_Command', 'channels', fallback='') if channels_str: # Store both original and lowercase versions for case-insensitive matching @@ -766,16 +767,19 @@ class GreeterCommand(BaseCommand): if not message.channel: return False - # Check if channel is in greeter-specific channels or fall back to monitor_channels - if self.greeter_channels is not None: - # Use greeter-specific channels if configured (case-insensitive matching) - if message.channel and message.channel.lower() not in self.greeter_channels_lower: - return False - else: - # Fall back to general monitor_channels setting (case-insensitive matching) - monitor_channels_lower = [ch.lower() for ch in self.bot.command_manager.monitor_channels] - if message.channel and message.channel.lower() not in monitor_channels_lower: - return False + # Check channel access using standardized method (with case-insensitive fallback) + # First try standardized method (case-sensitive) + if not self.is_channel_allowed(message): + # If standardized check fails, try case-insensitive matching for backward compatibility + if self.greeter_channels is not None: + # Use greeter-specific channels if configured (case-insensitive matching) + if message.channel and message.channel.lower() not in self.greeter_channels_lower: + return False + else: + # Fall back to general monitor_channels setting (case-insensitive matching) + monitor_channels_lower = [ch.lower() for ch in self.bot.command_manager.monitor_channels] + if message.channel and message.channel.lower() not in monitor_channels_lower: + return False # Check if we're in an active rollout period rollout_active = self._is_rollout_active() diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index aae61d6..49ca8d7 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -425,6 +425,8 @@ class SportsCommand(BaseCommand): # Load default teams from config self.default_teams = self.load_default_teams() + # Note: allowed_channels is now loaded by BaseCommand from config + # Keep sports_channels for backward compatibility (used in execute() for channel-specific team defaults) self.sports_channels = self.load_sports_channels() self.channel_overrides = self.load_channel_overrides() @@ -518,16 +520,11 @@ class SportsCommand(BaseCommand): if not sports_enabled: return False - # Check if command requires DM and message is not DM - if self.requires_dm and not message.is_dm: + # Channel access is now handled by BaseCommand.is_channel_allowed() + # Call parent can_execute() which includes channel checking + if not super().can_execute(message): return False - # Check if command requires specific channels (only for channel messages, not DMs) - if not message.is_dm and self.sports_channels and message.channel not in self.sports_channels: - # Check if this channel has an override (allows sports command even if not in main channels list) - if message.channel not in self.channel_overrides: - return False - # Check per-user cooldown (don't set it here, just check) if self.cooldown_seconds > 0: import time diff --git a/modules/core.py b/modules/core.py index f8b8bbe..2c92a02 100644 --- a/modules/core.py +++ b/modules/core.py @@ -114,7 +114,11 @@ class MeshCoreBot: self.message_handler = MessageHandler(self) self.command_manager = CommandManager(self) - self.channel_manager = ChannelManager(self) + + # Load max_channels from config (default 40, MeshCore supports up to 40 channels) + max_channels = self.config.getint('Bot', 'max_channels', fallback=40) + self.channel_manager = ChannelManager(self, max_channels=max_channels) + self.scheduler = MessageScheduler(self) # Initialize repeater manager @@ -234,6 +238,24 @@ bot_latitude = 40.7128 # Example: -74.0060 for New York City, -123.00 for Victoria BC bot_longitude = -74.0060 +# Interval-based advertising settings +# Send periodic flood adverts at specified intervals +# 0: Disabled (default) +# >0: Send flood advert every N hours +advert_interval_hours = 0 + +# Send startup advert when bot finishes initializing +# false: No startup advert (default) +# zero-hop: Send local broadcast advert +# flood: Send network-wide flood advert +startup_advert = false + +# Auto-manage contact list when new contacts are discovered +# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) +# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) +# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) +auto_manage_contacts = false + [Jokes] # Enable or disable the joke command # true: Joke command is available @@ -256,18 +278,6 @@ dadjoke_enabled = true # true: Split long jokes into multiple messages long_jokes = false -# Send startup advert when bot finishes initializing -# false: No startup advert (default) -# zero-hop: Send local broadcast advert -# flood: Send network-wide flood advert -startup_advert = false - -# Auto-manage contact list when new contacts are discovered -# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) -# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) -# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) -auto_manage_contacts = false - [Admin_ACL] # Admin Access Control List (ACL) for restricted commands # Only users with public keys listed here can execute admin commands diff --git a/modules/message_handler.py b/modules/message_handler.py index 5366c6e..2aed5ab 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -1637,8 +1637,21 @@ class MessageHandler: self.logger.debug(f"Ignoring message from banned user: {message.sender_id}") return False - # Check if channel is monitored - if not message.is_dm and message.channel and message.channel not in self.bot.command_manager.monitor_channels: + # Check if channel is monitored (with command override support) + if not message.is_dm and message.channel: + # Check if channel is in global monitor_channels + if message.channel in self.bot.command_manager.monitor_channels: + return True # Global allow - all commands can work + + # Check if ANY command allows this channel (for selective access) + for command_name, command in self.bot.command_manager.commands.items(): + if hasattr(command, 'is_channel_allowed') and callable(command.is_channel_allowed): + if command.is_channel_allowed(message): + # At least one command allows this channel + self.logger.debug(f"Channel {message.channel} allowed by command '{command_name}' override") + return True + + # Channel not in global list and no command allows it self.logger.debug(f"Channel {message.channel} not in monitored channels: {self.bot.command_manager.monitor_channels}") return False