fix(announces): type peers/relays by aspect, stop relays leaking into Peers#97
Conversation
… Peers The Network announces "Peers" filter (the default) also showed relays. Two stacked bugs: 1. The .peers filter keyed on aspect (== "lxmf.delivery" || nil) while .relays keyed on the independent isRelay flag, so a contact that was both satisfied both predicates and showed under the default Peers view. 2. Contact.init ran PropagationNodeInfo.parse(app_data) before the aspect branches; that parser accepts any >=3-element msgpack array without verifying the destination is a propagation node, so a delivery/audio/site announce carrying such app_data was mis-flagged isRelay=true. Fix: make the aspect the sole signal for announce typing, matching the reference clients (Sideband types by RNS aspect_filter; Android by NodeType.fromAspect). Removed the app_data relay heuristic, so relays are strictly aspect "lxmf.propagation"; an unrecognized/empty aspect defaults to peer. Filters now key on the mutually-exclusive badgeType, so the four node types can't overlap. Both backends already guarantee a genuine propagation node arrives tagged "lxmf.propagation" (Python via per-aspect RNS announce handlers, native via cryptographic detectedAspect) or is dropped before reaching Contact.init, so the removed heuristic only ever caught false positives. Added a warning in handlePythonEvent when an announce arrives with an empty/unrecognized aspect (there is no fallback now), and corrected the stale rns_bridge.py scope docstring so the propagation handler can't be narrowed away by accident. Adds AnnounceClassificationTests (the filter previously had no coverage). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Greptile SummaryFixes a double bug where relays leaked into the default Peers view: the filter keyed on
Confidence Score: 5/5Safe to merge — the fix is surgical, well-tested, and aligns Columba with the reference clients' aspect-first classification contract. The change removes a single mis-scoped heuristic and replaces it with a single, mutually-exclusive discriminator that matches how both Sideband and Android already type announces. All four filter predicates now key on the same field, so the overlap that caused the leak is structurally impossible. Seven new tests pin the regression case and the full test suite passes. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[PathEntry arrives] --> B{entry.isLXMFPropagationNode?}
B -- yes --> C[badgeType = .relay\nisRelay = true]
B -- no --> D{entry.isLXSTTelephony?}
D -- yes --> E[badgeType = .audio\nisRelay = false]
D -- no --> F{detectedAspect ==\n'nomadnetwork.node'?}
F -- yes --> G[badgeType = .node\nisRelay = false]
F -- no --> H[badgeType = .peer\nisRelay = false\n'lxmf.delivery' or unknown]
subgraph applyAnnounceFilters
I[.peers] -->|badgeType == .peer| J[✔ peers only]
K[.relays] -->|badgeType == .relay| L[✔ relays only]
M[.audio] -->|badgeType == .audio| N[✔ audio only]
O[.sites] -->|badgeType == .node| P[✔ sites only]
end
C --> K
E --> M
G --> O
H --> I
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[PathEntry arrives] --> B{entry.isLXMFPropagationNode?}
B -- yes --> C[badgeType = .relay\nisRelay = true]
B -- no --> D{entry.isLXSTTelephony?}
D -- yes --> E[badgeType = .audio\nisRelay = false]
D -- no --> F{detectedAspect ==\n'nomadnetwork.node'?}
F -- yes --> G[badgeType = .node\nisRelay = false]
F -- no --> H[badgeType = .peer\nisRelay = false\n'lxmf.delivery' or unknown]
subgraph applyAnnounceFilters
I[.peers] -->|badgeType == .peer| J[✔ peers only]
K[.relays] -->|badgeType == .relay| L[✔ relays only]
M[.audio] -->|badgeType == .audio| N[✔ audio only]
O[.sites] -->|badgeType == .node| P[✔ sites only]
end
C --> K
E --> M
G --> O
H --> I
Reviews (2): Last reviewed commit: "fix(announces): key .audio filter on bad..." | Re-trigger Greptile |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Addresses Greptile P2: the .audio announce filter still keyed on isAudio (aspect == "lxst.telephony") while .peers/.relays/.sites were unified onto the mutually-exclusive badgeType. Equivalent today, but the asymmetry is a footgun for a Contact built via the memberwise init. Now all four filters discriminate on badgeType. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
In the Network announces tab, the Peers filter (the default) also showed relays.
Two stacked bugs:
.peersfiltered onaspect == "lxmf.delivery" || aspect == nilwhile.relaysfiltered on the independentisRelayflag. A contact that wasisRelay == trueand carried a delivery/nil aspect satisfied both predicates, so it appeared under the default Peers view.Contact.init(from:)ranPropagationNodeInfo.parse(app_data)before the aspect branches, and that parser accepts any ≥3-element msgpack array without verifying the destination is a propagation node. So alxmf.delivery(or audio/site) announce carrying such app_data got taggedisRelay = truewhile keeping its real aspect. (Columba's own delivery announces are 2-element arrays, which is why only some relays leaked — it fired on other clients / newer LXMF formats.)Fix
Make the aspect the sole signal for announce typing, matching the reference clients:
aspect_filter == "lxmf.propagation"vs"lxmf.delivery").NodeType.fromAspect.Changes:
Contact.init: removed thePropagationNodeInfo.parse(app_data)relay heuristic. Relays are now strictlyaspect == "lxmf.propagation"(viaentry.isLXMFPropagationNode); an unrecognized/empty aspect defaults to.peer(matchesNodeType.fromAspect's default)..peers/.relaysnow key on the mutually-exclusivebadgeType(.peer/.relay), symmetric with.sites, so the four node types can't overlap.handlePythonEvent: added a warning if an announce ever arrives with an empty/unrecognized aspect — since there's no app_data fallback anymore, this surfaces it loudly instead of silently classifying as a peer.app/rns_bridge.py: corrected the stale "lxmf.delivery only" scope docstring so the per-aspect propagation handler can't be narrowed away by accident.Why nothing regresses
Both announce backends guarantee a genuine propagation node arrives tagged
"lxmf.propagation"or is dropped before reachingContact.init, so the removed heuristic only ever caught false positives:aspect_filter), and RNS enforces the filter by cryptographic destination-hash match — a real propagation node can only emit"lxmf.propagation". Worst case is omission (no event), which the fallback couldn't have rescued.detectedAspectcryptographically; a propagation node yields"lxmf.propagation"ornil(dropped before emit). A mistag would require a SHA-256 collision.Tests
Adds
AnnounceClassificationTests(the filter had no prior coverage) — 7 cases pinning the classification, including the exact leak (delivery + ≥3-element app_data → .peer), the new strict contract (untagged propagation-shaped app_data → .peer, not relay), andunknown aspect → .peer. FullColumbaAppTestssuite passes (98 tests, 0 failures) on the iPhone 17 simulator.🤖 Generated with Claude Code