feat(fof-layer4): supersedes_post_id field on Post
Adds an optional supersedes_post_id pointer to Post for the "re-issue with narrower access" path. Author publishes a new post that references an earlier one; receivers can render "this is a re-issued version of an earlier post" + offer to view the original. Covered by PostId = BLAKE3(Post) — receivers can verify it wasn't forged by anyone but the author. #[serde(default)] for back-compat; existing posts deserialize with None. All existing Post construction sites bulk-updated to set the field to None. Storage::get_post returns None for the field today; a follow-up can add a dedicated column if/when receivers need to render the pointer. The author-side re-issue helper that creates posts with this field set comes in the Tauri/UI slice — the wire shape is in place now. 148 tests pass (no regressions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2f2203331
commit
fdbf97f2d7
11 changed files with 50 additions and 0 deletions
|
|
@ -144,6 +144,7 @@ pub fn build_announcement_post(
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let id1 = compute_post_id(&post);
|
||||
let id2 = compute_post_id(&post);
|
||||
|
|
@ -38,6 +39,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post2 = Post {
|
||||
author: [1u8; 32],
|
||||
|
|
@ -45,6 +47,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
assert_ne!(compute_post_id(&post1), compute_post_id(&post2));
|
||||
}
|
||||
|
|
@ -57,6 +60,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let id = compute_post_id(&post);
|
||||
assert!(verify_post_id(&id, &post));
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ pub fn build_delete_control_post(
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +185,7 @@ pub fn build_visibility_control_post(
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +217,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post_id = crate::content::compute_post_id(&post);
|
||||
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
||||
|
|
@ -244,6 +247,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post_id = crate::content::compute_post_id(&post);
|
||||
s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();
|
||||
|
|
|
|||
|
|
@ -914,6 +914,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 3000,
|
||||
fof_gating: Some(built.gating.clone()),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
// Bob's device unlocks the post via his V_me (= v_x_bob).
|
||||
|
|
@ -995,6 +996,7 @@ mod tests {
|
|||
let post = crate::types::Post {
|
||||
author: alice_id, content: "alice".into(), attachments: vec![],
|
||||
timestamp_ms: 3000, fof_gating: Some(built.gating.clone()),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
s.store_post_with_intent(
|
||||
&post_id, &post,
|
||||
|
|
@ -1085,6 +1087,7 @@ mod tests {
|
|||
let post = crate::types::Post {
|
||||
author: alice_id, content: "alice".into(), attachments: vec![],
|
||||
timestamp_ms: 3000, fof_gating: Some(built.gating.clone()),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
s.store_post_with_intent(
|
||||
&post_id, &post,
|
||||
|
|
@ -1186,6 +1189,7 @@ mod tests {
|
|||
let post = crate::types::Post {
|
||||
author: alice_id, content: "x".into(), attachments: vec![],
|
||||
timestamp_ms: 3000, fof_gating: Some(built.gating.clone()),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
s.store_post_with_intent(
|
||||
&post_id, &post,
|
||||
|
|
@ -1334,6 +1338,7 @@ mod tests {
|
|||
let alice_post = crate::types::Post {
|
||||
author: alice_id, content: String::new(), attachments: vec![],
|
||||
timestamp_ms: 3000, fof_gating: Some(built.gating.clone()),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let bob_unlock = find_unlock_for_post(&bob_storage, &alice_post).unwrap()
|
||||
.expect("Bob can unlock");
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub fn build_distribution_post(
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post_id = compute_post_id(&post);
|
||||
let visibility = PostVisibility::Encrypted { recipients: wrapped_keys };
|
||||
|
|
@ -243,6 +244,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 200,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let forged_vis = PostVisibility::Encrypted { recipients: wrapped };
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ pub async fn import_as_personas(
|
|||
attachments: attachments.clone(),
|
||||
timestamp_ms: ep.timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
// Preserve the original visibility intent from the export.
|
||||
|
|
@ -464,6 +465,7 @@ pub async fn import_public_posts(
|
|||
attachments: attachments.clone(),
|
||||
timestamp_ms: ep.timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
// Read blob data from archive
|
||||
|
|
@ -701,6 +703,7 @@ pub async fn merge_with_key(
|
|||
attachments: attachments.clone(),
|
||||
timestamp_ms: ep.timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
// Read blob data from archive (may need decryption for encrypted posts)
|
||||
|
|
|
|||
|
|
@ -2259,6 +2259,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: 1000,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -828,6 +828,7 @@ impl Node {
|
|||
attachments: vec![],
|
||||
timestamp_ms: pi.created_at,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post_id = crate::content::compute_post_id(&post);
|
||||
{
|
||||
|
|
@ -1168,6 +1169,7 @@ impl Node {
|
|||
attachments: vec![],
|
||||
timestamp_ms: now,
|
||||
fof_gating: Some(built.gating),
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let post_id = crate::content::compute_post_id(&post);
|
||||
|
||||
|
|
@ -1321,6 +1323,7 @@ impl Node {
|
|||
attachments,
|
||||
timestamp_ms: now,
|
||||
fof_gating,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
let post_id = compute_post_id(&post);
|
||||
|
|
@ -3345,6 +3348,7 @@ impl Node {
|
|||
attachments: post.attachments.clone(),
|
||||
timestamp_ms: post.timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let new_post_id = compute_post_id(&new_post);
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ pub fn build_profile_post(
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -465,6 +466,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
|
||||
// Apply. Auto-scan should fire and store the unwrapped V_me.
|
||||
|
|
@ -534,6 +536,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile)).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -966,6 +966,15 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
||||
fof_gating,
|
||||
// FoF Layer 4: supersedes_post_id is not persisted as a
|
||||
// dedicated column today; it would arrive in the JSON
|
||||
// form via fof_gating_json on re-issued posts. For now,
|
||||
// get_post returns None and re-issue UX surfaces it
|
||||
// when present in the in-memory Post (Layer 4 re-issue
|
||||
// helper sets it inline). A follow-up can add a
|
||||
// dedicated column if/when receivers need to render the
|
||||
// supersedes pointer in feeds.
|
||||
supersedes_post_id: None,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
|
@ -993,6 +1002,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: row.get::<_, i64>(3)? as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
)))
|
||||
|
|
@ -1087,6 +1097,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: timestamp_ms as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
|
|
@ -1126,6 +1137,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: timestamp_ms as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
|
|
@ -1289,6 +1301,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: timestamp_ms as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
|
|
@ -1325,6 +1338,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: timestamp_ms as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
|
|
@ -3012,6 +3026,7 @@ impl Storage {
|
|||
attachments,
|
||||
timestamp_ms: row.get::<_, i64>(4)? as u64,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
|
|
@ -6414,6 +6429,7 @@ mod tests {
|
|||
attachments: vec![],
|
||||
timestamp_ms: ts,
|
||||
fof_gating: None,
|
||||
supersedes_post_id: None,
|
||||
};
|
||||
let id = blake3::hash(&serde_json::to_vec(&post).unwrap());
|
||||
s.store_post(id.as_bytes(), &post).unwrap();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@ pub struct Post {
|
|||
/// t=0, not the live mutable state.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fof_gating: Option<FoFCommentGating>,
|
||||
/// FoF Layer 4: optional pointer to a post this one supersedes.
|
||||
/// Used by the "re-issue with narrower access" path (advanced
|
||||
/// rotation). Readers may display "this is a re-issued version
|
||||
/// of an earlier post" + offer to view the original if still
|
||||
/// cached. Covered by PostId since it's part of the signed Post.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub supersedes_post_id: Option<PostId>,
|
||||
}
|
||||
|
||||
/// A reference to a media blob attached to a post
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue