From baf7f5d137c4d93d07fb0d7b740a98b7f63835c0 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:41:49 +0200 Subject: [PATCH] Ensure node numeric aliases are derived from canonical IDs (#101) * Derive node numeric aliases when missing * Preserve raw message senders when storing payloads * Normalize packet message sender ids when available * run rufo --- web/app.rb | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/web/app.rb b/web/app.rb index c3a6be9..cfcdb15 100644 --- a/web/app.rb +++ b/web/app.rb @@ -236,6 +236,48 @@ get "/api/messages" do query_messages(limit).to_json end +# Determine the numeric node reference for a canonical node identifier. +# +# The Meshtastic protobuf encodes the node ID as a hexadecimal string prefixed +# with an exclamation mark (for example ``!4ed36bd0``). Many payloads also +# include a decimal ``num`` alias, but some integrations omit it. When the +# alias is missing we can reconstruct it from the canonical identifier so that +# later joins using ``nodes.num`` continue to work. +# +# @param node_id [String, nil] canonical node identifier (e.g. ``!4ed36bd0``). +# @param payload [Hash] raw node payload provided by the data daemon. +# @return [Integer, nil] numeric node reference if it can be determined. +def resolve_node_num(node_id, payload) + raw = payload["num"] + + case raw + when Integer + return raw + when Numeric + return raw.to_i + when String + trimmed = raw.strip + return nil if trimmed.empty? + return Integer(trimmed, 10) if trimmed.match?(/\A[0-9]+\z/) + return Integer(trimmed.delete_prefix("0x").delete_prefix("0X"), 16) if trimmed.match?(/\A0[xX][0-9A-Fa-f]+\z/) + if trimmed.match?(/\A[0-9A-Fa-f]+\z/) + canonical = node_id.is_a?(String) ? node_id.strip : "" + return Integer(trimmed, 16) if canonical.match?(/\A!?[0-9A-Fa-f]+\z/) + end + end + + return nil unless node_id.is_a?(String) + + hex = node_id.strip + return nil if hex.empty? + hex = hex.delete_prefix("!") + return nil unless hex.match?(/\A[0-9A-Fa-f]+\z/) + + Integer(hex, 16) +rescue ArgumentError + nil +end + # Insert or update a node row with the most recent metrics. # # @param db [SQLite3::Database] open database handle. @@ -259,9 +301,11 @@ def upsert_node(db, node_id, n) else v end } + node_num = resolve_node_num(node_id, n) + row = [ node_id, - n["num"], + node_num, user["shortName"], user["longName"], user["macaddr"], @@ -314,6 +358,15 @@ def require_token! halt 403, { error: "Forbidden" }.to_json unless token && !token.empty? && provided == token end +# Determine whether the canonical node identifier should replace the provided +# sender reference for a message payload. +# +# @param message [Object] raw request payload element. +# @return [Boolean] +def prefer_canonical_sender?(message) + message.is_a?(Hash) && message.key?("packet_id") && !message.key?("id") +end + # Insert a text message if it does not already exist. # # @param db [SQLite3::Database] open database handle. @@ -323,8 +376,16 @@ def insert_message(db, m) return unless msg_id rx_time = m["rx_time"]&.to_i || Time.now.to_i rx_iso = m["rx_iso"] || Time.at(rx_time).utc.iso8601 - from_id = normalize_node_id(db, m["from_id"]) || m["from_id"] - from_id = from_id.to_s.strip unless from_id.nil? + raw_from_id = m["from_id"] + trimmed_from_id = raw_from_id.nil? ? nil : raw_from_id.to_s.strip + trimmed_from_id = nil if trimmed_from_id&.empty? + canonical_from_id = normalize_node_id(db, raw_from_id) + use_canonical = canonical_from_id && (trimmed_from_id.nil? || prefer_canonical_sender?(m)) + from_id = if use_canonical + canonical_from_id.to_s.strip + else + trimmed_from_id + end from_id = nil if from_id&.empty? row = [ msg_id,