the proposal board: a Space's pinned playlist takes signed proposals, and an accept republishes it

This commit is contained in:
KeelrLeek committed 2026-09-02 16:33:38 -05:00
1 parent 8b511b20e7
commit bf81792c49
15 files changed
+3207 -10

No files matched your search

+80 -6
View File
@@ -98,6 +98,8 @@ struct ShareRef {
#[derive(Debug, Serialize)]
pub struct ShareEntry {
/// The occurrence id (b58) — what a proposal's remove or move names.
pub entry_id: String,
pub position: u32,
pub title: String,
pub artist: String,
@@ -245,12 +247,12 @@ pub async fn publish(app: &tauri::AppHandle, lib: &LibraryState, playlist_id: i6
.iter()
.enumerate()
.map(|(i, it)| pl::EntryV1 {
entry_id: rand16(),
entry_id: entry_id_for(&uuid, it.id),
position: i as u32,
recording: pl::RecordingRef {
musicbrainz_recording_id: None,
isrcs: vec![],
deezer_id: None,
deezer_id: it.track_id.clone().filter(|t| !t.is_empty()),
apple_music_id: None,
title: cap(&it.title, pl::MAX_FIELD_BYTES),
artists: vec![cap(&it.artist, pl::MAX_FIELD_BYTES)],
@@ -281,6 +283,7 @@ pub async fn publish(app: &tauri::AppHandle, lib: &LibraryState, playlist_id: i6
.contract_put(REV_WASM.to_vec(), &pl::make_rev_params(&owner, &hash), bytes.clone(), false)
.await
.map_err(|e| e.to_string())?;
cache_revision(app, &hash, &bytes);
let pointer_seq = prev.as_ref().map(|p| p.pointer_seq + 1).unwrap_or(1);
let mut p = pl::PointerState {
schema_version: pl::SCHEMA_VERSION,
@@ -385,10 +388,7 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R
entries: vec![],
});
};
let rev_ids = FreenetAdapter::contract_ids(REV_WASM.to_vec(), &pl::make_rev_params(&owner, &r.content_hash));
let rbytes = adapter.contract_get(&rev_ids.instance, false).await.map_err(|e| e.to_string())?;
let m = pl::validate_revision(&owner, &r.content_hash, &rbytes)
.map_err(|e| format!("revision failed verification: {e}"))?;
let (m, _) = revision_by_hash(app, &owner, &r.content_hash).await?;
Ok(ShareView {
owner_key: owner_b58.to_string(),
playlist_uuid: uuid_b58.to_string(),
@@ -401,6 +401,7 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R
.entries
.into_iter()
.map(|e| ShareEntry {
entry_id: b58(&e.entry_id),
position: e.position,
title: e.recording.title,
artist: e.recording.artists.join(", "),
@@ -412,6 +413,79 @@ pub async fn fetch(app: &tauri::AppHandle, owner_b58: &str, uuid_b58: &str) -> R
})
}
/// The occurrence id a local row publishes under: derived from the playlist
/// uuid and the row id, so it survives re-publication — a proposal's remove
/// or move names the same row the owner has.
pub(crate) fn entry_id_for(uuid: &[u8; 16], item_id: i64) -> [u8; 16] {
let mut buf = Vec::with_capacity(48);
buf.extend_from_slice(b"soulstream.entry.v1");
buf.extend_from_slice(uuid);
buf.extend_from_slice(&item_id.to_le_bytes());
ps::content_hash(&buf)[..16].try_into().unwrap()
}
fn revisions_dir(app: &tauri::AppHandle) -> Option<PathBuf> {
app.path().app_data_dir().ok().map(|d| d.join("freenet").join("revisions"))
}
/// Revisions are kept on disk by hash: the owner's accept path needs the
/// exact base bytes, and a network GET for them costs seconds.
fn cache_revision(app: &tauri::AppHandle, hash: &[u8; 32], bytes: &[u8]) {
if let Some(dir) = revisions_dir(app) {
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(dir.join(format!("{}.bin", b58(hash))), bytes);
}
}
/// A revision by content hash — the local cache first, the network after —
/// verified against the owner's key either way.
pub(crate) async fn revision_by_hash(
app: &tauri::AppHandle,
owner: &[u8; 32],
hash: &[u8; 32],
) -> Result<(pl::ManifestV1, Vec<u8>), String> {
let cached = revisions_dir(app).and_then(|d| std::fs::read(d.join(format!("{}.bin", b58(hash)))).ok());
let bytes = match cached {
Some(b) => b,
None => {
let adapter = app.state::<AppState>().freenet.clone();
let ids = FreenetAdapter::contract_ids(REV_WASM.to_vec(), &pl::make_rev_params(owner, hash));
let b = adapter.contract_get(&ids.instance, false).await.map_err(|e| e.to_string())?;
cache_revision(app, hash, &b);
b
}
};
let m = pl::validate_revision(owner, hash, &bytes).map_err(|e| format!("revision failed verification: {e}"))?;
Ok((m, bytes))
}
/// The current revision of (owner, uuid): the pointer, then the revision it
/// names. None when the pointer says withdrawn.
pub(crate) async fn current_revision(
app: &tauri::AppHandle,
owner: &[u8; 32],
uuid: &[u8; 16],
) -> Result<Option<(pl::ManifestV1, [u8; 32])>, String> {
let adapter = app.state::<AppState>().freenet.clone();
let ptr_ids = FreenetAdapter::contract_ids(PTR_WASM.to_vec(), &pl::make_ptr_params(owner, uuid));
let pbytes = adapter.contract_get(&ptr_ids.instance, false).await.map_err(|e| {
if e.to_string().contains("not found") {
"no such shared playlist on the network (or not reachable yet)".to_string()
} else {
e.to_string()
}
})?;
let p = pl::validate_pointer(owner, uuid, &pbytes).map_err(|e| format!("pointer failed verification: {e}"))?;
let Some(r) = p.current.filter(|_| !p.withdrawn) else { return Ok(None) };
let (m, _) = revision_by_hash(app, owner, &r.content_hash).await?;
Ok(Some((m, r.content_hash)))
}
/// The local playlist behind a published uuid, when this device published it.
pub(crate) fn published_for_uuid(app: &tauri::AppHandle, uuid_b58: &str) -> Option<Published> {
load(app).published.into_values().find(|p| p.uuid == uuid_b58)
}
/// A verified shared playlist staged through the app's own import path.
pub async fn import(
app: &tauri::AppHandle,