feat(fof-layer2): wire FoF gating into post-create path

Threads optional fof_gating through create_post_inner so a published
post can carry the author-signed gating snapshot at publish time
(covered by PostId = BLAKE3(Post)).

New public entry point:

  Node::create_post_with_fof_comments(content, attachments)
      -> (PostId, Post, PostVisibility, cek: [u8; 32])

Builds the FoFCommentGating block via fof::build_fof_comment_gating
from the default persona's keyring (own V_me + every received V_x),
then calls create_post_inner with VisibilityIntent::Public (Mode 2
keeps body public). Returns the per-post CEK to the caller for local
caching (decrypting one's own comments later).

Existing create_post / create_post_as / create_post_with_visibility
threads `None` through the new arg — back-compat for non-FoF posts.

CommentPolicy::FriendsOfFriends is NOT published as a SetPolicy diff
in this commit; the post's `fof_gating` field is itself the signal
that the post supports FoF commenting. The four-check CDN verify gate
(next commit) reads fof_gating directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-14 14:01:57 -04:00
parent bdcd2142cd
commit 673f9e2261

View file

@ -1014,6 +1014,7 @@ impl Node {
content,
intent,
attachment_data,
None,
).await
}
@ -1038,9 +1039,43 @@ impl Node {
content,
intent,
attachment_data,
None,
).await
}
/// FoF Layer 2: create a Mode 2 post (public body, FoF-gated
/// comments). Intent is Public; the FoF gating block is built
/// from the default persona's keyring and embedded in
/// `Post.fof_gating`. The author retains the per-post CEK locally
/// for decrypting their own comments later.
///
/// Returns `(post_id, post, visibility, cek)`. `visibility` is
/// always Public for Mode 2.
pub async fn create_post_with_fof_comments(
&self,
content: String,
attachment_data: Vec<(Vec<u8>, String)>,
) -> anyhow::Result<(PostId, Post, PostVisibility, [u8; 32])> {
// Build the gating block from the default persona's keyring.
let built = {
let storage = self.storage.get().await;
crate::fof::build_fof_comment_gating(&*storage, &self.default_posting_id)?
.ok_or_else(|| anyhow::anyhow!(
"default persona has no V_me; rotate or recreate before FoF posts"
))?
};
let cek = built.cek;
let (post_id, post, visibility) = self.create_post_inner(
&self.default_posting_id,
&self.default_posting_secret,
content,
VisibilityIntent::Public,
attachment_data,
Some(built.gating),
).await?;
Ok((post_id, post, visibility, cek))
}
async fn create_post_inner(
&self,
posting_id: &NodeId,
@ -1048,6 +1083,7 @@ impl Node {
content: String,
intent: VisibilityIntent,
attachment_data: Vec<(Vec<u8>, String)>,
fof_gating: Option<crate::types::FoFCommentGating>,
) -> anyhow::Result<(PostId, Post, PostVisibility)> {
// Validate attachments
if attachment_data.len() > 4 {
@ -1163,7 +1199,7 @@ impl Node {
content: final_content,
attachments,
timestamp_ms: now,
fof_gating: None,
fof_gating,
};
let post_id = compute_post_id(&post);