holdings: a member of a Space publishes what they hold as room-salted album keys, sealed to the room, so "who has this" is answered while they are offline

This commit is contained in:
KeelrLeek committed 2026-09-08 12:10:49 -05:00
1 parent 08ca09586a
commit a98c750908
12 files changed
+2955 -8

No files matched your search

+64 -5
View File
@@ -66,6 +66,11 @@ struct SpaceRecord {
/// room is not consent to be watched in it.
#[serde(default)]
share_listens: bool,
/// Whether this device publishes what it HOLDS into this Space — album
/// keys sealed to the room, never files (`holdings.rs`). Per Space and
/// off until asked, for the same reason as listens.
#[serde(default)]
share_holdings: bool,
/// The last listen posted here — what, and when. A reload or a restart
/// re-reports whatever is already past the player's threshold, and this
/// is the only side that knows what a room was last told.
@@ -153,6 +158,8 @@ pub struct SpaceView {
pub can_post: bool,
/// Whether THIS device posts what it plays here.
pub share_listens: bool,
/// Whether THIS device publishes its holdings here.
pub share_holdings: bool,
pub has_key: bool,
/// The contract exists on the network (a Space someone invited us to
/// may not have propagated to this node yet).
@@ -226,13 +233,13 @@ fn rand32() -> [u8; 32] {
rand::thread_rng().fill_bytes(&mut n);
n
}
fn b58(bytes: &[u8]) -> String {
pub(crate) fn b58(bytes: &[u8]) -> String {
bs58::encode(bytes).into_string()
}
fn key_of(s: &str) -> Result<[u8; 32], String> {
pub(crate) fn key_of(s: &str) -> Result<[u8; 32], String> {
inbox::key_of(s)
}
fn id_of(s: &str) -> Result<[u8; 16], String> {
pub(crate) fn id_of(s: &str) -> Result<[u8; 16], String> {
bs58::decode(s)
.into_vec()
.map_err(|e| format!("bad id: {e}"))?
@@ -250,7 +257,7 @@ fn cap(s: &str, max: usize) -> String {
}
s[..end].to_string()
}
fn record_id(owner: &str, space_id: &str) -> String {
pub(crate) fn record_id(owner: &str, space_id: &str) -> String {
format!("{owner}:{space_id}")
}
@@ -281,7 +288,7 @@ fn save(app: &tauri::AppHandle, l: &Ledger) -> Result<(), String> {
Ok(())
}
async fn my_key(app: &tauri::AppHandle) -> Result<[u8; 32], String> {
pub(crate) async fn my_key(app: &tauri::AppHandle) -> Result<[u8; 32], String> {
let adapter = app.state::<AppState>().freenet.clone();
let info = persona::current(&adapter)
.await?
@@ -404,6 +411,7 @@ fn view_of(app: &tauri::AppHandle, rec: &SpaceRecord, state: &sp::SpaceState, me
listens: vec![],
can_post: false,
share_listens: rec.share_listens,
share_holdings: rec.share_holdings,
has_key: false,
reachable,
};
@@ -490,6 +498,7 @@ fn view_of(app: &tauri::AppHandle, rec: &SpaceRecord, state: &sp::SpaceState, me
.collect(),
can_post: is_member && has_key,
share_listens: rec.share_listens,
share_holdings: rec.share_holdings,
has_key,
reachable,
}
@@ -563,6 +572,7 @@ pub async fn create(app: &tauri::AppHandle, name: &str, description: &str, priva
roster: BTreeMap::new(),
invited: BTreeMap::new(),
share_listens: false,
share_holdings: false,
last_listen: None,
join_sent_at: 0,
};
@@ -826,6 +836,53 @@ pub async fn set_pinned(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str
/// Turn posting-what-I-play on or off for one Space. Nothing is published
/// by flipping it; it only decides what the next track does.
/// What `holdings.rs` needs to know about a room, without the record.
pub(crate) struct RoomFacts {
pub name: String,
pub private: bool,
pub my_role: String,
pub share_holdings: bool,
}
pub(crate) fn room_facts(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str) -> Option<RoomFacts> {
let l = load(app);
let rec = l.spaces.get(&record_id(owner_b58, space_b58))?;
Some(RoomFacts { name: rec.name.clone(), private: rec.private, my_role: rec.my_role.clone(), share_holdings: rec.share_holdings })
}
/// The room's key for one epoch, if this device was handed it.
pub(crate) fn epoch_key_for(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, epoch: u64) -> Option<[u8; 32]> {
let l = load(app);
let rec = l.spaces.get(&record_id(owner_b58, space_b58))?;
rec.keys.get(&epoch.to_string()).and_then(|k| key_of(k).ok())
}
/// Every room this device is in: (owner, space id, name).
pub(crate) fn rooms(app: &tauri::AppHandle) -> Vec<(String, String, String)> {
load(app).spaces.values().map(|r| (r.owner.clone(), r.space_id.clone(), r.name.clone())).collect()
}
/// The rooms this device publishes its holdings into.
pub(crate) fn rooms_sharing_holdings(app: &tauri::AppHandle) -> Vec<(String, String)> {
load(app).spaces.values().filter(|r| r.share_holdings).map(|r| (r.owner.clone(), r.space_id.clone())).collect()
}
/// The Soulseek name behind a room key as this device knows it: through the
/// owner's private roster, or the key itself when it is a persona (an owner's).
pub(crate) fn username_behind(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, member_b58: &str) -> Option<String> {
let l = load(app);
let rec = l.spaces.get(&record_id(owner_b58, space_b58))?;
slsk_link::username_for_key(app, rec.roster.get(member_b58).map(String::as_str).unwrap_or(member_b58))
}
pub(crate) async fn set_share_holdings_flag(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, on: bool) -> Result<(), String> {
let _ledger = ledger::hold(LEDGER_FILE).await;
let mut l = load(app);
let rec = l.spaces.get_mut(&record_id(owner_b58, space_b58)).ok_or("not a Space this device knows")?;
rec.share_holdings = on;
save(app, &l)
}
pub async fn set_share_listens(app: &tauri::AppHandle, owner_b58: &str, space_b58: &str, on: bool) -> Result<SpaceView, String> {
{
let _ledger = ledger::hold(LEDGER_FILE).await;
@@ -946,6 +1003,7 @@ pub(crate) async fn on_inbox_invite(app: &tauri::AppHandle, sender: [u8; 32], in
roster: BTreeMap::new(),
invited: BTreeMap::new(),
share_listens: false,
share_holdings: false,
last_listen: None,
join_sent_at: 0,
});
@@ -1197,6 +1255,7 @@ mod tests {
roster: BTreeMap::new(),
invited: BTreeMap::new(),
share_listens: false,
share_holdings: false,
last_listen: None,
join_sent_at: 0,
}