Phase 2e: rich comments — optional ref_post_id with signed preview
A comment can now reference a separate Post that carries the full body (long text, attachments, rich formatting). The inline comment's `content` becomes a short preview string; the referenced post propagates through the normal CDN and readers fetch it lazily when rendering the expanded view. Type change: - `InlineComment` gains `ref_post_id: Option<PostId>` (#[serde(default)]). When None, `content` is the full comment text (v0.6.1 shape — unchanged on the wire). When Some, `content` is the preview. Signature binding: - `crypto::sign_comment` / `verify_comment_signature` now take `ref_post_id: Option<&PostId>`. The signed digest appends `b"ref:" || ref_post_id` only when a ref is present, so plain comments produce the same digest as the v0.6.1 scheme and remain verifiable without a migration. When a ref is present the signature binds it, so a peer can't strip or swap the reference without re-signing. Storage: - `comments` table gets a `ref_post_id BLOB` column (nullable). Added to both the CREATE TABLE statement and a conditional ALTER TABLE migration so upgraded DBs pick it up automatically. - `store_comment`, `get_comments`, `get_comments_with_tombstones` read and write the column. Node API: - `comment_on_post` stays as the plain-comment entry point (calls the inner helper with `ref_post_id = None`). - New `comment_on_post_with_ref(post_id, preview, ref_post_id)` for rich comments. Both share a single inner helper that signs, stores, and propagates via BlobHeaderDiff. connection.rs BlobHeaderDiff handler passes `comment.ref_post_id.as_ref()` to the signature verify so forged or rewritten refs are rejected. Tests: new crypto test asserting the signature binds ref_post_id (strip / swap / drop all fail); new storage test asserting ref_post_id roundtrips through live + tombstone reads. 116 / 116 core tests pass. Client-side UX (pulling the ref post on expand, composing rich comments) is frontend work that will land with the next UI iteration.
This commit is contained in:
parent
8b2881d84a
commit
88d5cc9f23
5 changed files with 166 additions and 27 deletions
|
|
@ -3801,11 +3801,36 @@ impl Node {
|
|||
Ok(counts)
|
||||
}
|
||||
|
||||
/// Add a comment to a post (signed with our key).
|
||||
/// Add a plain inline comment to a post (signed with our posting key).
|
||||
/// The comment's `content` is the full text; `ref_post_id` is None.
|
||||
pub async fn comment_on_post(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
content: String,
|
||||
) -> anyhow::Result<crate::types::InlineComment> {
|
||||
self.comment_on_post_inner(post_id, content, None).await
|
||||
}
|
||||
|
||||
/// Add a rich comment: the full body lives in `ref_post_id` (typically a
|
||||
/// newly-created public post by the commenter that carries attachments
|
||||
/// or a long body). The inline `preview` text appears in the parent
|
||||
/// post's header-diff and is what most clients render by default; the
|
||||
/// expanded view fetches the referenced post. Signature binds the
|
||||
/// preview + ref_post_id so a peer can't rewrite either independently.
|
||||
pub async fn comment_on_post_with_ref(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
preview: String,
|
||||
ref_post_id: PostId,
|
||||
) -> anyhow::Result<crate::types::InlineComment> {
|
||||
self.comment_on_post_inner(post_id, preview, Some(ref_post_id)).await
|
||||
}
|
||||
|
||||
async fn comment_on_post_inner(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
content: String,
|
||||
ref_post_id: Option<PostId>,
|
||||
) -> anyhow::Result<crate::types::InlineComment> {
|
||||
let our_node_id = self.default_posting_id;
|
||||
let seed = self.default_posting_secret;
|
||||
|
|
@ -3813,7 +3838,14 @@ impl Node {
|
|||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64;
|
||||
|
||||
let signature = crate::crypto::sign_comment(&seed, &our_node_id, &post_id, &content, now);
|
||||
let signature = crate::crypto::sign_comment(
|
||||
&seed,
|
||||
&our_node_id,
|
||||
&post_id,
|
||||
&content,
|
||||
now,
|
||||
ref_post_id.as_ref(),
|
||||
);
|
||||
|
||||
let comment = crate::types::InlineComment {
|
||||
author: our_node_id,
|
||||
|
|
@ -3822,13 +3854,14 @@ impl Node {
|
|||
timestamp_ms: now,
|
||||
signature,
|
||||
deleted_at: None,
|
||||
ref_post_id,
|
||||
};
|
||||
|
||||
let storage = self.storage.get().await;
|
||||
storage.store_comment(&comment)?;
|
||||
drop(storage);
|
||||
|
||||
// Propagate via BlobHeaderDiff to downstream + upstream
|
||||
// Propagate via BlobHeaderDiff to the target post's known holders.
|
||||
{
|
||||
let network = &self.network;
|
||||
let diff = crate::protocol::BlobHeaderDiffPayload {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue