added generic per-command channel configuration, allows commands to be mapped to different channels or dms only

This commit is contained in:
agessaman
2025-11-25 21:19:48 -08:00
parent 82aa5ee160
commit 5c2af1a014
8 changed files with 155 additions and 300 deletions
+35 -25
View File
@@ -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 <command>' 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 <command>'"
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
-239
View File
@@ -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 <PID>
```
**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.**
+2 -2
View File
@@ -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
+60
View File
@@ -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
+15 -11
View File
@@ -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()
+5 -8
View File
@@ -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
+23 -13
View File
@@ -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
+15 -2
View File
@@ -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