forked from Keelrleek/SoulStream
the proposal board: a Space's pinned playlist takes signed proposals, and an accept republishes it
This commit is contained in:
1 parent
8b511b20e7
commit
bf81792c49
15 files changed
+3207
-10
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user