fix(mesh): message federation contacts without a radio (fixes 'Missing contact_id')
Messaging a federation-only peer (e.g. 'Arch Dev') failed with 'Missing contact_id'. The UI gave federation-only rows a *negative* placeholder contact_id derived from a DID hash, but the backend parses contact_id as u64, so a negative value deserialized to None. The negative id also never matched the positive federation-synthetic id that federation-routed messages are stored under, so those threads looked empty. - Frontend: derive the SAME positive federation-synthetic id the backend uses (federationContactId mirrors federation_peer_contact_id) so mesh.send accepts it and messages thread correctly. - Backend: send_typed_wire now resolves a federation-synthetic contact_id from nodes.json when it isn't in the live mesh peer table (radio-less node), instead of bailing 'Unknown federation peer'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e456c9701b
commit
1ea3f8d65c
@ -825,15 +825,33 @@ impl MeshService {
|
|||||||
let is_federation_synthetic = contact_id & 0x8000_0000 != 0;
|
let is_federation_synthetic = contact_id & 0x8000_0000 != 0;
|
||||||
let exceeds_lora = wire.len() > protocol::MAX_MESSAGE_LEN;
|
let exceeds_lora = wire.len() > protocol::MAX_MESSAGE_LEN;
|
||||||
if is_federation_synthetic || exceeds_lora {
|
if is_federation_synthetic || exceeds_lora {
|
||||||
let (peer_pubkey, peer_did) = {
|
// Resolve the peer's pubkey/did. Prefer the live mesh peer table,
|
||||||
|
// but fall back to federation storage for federation-synthetic ids
|
||||||
|
// that were never seeded into `state.peers` — e.g. a radio-less
|
||||||
|
// node where the mesh device table is empty. Without this fallback
|
||||||
|
// chatting a federation contact bails "Unknown federation peer"
|
||||||
|
// even though we know its onion from nodes.json.
|
||||||
|
let from_table = {
|
||||||
let peers = self.state.peers.read().await;
|
let peers = self.state.peers.read().await;
|
||||||
match peers.get(&contact_id) {
|
peers
|
||||||
Some(p) => (p.pubkey_hex.clone(), p.did.clone()),
|
.get(&contact_id)
|
||||||
None if is_federation_synthetic => {
|
.map(|p| (p.pubkey_hex.clone(), p.did.clone()))
|
||||||
anyhow::bail!("Unknown federation peer {}", contact_id);
|
};
|
||||||
|
let (peer_pubkey, peer_did) = match from_table {
|
||||||
|
Some(v) => v,
|
||||||
|
None if is_federation_synthetic => {
|
||||||
|
let nodes = crate::federation::load_nodes(&self.data_dir)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
match nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| federation_peer_contact_id(&n.pubkey) == contact_id)
|
||||||
|
{
|
||||||
|
Some(n) => (Some(n.pubkey.clone()), Some(n.did.clone())),
|
||||||
|
None => anyhow::bail!("Unknown federation peer {}", contact_id),
|
||||||
}
|
}
|
||||||
None => (None, None),
|
|
||||||
}
|
}
|
||||||
|
None => (None, None),
|
||||||
};
|
};
|
||||||
let nodes = crate::federation::load_nodes(&self.data_dir)
|
let nodes = crate::federation::load_nodes(&self.data_dir)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -626,10 +626,12 @@ const mergedPeers = computed<MergedPeer[]>(() => {
|
|||||||
if (groups.has(key)) continue
|
if (groups.has(key)) continue
|
||||||
// Synthesise a placeholder MeshPeer so openChat() and the existing
|
// Synthesise a placeholder MeshPeer so openChat() and the existing
|
||||||
// rssi/avatar template paths don't need a separate code path for
|
// rssi/avatar template paths don't need a separate code path for
|
||||||
// "federation-only" rows. The contact_id is a stable negative number
|
// "federation-only" rows. The contact_id MUST match the backend's
|
||||||
// derived from the DID hash so it never collides with real mesh
|
// federation-synthetic id (high bit set, derived from the pubkey) so
|
||||||
// contact_ids (which are u32 from the radio firmware).
|
// `mesh.send` accepts it (it parses contact_id as u64 — a negative
|
||||||
const synthCid = -100 - Math.abs(hashStringToInt(fed.did))
|
// placeholder fails with "Missing contact_id") and so federation-routed
|
||||||
|
// messages stored under that id land in this thread.
|
||||||
|
const synthCid = federationContactId(fed.pubkey)
|
||||||
const placeholder: MeshPeer = {
|
const placeholder: MeshPeer = {
|
||||||
contact_id: synthCid,
|
contact_id: synthCid,
|
||||||
advert_name: fed.name || fed.did,
|
advert_name: fed.name || fed.did,
|
||||||
@ -657,10 +659,16 @@ const mergedPeers = computed<MergedPeer[]>(() => {
|
|||||||
return Array.from(groups.values())
|
return Array.from(groups.values())
|
||||||
})
|
})
|
||||||
|
|
||||||
function hashStringToInt(s: string): number {
|
// Mirror of the backend's `federation_peer_contact_id` (mesh/mod.rs): take the
|
||||||
let h = 0
|
// first 4 bytes of the archipelago pubkey as a little-endian u32, clear the top
|
||||||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
|
// bit, then set it as the federation marker. Producing the SAME id here means a
|
||||||
return h
|
// federation-only row addresses the exact contact the backend routes/stores
|
||||||
|
// under, instead of an unsendable negative placeholder.
|
||||||
|
function federationContactId(pubkeyHex: string): number {
|
||||||
|
const bytes = pubkeyHex.match(/../g)?.map(h => parseInt(h, 16)) ?? []
|
||||||
|
if (bytes.length < 4) return 0x80000001
|
||||||
|
const low = ((bytes[0]) | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >>> 0
|
||||||
|
return ((0x80000000 | (low & 0x7fffffff)) >>> 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// activeChatPeer is a single MeshPeer (the row the user clicked). To unify
|
// activeChatPeer is a single MeshPeer (the row the user clicked). To unify
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user