mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-30 17:42:50 +00:00
command_manager: _record_transmission passes extra keyword arguments through, so #336's recipient_key (DMs) and channel_idx (channels) reach record_transmission as on dev. graph_trace_helper: dev's rewrite from #336, with its two repeater-key lookups (immediate neighbor and _unique_repeater_key) going through contacts_repo.unique_recent_repeater_key, whose queries are identical.
223 lines
9.1 KiB
Python
223 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Graph update helper for trace data.
|
|
Shared by message_handler (on RX) and trace command (when TRACE_DATA is received).
|
|
"""
|
|
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
from .contacts_repo import unique_recent_repeater_key
|
|
from .transmission_tracker import own_public_key
|
|
|
|
|
|
def update_mesh_graph_from_trace_data(
|
|
bot: Any,
|
|
path_hashes: list[str],
|
|
packet_info: dict[str, Any],
|
|
*,
|
|
is_our_trace: Optional[bool] = None,
|
|
) -> None:
|
|
"""Update mesh graph with edges from a trace packet's pathHashes.
|
|
|
|
When the bot receives a trace packet, it's the destination, so we can confirm
|
|
the edges in the path. The pathHashes represent the routing path the packet took.
|
|
|
|
Special case: If this is a trace we sent that came back through an immediate neighbor,
|
|
we can trust both directions (Bot -> Neighbor and Neighbor -> Bot).
|
|
|
|
Args:
|
|
bot: MeshCoreBot instance (mesh_graph, transmission_tracker, config, db_manager, meshcore).
|
|
path_hashes: Per-hop hash strings from the trace payload (uppercase hex). Length in nibbles is
|
|
``2 * (1 << (flags & 3))`` per hop (1, 2, 4, or 8 bytes), not always 2 hex chars.
|
|
packet_info: Packet information dictionary (packet_hash optional; used when is_our_trace is None).
|
|
is_our_trace: If None, derived from packet_info['packet_hash'] and transmission_tracker.
|
|
If True/False, use that value (e.g. trace command sets True when TRACE_DATA matches our tag).
|
|
"""
|
|
if not path_hashes or len(path_hashes) == 0:
|
|
bot.logger.debug("Mesh graph: Trace packet has no pathHashes, skipping graph update")
|
|
return
|
|
|
|
if not hasattr(bot, "mesh_graph") or not bot.mesh_graph:
|
|
bot.logger.debug("Mesh graph: Graph not initialized, skipping trace update")
|
|
return
|
|
|
|
if not hasattr(bot, "transmission_tracker") or not bot.transmission_tracker:
|
|
bot.logger.debug("Mesh graph: Cannot get bot prefix, skipping trace update")
|
|
return
|
|
|
|
mesh_graph = bot.mesh_graph
|
|
# Name the bot at the trace's own hash width, so its edges match the hops'
|
|
bot_key = own_public_key(bot)
|
|
width = len(path_hashes[-1])
|
|
bot_prefix = bot_key[:width] if bot_key and len(bot_key) >= width else bot.transmission_tracker.bot_prefix
|
|
|
|
if not bot_prefix:
|
|
bot.logger.debug("Mesh graph: Bot prefix not available, skipping trace update")
|
|
return
|
|
|
|
bot_prefix = bot_prefix.lower()
|
|
# 2-byte and wider hashes confirm the link at that width
|
|
edge_width = {"prefix_bytes": 2} if width >= 4 else {}
|
|
|
|
# Resolve is_our_trace
|
|
if is_our_trace is None:
|
|
is_our_trace = False
|
|
packet_hash = packet_info.get("packet_hash")
|
|
if packet_hash and hasattr(bot, "transmission_tracker"):
|
|
record = bot.transmission_tracker.match_packet_hash(packet_hash, time.time())
|
|
if record:
|
|
is_our_trace = True
|
|
bot.logger.debug("Mesh graph: Trace packet is one we sent (matched transmission record)")
|
|
|
|
# Check if this came back through an immediate neighbor
|
|
is_immediate_neighbor = False
|
|
if is_our_trace and len(path_hashes) == 1:
|
|
is_immediate_neighbor = True
|
|
bot.logger.info(
|
|
f"Mesh graph: Trace came back through immediate neighbor {path_hashes[0]} - trusting both directions"
|
|
)
|
|
|
|
bot.logger.debug(
|
|
f"Mesh graph: Updating graph from trace pathHashes: {path_hashes} "
|
|
f"(bot is destination: {bot_prefix}, is_our_trace: {is_our_trace}, immediate_neighbor: {is_immediate_neighbor})"
|
|
)
|
|
|
|
recency_days = bot.config.getint("Path_Command", "graph_edge_expiration_days", fallback=7)
|
|
|
|
from .utils import _get_node_location_from_db, calculate_distance
|
|
|
|
bot_location = None
|
|
try:
|
|
bot_location_result = _get_node_location_from_db(bot, bot_prefix, None, recency_days)
|
|
if bot_location_result:
|
|
bot_location, _ = bot_location_result
|
|
except Exception as e:
|
|
bot.logger.debug(f"Could not get bot location: {e}")
|
|
|
|
if len(path_hashes) == 0:
|
|
return
|
|
|
|
|
|
if is_immediate_neighbor:
|
|
neighbor_prefix = path_hashes[0].lower()
|
|
if neighbor_prefix == bot_prefix:
|
|
# The graph, keyed by prefix, can't tell this neighbor from the bot
|
|
bot.logger.debug(f"Mesh graph: Neighbor {neighbor_prefix} shares the bot's prefix, skipping trace update")
|
|
return
|
|
neighbor_key = None
|
|
try:
|
|
_, unique_key = unique_recent_repeater_key(bot.db_manager, neighbor_prefix, recency_days)
|
|
if unique_key:
|
|
neighbor_key = unique_key
|
|
except Exception as e:
|
|
bot.logger.debug(f"Error checking uniqueness for immediate neighbor {neighbor_prefix}: {e}")
|
|
|
|
geographic_distance = None
|
|
try:
|
|
if bot_location:
|
|
neighbor_result = _get_node_location_from_db(bot, neighbor_prefix, bot_location, recency_days)
|
|
if neighbor_result:
|
|
# The location match is only for the distance, not the neighbor's identity
|
|
neighbor_location, _ = neighbor_result
|
|
if neighbor_location and bot_location:
|
|
geographic_distance = calculate_distance(
|
|
neighbor_location[0], neighbor_location[1],
|
|
bot_location[0], bot_location[1],
|
|
)
|
|
except Exception as e:
|
|
bot.logger.debug(f"Could not calculate distance for immediate neighbor edge: {e}")
|
|
|
|
mesh_graph.add_edge(
|
|
from_prefix=bot_prefix,
|
|
to_prefix=neighbor_prefix,
|
|
from_public_key=bot_key,
|
|
to_public_key=neighbor_key,
|
|
hop_position=1,
|
|
geographic_distance=geographic_distance,
|
|
**edge_width,
|
|
)
|
|
mesh_graph.add_edge(
|
|
from_prefix=neighbor_prefix,
|
|
to_prefix=bot_prefix,
|
|
from_public_key=neighbor_key,
|
|
to_public_key=bot_key,
|
|
hop_position=1,
|
|
geographic_distance=geographic_distance,
|
|
**edge_width,
|
|
)
|
|
bot.logger.info(f"Mesh graph: Created trusted bidirectional edge with immediate neighbor {neighbor_prefix}")
|
|
return
|
|
|
|
# Regular case: the bot received the trace at the end of its path. Our own
|
|
# trace also left from the bot, so it confirms the bot -> first hop link too.
|
|
chain = [h.lower() for h in path_hashes]
|
|
if is_our_trace:
|
|
chain = [bot_prefix] + chain
|
|
chain = chain + [bot_prefix]
|
|
# Which chain positions are the bot itself (a hop may share its short prefix)
|
|
is_bot = [is_our_trace and i == 0 or i == len(chain) - 1 for i in range(len(chain))]
|
|
|
|
hop_keys: dict[str, Optional[str]] = {}
|
|
keys: list[Optional[str]] = []
|
|
for i, node in enumerate(chain):
|
|
if is_bot[i]:
|
|
keys.append(bot_key)
|
|
else:
|
|
if node not in hop_keys:
|
|
hop_keys[node] = _unique_repeater_key(bot, node, recency_days)
|
|
keys.append(hop_keys[node])
|
|
|
|
# Locations only feed distances; a location match among colliding prefixes is a
|
|
# guess, so it never becomes the stored public key.
|
|
locations: dict[int, Optional[tuple[float, float]]] = {}
|
|
reference = bot_location
|
|
for i, node in enumerate(chain):
|
|
if is_bot[i]:
|
|
locations[i] = bot_location
|
|
else:
|
|
locations[i] = None
|
|
try:
|
|
found = _get_node_location_from_db(bot, node, reference, recency_days)
|
|
if found and found[0]:
|
|
locations[i] = found[0]
|
|
except Exception as e:
|
|
bot.logger.debug(f"Could not locate trace node {node}: {e}")
|
|
if locations[i]:
|
|
reference = locations[i]
|
|
|
|
for i in range(len(chain) - 1):
|
|
from_node, to_node = chain[i], chain[i + 1]
|
|
if from_node == to_node:
|
|
# A hop sharing its neighbor's prefix: the graph, keyed by prefix, can't tell them apart
|
|
continue
|
|
geographic_distance = None
|
|
from_location, to_location = locations[i], locations[i + 1]
|
|
if from_location and to_location:
|
|
try:
|
|
geographic_distance = calculate_distance(
|
|
from_location[0], from_location[1], to_location[0], to_location[1],
|
|
)
|
|
except Exception as e:
|
|
bot.logger.debug(f"Could not calculate distance for trace edge {from_node}->{to_node}: {e}")
|
|
mesh_graph.add_edge(
|
|
from_prefix=from_node,
|
|
to_prefix=to_node,
|
|
from_public_key=keys[i],
|
|
to_public_key=keys[i + 1],
|
|
hop_position=i + 1,
|
|
geographic_distance=geographic_distance,
|
|
**edge_width,
|
|
)
|
|
|
|
|
|
def _unique_repeater_key(bot: Any, prefix: str, recency_days: int) -> Optional[str]:
|
|
"""The public key of the only recently heard repeater or room server with ``prefix``, else None."""
|
|
try:
|
|
_, unique_key = unique_recent_repeater_key(bot.db_manager, prefix, recency_days)
|
|
if unique_key:
|
|
return unique_key
|
|
except Exception as e:
|
|
bot.logger.debug(f"Error checking uniqueness for trace node {prefix}: {e}")
|
|
return None
|