forked from Keelrleek/SoulStream
a second instance on one machine: its own data, its own peer port, and a join that says itself again when it was never seated
This commit is contained in:
1 parent
67e52697b2
commit
6f7452c803
17 files changed
+125
-56
No files matched your search
+55
-13
@@ -61,6 +61,12 @@ struct SpaceRecord {
|
||||
/// OWNER ONLY: personas we have invited and will accept a join from.
|
||||
#[serde(default)]
|
||||
invited: BTreeMap<String, u64>,
|
||||
/// When we last told the owner which key we would wear. The answer is one
|
||||
/// sealed message and it can simply fail to land — the owner's descriptor
|
||||
/// may not resolve on a young node — and a member who is never seated has
|
||||
/// no way back in. Opening the Space re-sends after this goes stale.
|
||||
#[serde(default)]
|
||||
join_sent_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
@@ -170,9 +176,7 @@ fn record_id(owner: &str, space_id: &str) -> String {
|
||||
}
|
||||
|
||||
fn path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
Ok(crate::paths::app_data(app)
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("freenet")
|
||||
.join(LEDGER_FILE))
|
||||
@@ -433,6 +437,7 @@ pub async fn create(app: &tauri::AppHandle, name: &str, description: &str, priva
|
||||
room_secret: None,
|
||||
roster: BTreeMap::new(),
|
||||
invited: BTreeMap::new(),
|
||||
join_sent_at: 0,
|
||||
};
|
||||
let _ledger = ledger::hold(LEDGER_FILE).await;
|
||||
let mut l = load(app);
|
||||
@@ -470,7 +475,10 @@ pub async fn open(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str) -> R
|
||||
}
|
||||
}
|
||||
let rec = l.spaces.get(&rid).cloned().unwrap_or(rec);
|
||||
Ok(view_of(app, &rec, &state, &worn_key(&rec, &me), reachable))
|
||||
let worn = worn_key(&rec, &me);
|
||||
let seated = state.checkpoint.as_ref().is_some_and(|c| c.members.iter().any(|m| m.key == worn));
|
||||
retry_join_if_stranded(app, &rec, seated);
|
||||
Ok(view_of(app, &rec, &state, &worn, reachable))
|
||||
}
|
||||
|
||||
pub async fn post(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, text: &str) -> Result<(), String> {
|
||||
@@ -685,6 +693,7 @@ pub(crate) async fn on_inbox_invite(app: &tauri::AppHandle, sender: [u8; 32], in
|
||||
room_secret: None,
|
||||
roster: BTreeMap::new(),
|
||||
invited: BTreeMap::new(),
|
||||
join_sent_at: 0,
|
||||
});
|
||||
rec.name = cap(&inv.name, sp::MAX_NAME_BYTES);
|
||||
rec.private = inv.private;
|
||||
@@ -708,23 +717,55 @@ pub(crate) async fn on_inbox_invite(app: &tauri::AppHandle, sender: [u8; 32], in
|
||||
secret
|
||||
};
|
||||
log::info!("spaces: invited to {:?} by {}", inv.name, b58(&inv.owner));
|
||||
// The answer: the key we will wear, and a persona signature so the owner
|
||||
// knows it is us. Sealed to the owner — this pairing is exactly what must
|
||||
// never be published.
|
||||
let room_key = sp::member_public(&room);
|
||||
let proof = match sign(app, sp::join_proof_payload(&inv.space_id, &inv.owner, &room_key)).await {
|
||||
send_join(app, &inv.owner, &inv.space_id, &room).await;
|
||||
}
|
||||
|
||||
/// Tell the owner which key we will wear: the key itself, and a persona
|
||||
/// signature over it so they know it is us. Sealed — that pairing is exactly
|
||||
/// what must never be published. Best-effort, and stamped so `open` can try
|
||||
/// again; being un-seated is the one state a member cannot fix by hand.
|
||||
async fn send_join(app: &tauri::AppHandle, owner: &[u8; 32], space_id: &[u8; 16], room: &[u8; 32]) {
|
||||
let room_key = sp::member_public(room);
|
||||
let proof = match sign(app, sp::join_proof_payload(space_id, owner, &room_key)).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::warn!("spaces: could not sign the join for {:?}: {e}", inv.name);
|
||||
log::warn!("spaces: could not sign a join: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let join = Cargo::SpaceJoin(SpaceJoin { space_id: inv.space_id, owner: inv.owner, room_key, proof });
|
||||
if let Err(e) = inbox::send(app, &inv.owner, &join).await {
|
||||
log::warn!("spaces: could not answer the invitation to {:?}: {e}", inv.name);
|
||||
let join = Cargo::SpaceJoin(SpaceJoin { space_id: *space_id, owner: *owner, room_key, proof });
|
||||
match inbox::send(app, owner, &join).await {
|
||||
Ok(()) => {
|
||||
let _ledger = ledger::hold(LEDGER_FILE).await;
|
||||
let mut l = load(app);
|
||||
if let Some(rec) = l.spaces.get_mut(&record_id(&b58(owner), &b58(space_id))) {
|
||||
rec.join_sent_at = now_secs();
|
||||
let _ = save(app, &l);
|
||||
}
|
||||
log::info!("spaces: told {} which key we will wear", b58(owner));
|
||||
}
|
||||
Err(e) => log::warn!("spaces: could not answer an invitation: {e} — will try again on open"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A member who holds a room key but is not on the roster never got seated.
|
||||
/// Say so again, spaced out, rather than leaving them stranded.
|
||||
const JOIN_RETRY_SECS: u64 = 120;
|
||||
fn retry_join_if_stranded(app: &tauri::AppHandle, rec: &SpaceRecord, seated: bool) {
|
||||
if seated || rec.my_role == "owner" {
|
||||
return;
|
||||
}
|
||||
let Some(room) = room_secret_of(rec) else { return };
|
||||
if now_secs().saturating_sub(rec.join_sent_at) < JOIN_RETRY_SECS {
|
||||
return;
|
||||
}
|
||||
let (Ok(owner), Ok(space_id)) = (key_of(&rec.owner), id_of(&rec.space_id)) else { return };
|
||||
let handle = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send_join(&handle, &owner, &space_id, &room).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// The owner's side of the handshake: someone we invited says which key they
|
||||
/// will wear. Verify it is really them, then put THAT key — and only that key
|
||||
/// — on the published roster.
|
||||
@@ -843,6 +884,7 @@ mod tests {
|
||||
room_secret: secret.map(|s| b58(&s)),
|
||||
roster: BTreeMap::new(),
|
||||
invited: BTreeMap::new(),
|
||||
join_sent_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user