From 578fa2caa4fc5882ff278b4bc1ba310e43bcbcd7 Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 5 Jan 2026 17:20:15 -0800 Subject: [PATCH] fix: Prevent potential segfaults by deep copying event payloads - Added deep copy of event payloads in multiple modules to avoid segmentation faults when events are freed. - Updated the handling of payloads in ChannelManager, MessageHandler, DiscordBridgeService, MapUploaderService, and PacketCaptureService to ensure safe access to event data. - Enhanced logging for cases where payloads are missing, improving error handling and debugging capabilities. --- modules/channel_manager.py | 11 +- modules/commands/repeater_command.py | 723 +++--------------- modules/message_handler.py | 48 +- .../service_plugins/discord_bridge_service.py | 7 +- .../service_plugins/map_uploader_service.py | 7 +- .../service_plugins/packet_capture_service.py | 13 +- 6 files changed, 179 insertions(+), 630 deletions(-) diff --git a/modules/channel_manager.py b/modules/channel_manager.py index 66cbda9..2aeb684 100644 --- a/modules/channel_manager.py +++ b/modules/channel_manager.py @@ -8,6 +8,7 @@ import asyncio import sys import os import hashlib +import copy from typing import Dict, Any, List, Optional from meshcore import EventType @@ -574,8 +575,9 @@ class ChannelManager: async def on_channel_info(event): nonlocal channel_set - if event.payload.get('channel_idx') == channel_idx: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload and payload.get('channel_idx') == channel_idx: if payload.get('channel_name') == channel_name: channel_set = True event_received.set() @@ -660,8 +662,9 @@ class ChannelManager: async def on_channel_info(event): nonlocal channel_cleared - if event.payload.get('channel_idx') == channel_idx: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload and payload.get('channel_idx') == channel_idx: event_secret = payload.get('channel_secret', b'') # Check if the channel was cleared (all zeros or empty name) if isinstance(event_secret, bytes) and event_secret == empty_secret: diff --git a/modules/commands/repeater_command.py b/modules/commands/repeater_command.py index 9600f55..75e5895 100644 --- a/modules/commands/repeater_command.py +++ b/modules/commands/repeater_command.py @@ -12,11 +12,11 @@ from typing import List, Optional class RepeaterCommand(BaseCommand): """Command for managing repeater contacts. - + Provides functionality to scan, list, purge, and manage repeater and companion contacts within the mesh network. Includes automated cleanup tools and statistics. """ - + # Plugin metadata name = "repeater" keywords = ["repeater", "repeaters", "rp"] @@ -25,9 +25,44 @@ class RepeaterCommand(BaseCommand): cooldown_seconds = 0 category = "management" requires_internet = True # Requires internet access for geocoding (Nominatim) - + + # LoRa message size limit (conservative to avoid overload) + # DM limit is 150 chars, public channel is 237 chars + MAX_LORA_MESSAGE_SIZE = 140 # characters, leaves buffer for protocol overhead + def __init__(self, bot): super().__init__(bot) + + def _truncate_for_lora(self, message: str, max_size: int = None) -> str: + """Truncate message to fit within LoRa size limits. + + Args: + message: The message to truncate + max_size: Maximum size (defaults to MAX_LORA_MESSAGE_SIZE) + + Returns: + Truncated message with indicator if truncated + """ + max_size = max_size or self.MAX_LORA_MESSAGE_SIZE + if len(message) <= max_size: + return message + + truncate_suffix = "...(use web viewer)" + available_size = max_size - len(truncate_suffix) + return message[:available_size] + truncate_suffix + + def _get_deprecation_warning(self, web_viewer_url: str = None) -> str: + """Get deprecation warning message for commands replaced by web viewer. + + Args: + web_viewer_url: Optional custom web viewer URL + + Returns: + Deprecation warning message + """ + if web_viewer_url: + return f"⚠️ DEPRECATED: Use web viewer at {web_viewer_url}/contacts" + return "⚠️ DEPRECATED: Use web viewer for this feature (check bot config for URL)" def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with 'repeater' keyword. @@ -150,93 +185,26 @@ class RepeaterCommand(BaseCommand): return True async def _handle_scan(self) -> str: - """Scan contacts for repeaters. - + """Scan contacts for repeaters (DEPRECATED - automatic in backend). + Triggers a scan of the device's contact list to identify and catalog repeaters. - + Returns: str: Result message describing the scan outcome. """ - self.logger.info("Repeater scan command received") - - if not hasattr(self.bot, 'repeater_manager'): - self.logger.error("Repeater manager not found on bot object") - return "Repeater manager not initialized. Please check bot configuration." - - self.logger.info("Repeater manager found, starting scan...") - - try: - cataloged_count = await self.bot.repeater_manager.scan_and_catalog_repeaters() - self.logger.info(f"Scan completed, cataloged {cataloged_count} repeaters") - - # Get more detailed information about what happened - if cataloged_count > 0: - return f"✅ Scanned contacts and cataloged {cataloged_count} new repeaters" - else: - # Check if there are any repeaters in the database - repeaters = await self.bot.repeater_manager.get_repeater_contacts(active_only=True) - if repeaters: - return f"✅ Scanned contacts - no new repeaters found, but updated location data for existing {len(repeaters)} repeaters" - else: - return "✅ Scanned contacts - no repeaters found in contact list" - except Exception as e: - self.logger.error(f"Error in repeater scan: {e}") - return f"❌ Error scanning for repeaters: {e}" + # Return deprecation warning + return self._get_deprecation_warning() + "\nScanning happens automatically in the backend." async def _handle_list(self, args: List[str]) -> str: - """List repeater contacts. - + """List repeater contacts (DEPRECATED - use web viewer or prefix command). + Args: - args: Command arguments (e.g., '--all' to show purged repeaters). - + args: Command arguments (ignored). + Returns: - str: Formatted list of repeaters. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - # Check for --all flag to show purged repeaters too - show_all = "--all" in args or "-a" in args - active_only = not show_all - - repeaters = await self.bot.repeater_manager.get_repeater_contacts(active_only=active_only) - - if not repeaters: - status = "all" if show_all else "active" - return f"No {status} repeaters found in database" - - # Format the output - lines = [] - lines.append(f"📡 **Repeater Contacts** ({'All' if show_all else 'Active'}):") - lines.append("") - - for repeater in repeaters: - status_icon = "🟢" if repeater['is_active'] else "🔴" - device_icon = "📡" if repeater['device_type'] == 'Repeater' else "🏠" - - last_seen = repeater['last_seen'] - if last_seen: - # Parse and format the timestamp - try: - from datetime import datetime - dt = datetime.fromisoformat(last_seen.replace('Z', '+00:00')) - last_seen_str = dt.strftime("%Y-%m-%d %H:%M") - except: - last_seen_str = last_seen - else: - last_seen_str = "Unknown" - - lines.append(f"{status_icon} {device_icon} **{repeater['name']}**") - lines.append(f" Type: {repeater['device_type']}") - lines.append(f" Last seen: {last_seen_str}") - lines.append(f" Purge count: {repeater['purge_count']}") - lines.append("") - - return "\n".join(lines) - - except Exception as e: - return f"❌ Error listing repeaters: {e}" + return self._get_deprecation_warning() + "\nUse 'prefix ' to find specific repeaters or web viewer to browse all." async def _handle_purge(self, args: List[str]) -> str: """Purge repeater or companion contacts. @@ -498,84 +466,23 @@ class RepeaterCommand(BaseCommand): return f"❌ Error purging companions: {e}" async def _handle_restore(self, args: List[str]) -> str: - """Restore purged repeater contacts. - + """Restore purged repeater contacts (DEPRECATED - use web viewer). + Args: args: Command arguments (name pattern to restore). - + Returns: - str: Result message describing the restore outcome. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - if not args: - return "Usage: !repeater restore [reason]\nExample: !repeater restore 'Hillcrest' 'Manual restore'" - - try: - name_pattern = args[0] - reason = " ".join(args[1:]) if len(args) > 1 else "Manual restore" - - # Find purged repeaters matching the name pattern - repeaters = await self.bot.repeater_manager.get_repeater_contacts(active_only=False) - matching_repeaters = [r for r in repeaters if not r['is_active'] and name_pattern.lower() in r['name'].lower()] - - if not matching_repeaters: - return f"❌ No purged repeaters found matching '{name_pattern}'" - - if len(matching_repeaters) == 1: - # Restore the single match - repeater = matching_repeaters[0] - success = await self.bot.repeater_manager.restore_repeater( - repeater['public_key'], reason - ) - if success: - return f"✅ Restored repeater: {repeater['name']}" - else: - return f"❌ Failed to restore repeater: {repeater['name']}" - else: - # Multiple matches - show options - lines = [f"Multiple purged repeaters found matching '{name_pattern}':"] - for i, repeater in enumerate(matching_repeaters, 1): - lines.append(f"{i}. {repeater['name']} ({repeater['device_type']})") - lines.append("") - lines.append("Please be more specific with the name.") - return "\n".join(lines) - - except Exception as e: - return f"❌ Error restoring repeaters: {e}" + return self._get_deprecation_warning() async def _handle_stats(self) -> str: - """Show repeater management statistics. - + """Show statistics (DEPRECATED - use web viewer). + Returns: - str: Formatted statistics summary. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - stats = await self.bot.repeater_manager.get_purging_stats() - - # Shortened for LoRa transmission - total = stats.get('total_repeaters', 0) - active = stats.get('active_repeaters', 0) - purged = stats.get('purged_repeaters', 0) - - response = f"📊 Stats: {total} total, {active} active, {purged} purged" - - recent_activity = stats.get('recent_activity_7_days', {}) - if recent_activity: - activity_summary = [] - for action, count in recent_activity.items(): - activity_summary.append(f"{action}:{count}") - if activity_summary: - response += f" | 7d: {', '.join(activity_summary)}" - - return response - - except Exception as e: - return f"❌ Error getting statistics: {e}" + return self._get_deprecation_warning() + "\nView detailed statistics in the web viewer." async def _handle_status(self) -> str: """Show contact list status and limits. @@ -611,128 +518,34 @@ class RepeaterCommand(BaseCommand): return f"❌ Error getting contact status: {e}" async def _handle_manage(self, args: List[str]) -> str: - """Manage contact list to prevent hitting limits. - + """Manage contact list (DEPRECATED - use web viewer). + Args: args: Command arguments (e.g., '--dry-run'). - + Returns: - str: Result message describing actions taken or proposed. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - # Check for --dry-run flag - dry_run = "--dry-run" in args or "-d" in args - auto_cleanup = not dry_run - - if dry_run: - # Just show what would be done - status = await self.bot.repeater_manager.get_contact_list_status() - if not status: - return "❌ Failed to get contact list status" - - lines = [] - lines.append("🔍 **Contact List Management (Dry Run)**") - lines.append("") - lines.append(f"📊 Current status: {status['current_contacts']}/{status['estimated_limit']} ({status['usage_percentage']:.1f}%)") - - if status['is_near_limit']: - lines.append("") - lines.append("⚠️ **Actions that would be taken:**") - if status['stale_contacts']: - lines.append(f" • Remove {min(10, len(status['stale_contacts']))} stale contacts") - if status['repeater_count'] > 0: - lines.append(" • Remove old repeaters (14+ days)") - if status['is_at_limit']: - lines.append(" • Aggressive cleanup (7+ day repeaters, 14+ day stale contacts)") - else: - lines.append("✅ No management actions needed") - - return "\n".join(lines) - else: - # Actually perform management - result = await self.bot.repeater_manager.manage_contact_list(auto_cleanup=True) - - if not result.get('success', False): - return f"❌ Contact list management failed: {result.get('error', 'Unknown error')}" - - lines = [] - lines.append("🔧 **Contact List Management Results**") - lines.append("") - - status = result['status'] - lines.append(f"📊 Final status: {status['current_contacts']}/{status['estimated_limit']} ({status['usage_percentage']:.1f}%)") - - actions = result.get('actions_taken', []) - if actions: - lines.append("") - lines.append("✅ **Actions taken:**") - for action in actions: - lines.append(f" • {action}") - else: - lines.append("") - lines.append("ℹ️ No actions were needed") - - return "\n".join(lines) - - except Exception as e: - return f"❌ Error managing contact list: {e}" + return self._get_deprecation_warning() async def _handle_add(self, args: List[str]) -> str: - """Add a discovered contact to the contact list. - + """Add a discovered contact (DEPRECATED - use web viewer). + Args: args: Command arguments (name, public_key, reason). - + Returns: - str: Result message indicating success or failure. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - if not args: - return "❌ Please specify a contact name to add" - - try: - contact_name = args[0] - public_key = args[1] if len(args) > 1 else None - reason = " ".join(args[2:]) if len(args) > 2 else "Manual addition" - - success = await self.bot.repeater_manager.add_discovered_contact( - contact_name, public_key, reason - ) - - if success: - return f"✅ Successfully added contact: {contact_name}" - else: - return f"❌ Failed to add contact: {contact_name}" - - except Exception as e: - return f"❌ Error adding contact: {e}" + return self._get_deprecation_warning() async def _handle_discover(self) -> str: - """Discover companion contacts. - - Triggers manual discovery of companion contacts. - + """Discover companion contacts (DEPRECATED - automatic in backend). + Returns: - str: Result message. + str: Deprecation warning. """ - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - success = await self.bot.repeater_manager.discover_companion_contacts("Manual discovery command") - - if success: - return "✅ Companion contact discovery initiated" - else: - return "❌ Failed to initiate companion contact discovery" - - except Exception as e: - return f"❌ Error discovering contacts: {e}" + return self._get_deprecation_warning() + "\nDiscovery happens automatically in the backend." async def _handle_contact_stats(self) -> str: """Show statistics about the complete repeater tracking database. @@ -905,371 +718,57 @@ class RepeaterCommand(BaseCommand): return f"❌ Error testing purge system: {e}" async def _handle_debug_purge(self) -> str: - """Debug the purge system to see what repeaters are available""" - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - # Get device contact info - total_contacts = len(self.bot.meshcore.contacts) - repeater_count = 0 - repeaters_info = [] - - for contact_key, contact_data in self.bot.meshcore.contacts.items(): - if self.bot.repeater_manager._is_repeater_device(contact_data): - repeater_count += 1 - name = contact_data.get('adv_name', contact_data.get('name', 'Unknown')) - device_type = 'Repeater' - if contact_data.get('type') == 3: - device_type = 'RoomServer' - - last_seen = contact_data.get('last_seen', contact_data.get('last_advert', 'Unknown')) - repeaters_info.append(f"• {name} ({device_type}) - Last seen: {last_seen}") - - # Shortened for LoRa transmission - response = f"🔍 Debug: {total_contacts} total, {repeater_count} repeaters" - - if repeaters_info: - # Show first 3 repeaters only - for info in repeaters_info[:3]: - response += f" | {info[:30]}..." - if len(repeaters_info) > 3: - response += f" | +{len(repeaters_info) - 3} more" - else: - response += " | ❌ No repeaters found" - - # Test the purge selection - test_repeaters = await self.bot.repeater_manager._get_repeaters_for_purging(3) - if test_repeaters: - response += f" | Test: {len(test_repeaters)} available" - else: - response += " | Test: ❌ None available" - - return response - - except Exception as e: - return f"❌ Error debugging purge system: {e}" + """Debug purge system (DEPRECATED - debug feature). + + Returns: + str: Deprecation warning. + """ + return "⚠️ DEPRECATED: Debug feature - check logs for purge system status." async def _handle_auto(self, args: List[str]) -> str: - """Toggle manual contact addition setting""" - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - if not args: - return "❌ Please specify 'on' or 'off' for manual contact addition setting" - - try: - setting = args[0].lower() - reason = " ".join(args[1:]) if len(args) > 1 else "Manual toggle" - - if setting in ['on', 'enable', 'true', '1']: - enabled = True - setting_text = "enabled" - elif setting in ['off', 'disable', 'false', '0']: - enabled = False - setting_text = "disabled" - else: - return "❌ Invalid setting. Use 'on' or 'off'" - - success = await self.bot.repeater_manager.toggle_auto_add(enabled, reason) - - if success: - return f"✅ Manual contact addition {setting_text}" - else: - return f"❌ Failed to {setting_text} manual contact addition" - - except Exception as e: - return f"❌ Error toggling manual contact addition: {e}" + """Toggle auto settings (DEPRECATED - use config file). + + Returns: + str: Deprecation warning. + """ + return "⚠️ DEPRECATED: Edit config file to change auto-management settings." async def _handle_test(self, args: List[str]) -> str: - """Test meshcore-cli command functionality""" - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - results = await self.bot.repeater_manager.test_meshcore_cli_commands() - - lines = [] - lines.append("🧪 **MeshCore-CLI Command Test Results**") - lines.append("") - - if 'error' in results: - lines.append(f"❌ **ERROR**: {results['error']}") - return "\n".join(lines) - - # Test results - help_status = "✅ PASS" if results.get('help', False) else "❌ FAIL" - remove_status = "✅ PASS" if results.get('remove_contact', False) else "❌ FAIL" - - lines.append(f"📋 Help command: {help_status}") - lines.append(f"🗑️ Remove contact command: {remove_status}") - lines.append("") - - if not results.get('remove_contact', False): - lines.append("⚠️ **WARNING**: remove_contact command not available!") - lines.append("This means repeater purging will not work properly.") - lines.append("Check your meshcore-cli installation and device connection.") - else: - lines.append("✅ All required commands are available.") - - return "\n".join(lines) - - except Exception as e: - return f"❌ Error testing meshcore-cli commands: {e}" + """Test commands (DEPRECATED - debug feature). + + Returns: + str: Deprecation warning. + """ + return "⚠️ DEPRECATED: Debug feature - check logs for system status." async def _handle_locations(self) -> str: - """Show location data status for repeaters""" - try: - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - # Get all active repeaters - repeaters = await self.bot.repeater_manager.get_repeater_contacts(active_only=True) - - if not repeaters: - return "No active repeaters found in database" - - # Analyze location data - total_repeaters = len(repeaters) - with_coordinates = 0 - with_city = 0 - with_state = 0 - with_country = 0 - no_location = 0 - - location_examples = [] - - for repeater in repeaters: - has_coords = repeater.get('latitude') is not None and repeater.get('longitude') is not None - has_city = bool(repeater.get('city')) - has_state = bool(repeater.get('state')) - has_country = bool(repeater.get('country')) - - if has_coords: - with_coordinates += 1 - if has_city: - with_city += 1 - if has_state: - with_state += 1 - if has_country: - with_country += 1 - if not (has_coords or has_city or has_state or has_country): - no_location += 1 - - # Collect examples for display - if len(location_examples) < 3: # Reduced to 3 examples to fit in message - location_parts = [] - if has_city: - location_parts.append(repeater['city']) - if has_state: - location_parts.append(repeater['state']) - if has_country: - location_parts.append(repeater['country']) - - if location_parts: - location_examples.append(f"• {repeater['name']}: {', '.join(location_parts)}") - elif has_coords: - location_examples.append(f"• {repeater['name']}: {repeater['latitude']:.4f}, {repeater['longitude']:.4f}") - else: - location_examples.append(f"• {repeater['name']}: No location data") - - # Build first message (summary) - summary_lines = [ - f"📍 Repeater Locations ({total_repeaters} total):", - f"GPS: {with_coordinates} ({with_coordinates/total_repeaters*100:.0f}%)", - f"City: {with_city} ({with_city/total_repeaters*100:.0f}%)", - f"State: {with_state} ({with_state/total_repeaters*100:.0f}%)", - f"Country: {with_country} ({with_country/total_repeaters*100:.0f}%)", - f"None: {no_location} ({no_location/total_repeaters*100:.0f}%)" - ] - - first_message = "\n".join(summary_lines) - - # Build second message (examples) if we have examples - if location_examples: - example_lines = ["Examples:"] - example_lines.extend(location_examples) - second_message = "\n".join(example_lines) - - # Return tuple for multi-message response - return ("multi_message", first_message, second_message) - else: - return first_message - - except Exception as e: - self.logger.error(f"Error getting repeater location status: {e}") - return f"❌ Error getting location status: {e}" + """Show location data (DEPRECATED - use web viewer map). + + Returns: + str: Deprecation warning. + """ + return self._get_deprecation_warning() + "\nView locations on the interactive map in the web viewer." async def _handle_update_geolocation(self, dry_run: bool = False, batch_size: int = 10) -> str: - """Update missing geolocation data for repeaters""" - try: - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - self.logger.info(f"Starting geolocation update process (dry_run={dry_run})") - - # Call the repeater manager method - result = await self.bot.repeater_manager.populate_missing_geolocation_data(dry_run=dry_run, batch_size=batch_size) - - if 'error' in result: - return f"❌ Error updating geolocation data: {result['error']}" - - # Build response message - action = "Would update" if dry_run else "Updated" - response_lines = [ - f"🌍 Geolocation Update {'(Dry Run)' if dry_run else ''}", - f"Batch size: {batch_size}", - f"Found: {result['total_found']} repeaters with missing data", - f"{action}: {result['updated']} repeaters", - f"Errors: {result['errors']}", - f"Skipped: {result['skipped']}" - ] - - if result['total_found'] == 0: - response_lines.append("✅ All repeaters already have complete geolocation data!") - elif result['updated'] > 0: - if dry_run: - response_lines.append("💡 Run without 'dry-run' to apply these updates") - else: - response_lines.append("✅ Geolocation data updated successfully!") - - return "\n".join(response_lines) - - except Exception as e: - self.logger.error(f"Error updating geolocation data: {e}") - return f"❌ Error updating geolocation data: {e}" + """Update geolocation data (DEPRECATED - automatic in backend). + + Returns: + str: Deprecation warning. + """ + return self._get_deprecation_warning() + "\nGeocoding happens automatically in the backend." def get_help(self) -> str: - """Get help text for the repeater command""" - return """📡 **Repeater & Contact Management Commands** - -**Usage:** `!repeater [options]` - -**Repeater Management:** -• `scan` - Scan current contacts and catalog new repeaters -• `list` - List repeater contacts (use `--all` to show purged ones) -• `locations` - Show location data status for repeaters -• `update-geo` - Update missing geolocation data (state/country) from coordinates -• `update-geo dry-run` - Preview what would be updated without making changes -• `update-geo 5` - Update up to 5 repeaters (default: 10) -• `update-geo dry-run 3` - Preview updates for up to 3 repeaters - • `purge all` - Purge all repeaters - • `purge all force` - Force purge all repeaters (uses multiple removal methods) - • `purge ` - Purge repeaters older than specified days - • `purge ` - Purge specific repeater by name -• `restore ` - Restore a previously purged repeater -• `stats` - Show repeater management statistics - -**Contact List Management:** -• `status` - Show contact list status and limits -• `manage` - Manage contact list to prevent hitting limits -• `manage --dry-run` - Show what management actions would be taken -• `add [key]` - Add a discovered contact to contact list -• `auto-purge` - Show auto-purge status and controls -• `auto-purge trigger` - Manually trigger auto-purge -• `auto-purge enable/disable` - Enable/disable auto-purge -• `purge-status` - Show detailed purge status and recommendations -• `test-purge` - Test the improved purge system - • `discover` - Discover companion contacts - • `auto ` - Toggle manual contact addition setting - • `test` - Test meshcore-cli command functionality - -**Examples:** -• `!repeater scan` - Find and catalog new repeaters -• `!repeater status` - Check contact list capacity -• `!repeater manage` - Auto-manage contact list -• `!repeater manage --dry-run` - Preview management actions -• `!repeater add "John"` - Add contact named John -• `!repeater discover` - Discover new companion contacts -• `!repeater auto-purge` - Check auto-purge status -• `!repeater auto-purge trigger` - Manually trigger auto-purge -• `!repeater purge-status` - Detailed purge status - • `!repeater auto off` - Disable manual contact addition - • `!repeater test` - Test meshcore-cli commands - • `!repeater purge all` - Purge all repeaters - • `!repeater purge all force` - Force purge all repeaters - • `!repeater purge 30` - Purge repeaters older than 30 days -• `!repeater stats` - Show management statistics -• `!repeater geocode` - Show geocoding status -• `!repeater geocode trigger` - Manually trigger geocoding - -**Note:** This system helps manage both repeater contacts and overall contact list capacity. It automatically removes stale contacts and old repeaters when approaching device limits. - - **Automatic Features:** - • NEW_CONTACT events are automatically monitored - • Repeaters are automatically cataloged when discovered - • Contact list capacity is monitored in real-time - • `auto_manage_contacts = device`: Device handles auto-addition, bot manages capacity - • `auto_manage_contacts = bot`: Bot automatically adds companion contacts and manages capacity - • `auto_manage_contacts = false`: Manual mode - use !repeater commands to manage contacts""" + """Get help text for the repeater command (essential commands only)""" + # Ultra-compact help for 150 char DM limit + return "status|purge all|purge companions [days]|auto-purge\n⚠️ Use web viewer or 'prefix' cmd to browse" async def _handle_geocode(self, args: List[str]) -> str: - """Handle geocoding commands""" - if not hasattr(self.bot, 'repeater_manager'): - return "Repeater manager not initialized. Please check bot configuration." - - try: - if not args: - # Show geocoding status - status = await self._get_geocoding_status() - return status - elif args[0] == "trigger": - # Manually trigger background geocoding (single contact) - await self.bot.repeater_manager._background_geocoding() - return "🌍 Background geocoding triggered (1 contact)" - elif args[0] == "bulk": - # Trigger bulk geocoding for multiple contacts - batch_size = 10 - if len(args) > 1 and args[1].isdigit(): - batch_size = int(args[1]) - batch_size = min(batch_size, 50) # Cap at 50 for safety - - result = await self.bot.repeater_manager.populate_missing_geolocation_data( - dry_run=False, - batch_size=batch_size - ) - - if 'error' in result: - return f"❌ Bulk geocoding error: {result['error']}" - - return (f"🌍 Bulk geocoding completed:\n" - f"Found: {result['total_found']} contacts\n" - f"Updated: {result['updated']} contacts\n" - f"Errors: {result['errors']}\n" - f"Skipped: {result['skipped']}") - elif args[0] == "dry-run": - # Test bulk geocoding without making changes - batch_size = 10 - if len(args) > 1 and args[1].isdigit(): - batch_size = int(args[1]) - batch_size = min(batch_size, 50) - - result = await self.bot.repeater_manager.populate_missing_geolocation_data( - dry_run=True, - batch_size=batch_size - ) - - if 'error' in result: - return f"❌ Dry run error: {result['error']}" - - return (f"🌍 Dry run results:\n" - f"Would update: {result['updated']} contacts\n" - f"Found: {result['total_found']} contacts\n" - f"Errors: {result['errors']}\n" - f"Skipped: {result['skipped']}\n" - f"💡 Use '!repeater geocode bulk' to apply changes") - elif args[0] == "status": - # Show detailed geocoding status - return await self._get_geocoding_status() - else: - return ("Usage: !repeater geocode [trigger|bulk|dry-run|status]\n" - " trigger - Geocode 1 contact\n" - " bulk [N] - Geocode up to N contacts (default: 10, max: 50)\n" - " dry-run [N] - Test geocoding without changes\n" - " status - Show geocoding status") - except Exception as e: - self.logger.error(f"Error in geocoding command: {e}") - return f"❌ Geocoding error: {e}" + """Handle geocoding (DEPRECATED - automatic in backend). + + Returns: + str: Deprecation warning. + """ + return self._get_deprecation_warning() + "\nGeocoding happens automatically in the backend." async def _get_geocoding_status(self) -> str: """Get geocoding status""" diff --git a/modules/message_handler.py b/modules/message_handler.py index 269fc04..0682aa6 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -8,6 +8,7 @@ import asyncio import time import json import re +import copy from typing import List, Optional, Dict, Any from meshcore import EventType @@ -69,7 +70,12 @@ class MessageHandler: metadata: Optional metadata dictionary associated with the event. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + import copy + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("Contact message event has no payload") + return # Debug: Log the full payload structure self.logger.debug(f"Contact message payload: {payload}") @@ -362,7 +368,13 @@ class MessageHandler: metadata: Optional metadata dictionary. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + # Make a deep copy to ensure we have all the data we need + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("RAW_DATA event has no payload") + return + self.logger.info(f"📦 RAW_DATA EVENT RECEIVED: {payload}") self.logger.info(f"📦 Event type: {type(event)}") self.logger.info(f"📦 Metadata: {metadata}") @@ -559,7 +571,12 @@ class MessageHandler: metadata: Optional metadata dictionary. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + import copy + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("RF log data event has no payload") + return # Extract SNR from payload if 'snr' in payload: @@ -1318,7 +1335,13 @@ class MessageHandler: async def handle_channel_message(self, event, metadata=None): """Handle incoming channel message""" try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + import copy + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("Channel message event has no payload") + return + channel_idx = payload.get('channel_idx', 0) # Debug: Log the full payload structure @@ -1908,17 +1931,22 @@ class MessageHandler: async def handle_new_contact(self, event, metadata=None): """Handle NEW_CONTACT events for automatic contact management""" try: - self.logger.info(f"🔍 NEW_CONTACT EVENT RECEIVED: {event}") - self.logger.info(f"📦 Event type: {type(event)}") - self.logger.info(f"📦 Event payload: {event.payload if hasattr(event, 'payload') else 'No payload'}") - - # Extract contact information from the event - contact_data = event.payload if hasattr(event, 'payload') else event + # Copy payload immediately to avoid segfault if event is freed + # Make a deep copy to ensure we have all the data we need + if hasattr(event, 'payload'): + contact_data = copy.deepcopy(event.payload) + else: + # Fallback: try to copy the event itself if it's a dict-like object + contact_data = copy.deepcopy(event) if isinstance(event, dict) else None if not contact_data: self.logger.warning("NEW_CONTACT event has no payload data") return + self.logger.info(f"🔍 NEW_CONTACT EVENT RECEIVED: {event}") + self.logger.info(f"📦 Event type: {type(event)}") + self.logger.info(f"📦 Event payload: {contact_data}") + # Get contact details contact_name = contact_data.get('name', contact_data.get('adv_name', 'Unknown')) public_key = contact_data.get('public_key', '') diff --git a/modules/service_plugins/discord_bridge_service.py b/modules/service_plugins/discord_bridge_service.py index a23a1aa..3c5737c 100644 --- a/modules/service_plugins/discord_bridge_service.py +++ b/modules/service_plugins/discord_bridge_service.py @@ -7,6 +7,7 @@ Posts MeshCore channel messages to Discord via webhooks (one-way, read-only) import asyncio import logging import time +import copy from typing import Dict, Optional, Any from datetime import datetime @@ -268,7 +269,11 @@ class DiscordBridgeService(BaseServicePlugin): metadata: Optional metadata dictionary associated with the event. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("Channel message event has no payload") + return # Extract channel index and convert to channel name channel_idx = payload.get('channel_idx', 0) diff --git a/modules/service_plugins/map_uploader_service.py b/modules/service_plugins/map_uploader_service.py index 5ce5431..184fc76 100644 --- a/modules/service_plugins/map_uploader_service.py +++ b/modules/service_plugins/map_uploader_service.py @@ -10,6 +10,7 @@ import json import logging import hashlib import time +import copy from typing import Optional, Dict, Any # Import meshcore @@ -471,7 +472,11 @@ class MapUploaderService(BaseServicePlugin): metadata: Optional metadata for the event. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("RX log data event has no payload") + return # Get raw packet data raw_hex = None diff --git a/modules/service_plugins/packet_capture_service.py b/modules/service_plugins/packet_capture_service.py index 57fee62..fe32659 100644 --- a/modules/service_plugins/packet_capture_service.py +++ b/modules/service_plugins/packet_capture_service.py @@ -12,6 +12,7 @@ import hashlib import time import re import os +import copy from datetime import datetime from pathlib import Path from typing import Optional, Dict, Any, List @@ -449,7 +450,11 @@ class PacketCaptureService(BaseServicePlugin): metadata: Optional metadata dictionary. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("RX log data event has no payload") + return if 'snr' in payload: # Try to get packet data - prefer 'payload' field, fallback to 'raw_hex' @@ -483,7 +488,11 @@ class PacketCaptureService(BaseServicePlugin): metadata: Optional metadata dictionary. """ try: - payload = event.payload + # Copy payload immediately to avoid segfault if event is freed + payload = copy.deepcopy(event.payload) if hasattr(event, 'payload') else None + if payload is None: + self.logger.warning("Raw data event has no payload") + return raw_data = payload.get('data', '') if not raw_data: