mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-07-29 06:49:29 +00:00
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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 <name>' 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 <name_pattern> [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 <subcommand> [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 <days>` - Purge repeaters older than specified days
|
||||
• `purge <name>` - Purge specific repeater by name
|
||||
• `restore <name>` - 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 <name> [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 <on|off>` - 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"""
|
||||
|
||||
+38
-10
@@ -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', '')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user