mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 11:57:16 +00:00
refactor: restore behavior the branch had changed (codex review)
A codex review of the branch found seven places where earlier refactor commits changed behavior. Each is restored here with a test: - base_service imported flood_scope relatively; the local-service loader also runs that file as local_services.base_service, where the import cannot resolve. Now absolute. - Radio-health fields were initialized after commands and services were built, so a plugin constructor reading is_radio_offline raised. They are now set before any plugin. - Removing the per-command can_execute overrides made 31 commands honor skip_channel_check, which their overrides had dropped. BaseCommand.honors_skip_channel_check (False on exactly those commands) keeps them enforcing their channel list. - CommandManager.get_plugin_metadata, WxCommand.get_weather_for_zipcode and PluginLoader.validate_plugin were public compatibility entry points; they are back. - feed_manager stopped exporting DEFAULT_MAX_FEED_RESPONSE_BYTES and DEFAULT_MAX_PARSED_FEED_ITEMS despite the commit saying it re-exported them; it does now. - wx and gwx logged companion-location lookup failures at WARNING; the shared helper logged at debug. It now takes the level. - The INFO 'MESSAGE ROUTING' / 'DIRECT MESSAGE' lines for pubkey-correlated DMs were removed with the dead path_info computation; they are logged again by a log-only helper.
This commit is contained in:
1 parent
adedfa6c9e
commit
efe074b151
47 files changed
+277
-17
No files matched your search
@@ -2384,3 +2384,7 @@ class CommandManager:
|
||||
"""Reload a specific plugin"""
|
||||
return self.plugin_loader.reload_plugin(plugin_name)
|
||||
|
||||
def get_plugin_metadata(self, plugin_name: str | None = None) -> dict[str, Any]:
|
||||
"""Get plugin metadata"""
|
||||
return self.plugin_loader.get_plugin_metadata(plugin_name)
|
||||
|
||||
@@ -55,6 +55,7 @@ class AirplanesCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "airplanes"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "airplanes_enabled"
|
||||
keywords = ['airplanes', 'aircraft', 'planes', 'adsb', 'overhead']
|
||||
description = "Get aircraft overhead (usage: airplanes [location] [options] or overhead [lat,lon])"
|
||||
|
||||
@@ -176,6 +176,7 @@ class AlertCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "alert"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "alert_enabled"
|
||||
keywords = ['alert', 'alerts', 'incident', 'incidents']
|
||||
description = "Get active emergency incidents (usage: alert seattle, alert 98258, alert 178th seattle, alert seattle all)"
|
||||
|
||||
@@ -141,7 +141,7 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
|
||||
def _get_companion_location(self, message: MeshMessage) -> Optional[tuple[float, float]]:
|
||||
"""Get companion/sender location from the contact-tracking database."""
|
||||
return get_companion_lat_lon(self.bot, message, self.logger)
|
||||
return get_companion_lat_lon(self.bot, message, self.logger, error_level="warning")
|
||||
|
||||
def _get_bot_location(self) -> Optional[tuple[float, float]]:
|
||||
"""Get bot location from config ([Bot] bot_latitude, bot_longitude)."""
|
||||
|
||||
@@ -42,6 +42,7 @@ class AqiCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "aqi"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "aqi_enabled"
|
||||
keywords = ['aqi', 'air', 'airquality', 'air_quality']
|
||||
description = "Get Air Quality Index for a location (usage: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3, or aqi help)"
|
||||
|
||||
@@ -23,6 +23,7 @@ class AuroraCommand(BaseCommand):
|
||||
render_safe = True
|
||||
|
||||
name = "aurora"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "aurora_enabled"
|
||||
keywords = ["aurora", "kp"]
|
||||
description = "Get aurora forecast (KP index and probability) for a location"
|
||||
|
||||
@@ -85,6 +85,12 @@ class BaseCommand(ABC):
|
||||
# lists. requires_admin_access() is still the method callers ask.
|
||||
admin_only: bool = False
|
||||
|
||||
# Whether can_execute honors skip_channel_check. Commands whose own override
|
||||
# used to drop the argument (it called super().can_execute(message)) set this
|
||||
# to False so they keep enforcing their channel list even when a delegating
|
||||
# caller asks to skip it. Fixing that is a behavior change, kept separate.
|
||||
honors_skip_channel_check: bool = True
|
||||
|
||||
# Documentation fields - to be overridden by subclasses for website generation
|
||||
short_description: str = "" # Brief description for website (without usage syntax)
|
||||
usage: str = "" # Usage syntax, e.g., "wx <zipcode|city> [tomorrow|7d|hourly|alerts]"
|
||||
@@ -616,6 +622,9 @@ class BaseCommand(ABC):
|
||||
if self.enabled_attr is not None and not getattr(self, self.enabled_attr):
|
||||
return False
|
||||
|
||||
if not self.honors_skip_channel_check:
|
||||
skip_channel_check = False
|
||||
|
||||
# Check channel access (standardized channel override)
|
||||
if not skip_channel_check and not self.is_channel_allowed(message):
|
||||
return False
|
||||
|
||||
@@ -21,6 +21,7 @@ class CatfactCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "catfact"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "catfact_enabled"
|
||||
keywords = ['catfact', 'cat', 'meow', 'purr', 'kitten']
|
||||
description = "Get a random cat fact (hidden command)"
|
||||
|
||||
@@ -21,6 +21,7 @@ class ChannelsCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "channels"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "channels_enabled"
|
||||
keywords = ['channels', 'channel']
|
||||
description = "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels <category>' for specific categories, 'channels #channel' for specific channel info."
|
||||
|
||||
@@ -15,6 +15,7 @@ class CmdCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "cmd"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "cmd_enabled"
|
||||
keywords = ['cmd', 'cmds', 'command', 'commands']
|
||||
description = "Lists available commands in compact format"
|
||||
|
||||
@@ -17,6 +17,7 @@ class DiceCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "dice"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "dice_enabled"
|
||||
keywords = ['dice']
|
||||
description = "Roll dice for D&D and tabletop games. Use 'dice' for d6, 'dice d20' for d20, 'dice 2d6' for 2d6, 'dice d10 d6' for mixed dice, 'dice decade' for decade die (00-90), etc."
|
||||
|
||||
@@ -17,6 +17,7 @@ class FeedCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "feed"
|
||||
honors_skip_channel_check = False
|
||||
admin_only = True
|
||||
enabled_attr = "feed_enabled"
|
||||
keywords = ['feed', 'feeds', 'rss', 'subscription', 'subscriptions']
|
||||
|
||||
@@ -20,6 +20,7 @@ class HelloCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "hello"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "hello_enabled"
|
||||
keywords = ['hello', 'hi', 'hey', 'howdy', 'greetings', 'salutations', 'good morning', 'good afternoon', 'good evening', 'good night', 'yo', 'sup', 'whats up', 'what\'s up', 'morning', 'afternoon', 'evening', 'night', 'gday', 'g\'day', 'hola', 'bonjour', 'ciao', 'namaste', 'aloha', 'shalom', 'konnichiwa', 'guten tag', 'buenos dias', 'buenas tardes', 'buenas noches']
|
||||
description = "Responds to greetings with robot-themed responses"
|
||||
|
||||
@@ -21,6 +21,7 @@ class HelpCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "help"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "help_enabled"
|
||||
keywords = ['help']
|
||||
description = "Shows commands. Use 'help <command>' for details."
|
||||
|
||||
@@ -19,6 +19,7 @@ class HfcondCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "hfcond"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "hfcond_enabled"
|
||||
keywords = ['hfcond']
|
||||
description = "Get HF band conditions for ham radio"
|
||||
|
||||
@@ -26,6 +26,7 @@ class Magic8Command(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "magic8"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "magic8_enabled"
|
||||
keywords = ['magic8']
|
||||
description = "Emulates the classic Magic 8-ball toy'"
|
||||
|
||||
@@ -15,6 +15,7 @@ class MoonCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "moon"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "moon_enabled"
|
||||
keywords = ['moon']
|
||||
description = "Get moon phase, rise/set times and position"
|
||||
|
||||
@@ -462,6 +462,7 @@ class MultitestCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "multitest"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "multitest_enabled"
|
||||
keywords = ['multitest', 'mt']
|
||||
description = "Listens for 6 seconds and collects all unique paths from incoming messages"
|
||||
|
||||
@@ -29,6 +29,7 @@ class PathCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "path"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "path_enabled"
|
||||
keywords = ["path", "decode", "route"]
|
||||
description = "Decode hex path data to show which repeaters were involved in message routing"
|
||||
|
||||
@@ -19,6 +19,7 @@ class PingCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "ping"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "ping_enabled"
|
||||
keywords = ['ping']
|
||||
description = "Responds to 'ping' with 'Pong!'"
|
||||
|
||||
@@ -26,6 +26,7 @@ class PrefixCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "prefix"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "prefix_enabled"
|
||||
keywords = ['prefix', 'lookup']
|
||||
description = "Look up repeaters by prefix (2, 4, or 6 hex chars = 1–3 bytes; longer input truncated)"
|
||||
|
||||
@@ -727,6 +727,7 @@ class RainCommand(BaseCommand):
|
||||
render_safe = True
|
||||
|
||||
name = "rain"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "rain_enabled"
|
||||
keywords = ["rain", "nowcast", "snow"]
|
||||
description = "Rain/snow nowcast: when precip starts or stops in the next ~2h, with amount"
|
||||
|
||||
@@ -13,6 +13,7 @@ class ReloadCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "reload"
|
||||
honors_skip_channel_check = False
|
||||
admin_only = True
|
||||
keywords = ["reload", "reloadconfig", "configreload"]
|
||||
description = "Reload bot configuration without restart (DM only, admin only)"
|
||||
|
||||
@@ -19,6 +19,7 @@ class RepeaterCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "repeater"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "repeater_enabled"
|
||||
keywords = ["repeater", "repeaters", "rp"]
|
||||
description = "Manage repeater contacts and purging operations (DM only)"
|
||||
|
||||
@@ -22,6 +22,7 @@ class RollCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "roll"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "roll_enabled"
|
||||
keywords = ['roll']
|
||||
description = "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, 'roll 50' for 1-50, etc."
|
||||
|
||||
@@ -15,6 +15,7 @@ class SatpassCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "satpass"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "satpass_enabled"
|
||||
keywords = ['satpass']
|
||||
description = "Get satellite pass info: satpass <NORAD_number_or_shortcut> [visual]"
|
||||
|
||||
@@ -19,6 +19,7 @@ class SolarCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "solar"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "solar_enabled"
|
||||
keywords = ['solar']
|
||||
description = "Get current solar conditions and HF band info"
|
||||
|
||||
@@ -31,6 +31,7 @@ class SolarforecastCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "solarforecast"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "solarforecast_enabled"
|
||||
keywords = ['solarforecast', 'sf']
|
||||
description = "Get solar panel production forecast (usage: sf <location|repeater_name|coordinates|zipcode> [panel_size] [azimuth, 0=south] [angle])"
|
||||
|
||||
@@ -40,6 +40,7 @@ class SportsCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "sports"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "sports_enabled"
|
||||
keywords = ['sports', 'score', 'scores']
|
||||
description = "Get sports scores and schedules (usage: sports [team/league])"
|
||||
|
||||
@@ -19,6 +19,7 @@ class SunCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "sun"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "sun_enabled"
|
||||
keywords = ['sun']
|
||||
description = "Get sunrise/sunset times"
|
||||
|
||||
@@ -28,6 +28,7 @@ class TestCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "test"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "test_enabled"
|
||||
keywords = ['test', 't']
|
||||
description = "Responds to 'test' or 't' with connection info"
|
||||
|
||||
@@ -18,6 +18,7 @@ class TraceCommand(BaseCommand):
|
||||
"""Trace (manual path) and Tracer (reciprocal path) for link diagnostics."""
|
||||
|
||||
name = "trace"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "trace_enabled"
|
||||
keywords = ["trace", "tracer"]
|
||||
description = "Run a trace along a path (trace=manual if path given, else round-trip; tracer=always round-trip)"
|
||||
|
||||
@@ -13,6 +13,7 @@ class WebViewerCommand(BaseCommand):
|
||||
|
||||
# Plugin metadata
|
||||
name = "webviewer"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "webviewer_enabled"
|
||||
keywords = ["webviewer", "web", "viewer", "wv"]
|
||||
description = "Manage web viewer integration (DM only)"
|
||||
|
||||
@@ -38,6 +38,7 @@ class WorldCupCommand(BaseCommand):
|
||||
# Read-only informational output; safe for scheduled {cmd:...} rendering.
|
||||
render_safe = True
|
||||
name = "worldcup"
|
||||
honors_skip_channel_check = False
|
||||
enabled_attr = "worldcup_enabled"
|
||||
keywords = ["wc", "worldcup"]
|
||||
description = "FIFA World Cup scores, standings, and nation results (in-season only)"
|
||||
|
||||
@@ -226,7 +226,7 @@ class WxCommand(WeatherCommandMixin, BaseCommand):
|
||||
|
||||
def _get_companion_location(self, message: MeshMessage) -> Optional[tuple[float, float]]:
|
||||
"""Get companion/sender location from the contact-tracking database."""
|
||||
return get_companion_lat_lon(self.bot, message, self.logger)
|
||||
return get_companion_lat_lon(self.bot, message, self.logger, error_level="warning")
|
||||
|
||||
def _get_bot_location(self) -> Optional[tuple[float, float]]:
|
||||
"""Get bot location from config ([Bot] bot_latitude, bot_longitude)."""
|
||||
@@ -3143,3 +3143,7 @@ class WxCommand(WeatherCommandMixin, BaseCommand):
|
||||
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
|
||||
|
||||
return line
|
||||
|
||||
async def get_weather_for_zipcode(self, zipcode: str) -> str:
|
||||
"""Get weather data for a specific zipcode (legacy method)"""
|
||||
return await self.get_weather_for_location(zipcode, "zipcode")
|
||||
+12
-10
@@ -205,6 +205,18 @@ class MeshCoreBot:
|
||||
self.channel_rate_limiter = self._load_channel_rate_limiter()
|
||||
self.tx_delay_ms = self.config.getint('Bot', 'tx_delay_ms', fallback=250)
|
||||
|
||||
# Radio health, set before any command or service plugin is built, since
|
||||
# their constructors may read is_radio_offline / is_radio_zombie. Zombie: the firmware stopped acting on commands and only a
|
||||
# power cycle recovers it. Offline: repeated send timeouts. The probe
|
||||
# timestamp starts on the first health-loop pass, hence None until then.
|
||||
self._radio_zombie_detected = False
|
||||
self._radio_fail_count = 0
|
||||
self._tcp_probe_fail_count = 0
|
||||
self._radio_offline = False
|
||||
self._send_consecutive_failures = 0
|
||||
self._last_radio_probe: float | None = None
|
||||
self._last_health_update = 0.0
|
||||
|
||||
# Initialize translator for localization BEFORE CommandManager
|
||||
# This ensures translated keywords are available when commands are loaded
|
||||
try:
|
||||
@@ -363,16 +375,6 @@ class MeshCoreBot:
|
||||
# Web-viewer reboot/reconnect ops in flight (a count, since they can overlap)
|
||||
self._radio_relinks_in_progress = 0
|
||||
|
||||
# Radio health. Zombie: the firmware stopped acting on commands and only a
|
||||
# power cycle recovers it. Offline: repeated send timeouts. The probe
|
||||
# timestamp starts on the first health-loop pass, hence None until then.
|
||||
self._radio_zombie_detected = False
|
||||
self._radio_fail_count = 0
|
||||
self._tcp_probe_fail_count = 0
|
||||
self._radio_offline = False
|
||||
self._send_consecutive_failures = 0
|
||||
self._last_radio_probe: float | None = None
|
||||
self._last_health_update = 0.0
|
||||
|
||||
# Serialize host->radio commands: one companion frame in flight at a
|
||||
# time, with a minimum inter-command gap so the firmware's single
|
||||
|
||||
@@ -32,7 +32,9 @@ from modules.feed_format import (
|
||||
sort_feed_items,
|
||||
truncate_to_budget,
|
||||
)
|
||||
from modules.feed_parse import (
|
||||
from modules.feed_parse import ( # noqa: F401 - the DEFAULT_* names are re-exported
|
||||
DEFAULT_MAX_FEED_RESPONSE_BYTES,
|
||||
DEFAULT_MAX_PARSED_FEED_ITEMS,
|
||||
api_item_fields,
|
||||
feed_allow_private_urls,
|
||||
feed_max_parsed_items,
|
||||
|
||||
+8
-3
@@ -628,8 +628,13 @@ def get_config_default_lat_lon(bot: Any, section: str) -> Optional[tuple[float,
|
||||
return None
|
||||
|
||||
|
||||
def get_companion_lat_lon(bot: Any, message: Any, logger: Any = None) -> Optional[tuple[float, float]]:
|
||||
"""The sender's most recent advertised position from contact tracking, or None."""
|
||||
def get_companion_lat_lon(
|
||||
bot: Any, message: Any, logger: Any = None, error_level: str = "debug"
|
||||
) -> Optional[tuple[float, float]]:
|
||||
"""The sender's most recent advertised position from contact tracking, or None.
|
||||
|
||||
A lookup error is logged on ``logger`` at ``error_level`` and returns None.
|
||||
"""
|
||||
try:
|
||||
sender_pubkey = getattr(message, "sender_pubkey", None)
|
||||
if not sender_pubkey or not hasattr(bot, "db_manager"):
|
||||
@@ -649,7 +654,7 @@ def get_companion_lat_lon(bot: Any, message: Any, logger: Any = None) -> Optiona
|
||||
return (float(row["latitude"]), float(row["longitude"]))
|
||||
except Exception as e:
|
||||
if logger is not None:
|
||||
logger.debug(f"Error getting companion location: {e}")
|
||||
getattr(logger, error_level)(f"Error getting companion location: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -404,6 +404,40 @@ class MessageHandler:
|
||||
integration = getattr(self.bot, "web_viewer_integration", None)
|
||||
return integration.bot_integration if integration else None
|
||||
|
||||
def _log_dm_routing_from_rf(self, message_pubkey: str) -> None:
|
||||
"""Log the route of a DM's correlated RF packet (INFO), when one is found.
|
||||
|
||||
Only for DMs without raw_hex, correlated by sender pubkey. This is
|
||||
logging only: the route actually attached to the message is decided
|
||||
later in handle_contact_message.
|
||||
"""
|
||||
recent_rf_data = self.find_recent_rf_data(message_pubkey)
|
||||
if not (recent_rf_data and recent_rf_data.get("raw_hex")):
|
||||
return
|
||||
decoded_packet = self.decode_meshcore_packet(recent_rf_data["raw_hex"], recent_rf_data.get("payload"))
|
||||
if not decoded_packet:
|
||||
return
|
||||
self.logger.debug(f"Decoded packet for routing from RF data: {decoded_packet}")
|
||||
if not recent_rf_data.get("routing_info"):
|
||||
return
|
||||
if not rf_data_is_correlated(recent_rf_data):
|
||||
self.logger.debug("Ignoring routing info from an uncorrelated fallback packet")
|
||||
return
|
||||
routing_info = recent_rf_data["routing_info"]
|
||||
self.logger.debug(f"Found routing info: {routing_info}")
|
||||
path_len = routing_info.get("path_length", 0)
|
||||
if path_len > 0:
|
||||
path_hex = routing_info.get("path_hex", "")
|
||||
path_nodes = routing_info.get("path_nodes", [])
|
||||
route_type = routing_info.get("route_type", "Unknown")
|
||||
if path_nodes:
|
||||
path_info = f"{','.join(path_nodes)} ({path_len} hops via {route_type})"
|
||||
else:
|
||||
path_info = f"Path: {path_hex} ({path_len} hops via {route_type})"
|
||||
self.logger.info(f"🛣️ MESSAGE ROUTING: {path_info}")
|
||||
else:
|
||||
self.logger.info(f"📡 DIRECT MESSAGE: Direct via {routing_info.get('route_type', 'Unknown')}")
|
||||
|
||||
async def handle_contact_message(self, event: Any, metadata: dict[str, Any] | None = None) -> None:
|
||||
"""Handle incoming contact message (DM).
|
||||
|
||||
@@ -436,6 +470,8 @@ class MessageHandler:
|
||||
message_raw_hex = payload.get("raw_hex", "")
|
||||
message_packet_prefix = message_raw_hex[:32] if message_raw_hex else None
|
||||
message_pubkey = payload.get("pubkey_prefix", "") # Keep for contact lookup
|
||||
if not message_packet_prefix and message_pubkey:
|
||||
self._log_dm_routing_from_rf(message_pubkey)
|
||||
|
||||
# Get additional metadata - try multiple sources for SNR and RSSI
|
||||
snr: float | None = None
|
||||
|
||||
@@ -539,3 +539,28 @@ class PluginLoader:
|
||||
self.logger.error(f"Error reloading plugin {plugin_name}: {e}")
|
||||
return False
|
||||
|
||||
def validate_plugin(self, plugin_instance: BaseCommand) -> list[str]:
|
||||
"""Validate a plugin instance and return any issues"""
|
||||
issues = []
|
||||
metadata = plugin_instance.get_metadata()
|
||||
|
||||
# Check required metadata
|
||||
if not metadata.get('name'):
|
||||
issues.append("Plugin missing 'name' metadata")
|
||||
|
||||
if not metadata.get('description'):
|
||||
issues.append("Plugin missing 'description' metadata")
|
||||
|
||||
# Check if execute method is implemented
|
||||
if not hasattr(plugin_instance, 'execute'):
|
||||
issues.append("Plugin missing 'execute' method")
|
||||
|
||||
# Check for keyword conflicts
|
||||
for keyword in metadata.get('keywords', []):
|
||||
if keyword.lower() in self.keyword_mappings:
|
||||
existing_plugin = self.keyword_mappings[keyword.lower()]
|
||||
if existing_plugin != metadata['name']:
|
||||
issues.append(f"Keyword '{keyword}' conflicts with plugin '{existing_plugin}'")
|
||||
|
||||
return issues
|
||||
|
||||
@@ -10,7 +10,9 @@ from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..flood_scope import section_flood_scope
|
||||
# Absolute on purpose: the local-service loader also executes this file as
|
||||
# local_services.base_service, where a relative parent import cannot resolve.
|
||||
from modules.flood_scope import section_flood_scope
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in new issue
Block a user