mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
Add placeholder nodes for unknown senders (#181)
* Add placeholder nodes for unknown senders * run rufo
This commit is contained in:
110
web/app.rb
110
web/app.rb
@@ -504,6 +504,112 @@ rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
# Determine canonical node identifiers and derived metadata for a reference.
|
||||
#
|
||||
# @param node_ref [Object] raw node identifier or numeric reference.
|
||||
# @param fallback_num [Object] optional numeric reference used when the
|
||||
# identifier does not encode the value directly.
|
||||
# @return [Array(String, Integer, String), nil] tuple containing the canonical
|
||||
# node ID, numeric node reference, and uppercase short identifier suffix when
|
||||
# the reference can be parsed. Returns nil when the reference cannot be
|
||||
# converted into a canonical ID.
|
||||
def canonical_node_parts(node_ref, fallback_num = nil)
|
||||
fallback = coerce_integer(fallback_num)
|
||||
|
||||
hex = nil
|
||||
num = nil
|
||||
|
||||
case node_ref
|
||||
when Integer
|
||||
num = node_ref
|
||||
when Numeric
|
||||
num = node_ref.to_i
|
||||
when String
|
||||
trimmed = node_ref.strip
|
||||
return nil if trimmed.empty?
|
||||
|
||||
if trimmed.start_with?("!")
|
||||
hex = trimmed.delete_prefix("!")
|
||||
elsif trimmed.match?(/\A0[xX][0-9A-Fa-f]+\z/)
|
||||
hex = trimmed[2..].to_s
|
||||
elsif trimmed.match?(/\A-?\d+\z/)
|
||||
num = trimmed.to_i
|
||||
elsif trimmed.match?(/\A[0-9A-Fa-f]+\z/)
|
||||
hex = trimmed
|
||||
else
|
||||
return nil
|
||||
end
|
||||
when nil
|
||||
num = fallback if fallback
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
num ||= fallback if fallback
|
||||
|
||||
if hex
|
||||
begin
|
||||
num ||= Integer(hex, 16)
|
||||
rescue ArgumentError
|
||||
return nil
|
||||
end
|
||||
elsif num
|
||||
return nil if num.negative?
|
||||
hex = format("%08x", num & 0xFFFFFFFF)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
return nil if hex.nil? || hex.empty?
|
||||
|
||||
begin
|
||||
parsed = Integer(hex, 16)
|
||||
rescue ArgumentError
|
||||
return nil
|
||||
end
|
||||
|
||||
parsed &= 0xFFFFFFFF
|
||||
canonical_hex = format("%08x", parsed)
|
||||
short_id = canonical_hex[-4, 4].upcase
|
||||
|
||||
["!#{canonical_hex}", parsed, short_id]
|
||||
end
|
||||
|
||||
# Ensure a placeholder node entry exists for the provided identifier.
|
||||
#
|
||||
# Messages and telemetry can reference nodes before the daemon has received a
|
||||
# full node snapshot. When this happens we create a minimal hidden entry so the
|
||||
# sender can be resolved in the UI until richer metadata becomes available.
|
||||
#
|
||||
# @param db [SQLite3::Database] open database handle.
|
||||
# @param node_ref [Object] raw identifier extracted from the payload.
|
||||
# @param fallback_num [Object] optional numeric reference used when the
|
||||
# identifier is missing.
|
||||
def ensure_unknown_node(db, node_ref, fallback_num = nil)
|
||||
parts = canonical_node_parts(node_ref, fallback_num)
|
||||
return unless parts
|
||||
|
||||
node_id, node_num, short_id = parts
|
||||
|
||||
existing = db.get_first_value(
|
||||
"SELECT 1 FROM nodes WHERE node_id = ? LIMIT 1",
|
||||
[node_id],
|
||||
)
|
||||
return if existing
|
||||
|
||||
long_name = "Meshtastic #{short_id}"
|
||||
|
||||
with_busy_retry do
|
||||
db.execute(
|
||||
<<~SQL,
|
||||
INSERT OR IGNORE INTO nodes(node_id,num,short_name,long_name,role)
|
||||
VALUES (?,?,?,?,?)
|
||||
SQL
|
||||
[node_id, node_num, short_id, long_name, "CLIENT_HIDDEN"],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Insert or update a node row with the most recent metrics.
|
||||
#
|
||||
# @param db [SQLite3::Database] open database handle.
|
||||
@@ -743,6 +849,8 @@ def insert_position(db, payload)
|
||||
canonical = normalize_node_id(db, node_id || node_num)
|
||||
node_id = canonical if canonical
|
||||
|
||||
ensure_unknown_node(db, node_id || node_num, node_num)
|
||||
|
||||
to_id = string_or_nil(payload["to_id"] || payload["to"])
|
||||
|
||||
position_section = payload["position"].is_a?(Hash) ? payload["position"] : {}
|
||||
@@ -936,6 +1044,8 @@ def insert_message(db, m)
|
||||
|
||||
encrypted = string_or_nil(m["encrypted"])
|
||||
|
||||
ensure_unknown_node(db, from_id || raw_from_id, m["from_num"])
|
||||
|
||||
row = [
|
||||
msg_id,
|
||||
rx_time,
|
||||
|
||||
@@ -450,6 +450,39 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
end
|
||||
end
|
||||
|
||||
it "creates hidden nodes for unknown message senders" do
|
||||
payload = {
|
||||
"id" => 9_999,
|
||||
"rx_time" => reference_time.to_i,
|
||||
"rx_iso" => reference_time.iso8601,
|
||||
"from_id" => "!feedf00d",
|
||||
"to_id" => "^all",
|
||||
"channel" => 0,
|
||||
"portnum" => "TEXT_MESSAGE_APP",
|
||||
"text" => "Spec placeholder message",
|
||||
}
|
||||
|
||||
post "/api/messages", payload.to_json, auth_headers
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(JSON.parse(last_response.body)).to eq("status" => "ok")
|
||||
|
||||
with_db(readonly: true) do |db|
|
||||
db.results_as_hash = true
|
||||
row = db.get_first_row(
|
||||
"SELECT node_id, num, short_name, long_name, role FROM nodes WHERE node_id = ?",
|
||||
["!feedf00d"],
|
||||
)
|
||||
|
||||
expect(row).not_to be_nil
|
||||
expect(row["node_id"]).to eq("!feedf00d")
|
||||
expect(row["num"]).to eq(0xfeedf00d)
|
||||
expect(row["short_name"]).to eq("F00D")
|
||||
expect(row["long_name"]).to eq("Meshtastic F00D")
|
||||
expect(row["role"]).to eq("CLIENT_HIDDEN")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns 400 when the payload is not valid JSON" do
|
||||
post "/api/messages", "{", auth_headers
|
||||
|
||||
@@ -605,6 +638,37 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
end
|
||||
end
|
||||
|
||||
it "creates hidden nodes for unknown position senders" do
|
||||
payload = {
|
||||
"id" => 42,
|
||||
"node_id" => "!0badc0de",
|
||||
"rx_time" => reference_time.to_i,
|
||||
"rx_iso" => reference_time.iso8601,
|
||||
"latitude" => 52.1,
|
||||
"longitude" => 13.1,
|
||||
}
|
||||
|
||||
post "/api/positions", payload.to_json, auth_headers
|
||||
|
||||
expect(last_response).to be_ok
|
||||
expect(JSON.parse(last_response.body)).to eq("status" => "ok")
|
||||
|
||||
with_db(readonly: true) do |db|
|
||||
db.results_as_hash = true
|
||||
row = db.get_first_row(
|
||||
"SELECT node_id, num, short_name, long_name, role FROM nodes WHERE node_id = ?",
|
||||
["!0badc0de"],
|
||||
)
|
||||
|
||||
expect(row).not_to be_nil
|
||||
expect(row["node_id"]).to eq("!0badc0de")
|
||||
expect(row["num"]).to eq(0x0badc0de)
|
||||
expect(row["short_name"]).to eq("C0DE")
|
||||
expect(row["long_name"]).to eq("Meshtastic C0DE")
|
||||
expect(row["role"]).to eq("CLIENT_HIDDEN")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns 400 when the payload is not valid JSON" do
|
||||
post "/api/positions", "{", auth_headers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user