Phase 2b: control-post flow (delete/visibility) + remove BlobDeleteNotice
Replaces two persona-signed direct pushes with CDN-propagated control posts:
a single `VisibilityIntent::Control` post type whose content is a signed
`ControlOp` the receiver verifies and applies. Deletes and visibility updates
now flow through the same neighbor-manifest CDN path as regular content — no
direct recipient push needed for persona-signed ops.
Core pieces:
- `VisibilityIntent::Control` + `VisibilityIntent::Profile` variants.
- `ControlOp::DeletePost` / `ControlOp::UpdateVisibility` (JSON, ed25519-signed
by the target post's author over op-specific byte strings).
- `crypto::{sign,verify}_control_{delete,visibility}` signing primitives.
- `control::build_delete_control_post` + `build_visibility_control_post`
for authors to construct control posts.
- `control::receive_post` — unified incoming-post path used by all 6 receive
sites. Verifies control signatures BEFORE storing, so bogus controls never
enter storage and can't be re-propagated via neighbor-manifest diffs.
- `control::apply_control_post_if_applicable` — executes the op under the
same storage guard as the insert.
Feed filter:
- Feeds (`get_feed`, `get_feed_page`, `list_posts_page`,
`list_posts_reverse_chron`) now exclude `Control` and `Profile` posts so
they propagate + tombstone without surfacing.
- Sync/export path (`list_posts_with_visibility`) keeps its own unfiltered
query so control posts still propagate via CDN.
Wire protocol:
- `SyncPost` carries `intent: Option<VisibilityIntent>` so control posts
arrive with their intent preserved.
- `BlobDeleteNotice` (0x95) removed — orphan blobs on remote holders evict
naturally via LRU rather than via a persona-signed push. Code path,
payload, sender, tests, and `delete_blob_with_cdn_notify` all gone.
Tests: control delete roundtrip (apply + tombstone) and wrong-author
rejection (not stored, not applied). 112/112 core tests pass.
This commit is contained in:
parent
4da6a8dc85
commit
36b6a466d2
10 changed files with 585 additions and 196 deletions
|
|
@ -833,7 +833,9 @@ impl Storage {
|
|||
/// All posts, newest first (with visibility)
|
||||
pub fn list_posts_reverse_chron(&self) -> anyhow::Result<Vec<(PostId, Post, PostVisibility)>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, author, content, attachments, timestamp_ms, visibility FROM posts ORDER BY timestamp_ms DESC",
|
||||
"SELECT id, author, content, attachments, timestamp_ms, visibility FROM posts
|
||||
WHERE (visibility_intent IS NULL OR (visibility_intent != '\"Control\"' AND visibility_intent != '\"Profile\"'))
|
||||
ORDER BY timestamp_ms DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let id_bytes: Vec<u8> = row.get(0)?;
|
||||
|
|
@ -869,6 +871,7 @@ impl Storage {
|
|||
"SELECT p.id, p.author, p.content, p.attachments, p.timestamp_ms, p.visibility
|
||||
FROM posts p
|
||||
INNER JOIN follows f ON p.author = f.node_id
|
||||
WHERE (p.visibility_intent IS NULL OR (p.visibility_intent != '\"Control\"' AND p.visibility_intent != '\"Profile\"'))
|
||||
ORDER BY p.timestamp_ms DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
|
|
@ -905,10 +908,12 @@ impl Storage {
|
|||
"SELECT p.id, p.author, p.content, p.attachments, p.timestamp_ms, p.visibility
|
||||
FROM posts p INNER JOIN follows f ON p.author = f.node_id
|
||||
WHERE p.timestamp_ms < ?1
|
||||
AND (p.visibility_intent IS NULL OR (p.visibility_intent != '\"Control\"' AND p.visibility_intent != '\"Profile\"'))
|
||||
ORDER BY p.timestamp_ms DESC LIMIT ?2"
|
||||
} else {
|
||||
"SELECT p.id, p.author, p.content, p.attachments, p.timestamp_ms, p.visibility
|
||||
FROM posts p INNER JOIN follows f ON p.author = f.node_id
|
||||
WHERE (p.visibility_intent IS NULL OR (p.visibility_intent != '\"Control\"' AND p.visibility_intent != '\"Profile\"'))
|
||||
ORDER BY p.timestamp_ms DESC LIMIT ?2"
|
||||
};
|
||||
let mut stmt = self.conn.prepare(sql)?;
|
||||
|
|
@ -924,11 +929,15 @@ impl Storage {
|
|||
pub fn list_posts_page(&self, before_ms: Option<u64>, limit: usize) -> anyhow::Result<Vec<(PostId, Post, PostVisibility)>> {
|
||||
let sql = if before_ms.is_some() {
|
||||
"SELECT id, author, content, attachments, timestamp_ms, visibility
|
||||
FROM posts WHERE timestamp_ms < ?1
|
||||
FROM posts
|
||||
WHERE timestamp_ms < ?1
|
||||
AND (visibility_intent IS NULL OR (visibility_intent != '\"Control\"' AND visibility_intent != '\"Profile\"'))
|
||||
ORDER BY timestamp_ms DESC LIMIT ?2"
|
||||
} else {
|
||||
"SELECT id, author, content, attachments, timestamp_ms, visibility
|
||||
FROM posts ORDER BY timestamp_ms DESC LIMIT ?2"
|
||||
FROM posts
|
||||
WHERE (visibility_intent IS NULL OR (visibility_intent != '\"Control\"' AND visibility_intent != '\"Profile\"'))
|
||||
ORDER BY timestamp_ms DESC LIMIT ?2"
|
||||
};
|
||||
let mut stmt = self.conn.prepare(sql)?;
|
||||
let rows = if let Some(bms) = before_ms {
|
||||
|
|
@ -1051,9 +1060,39 @@ impl Storage {
|
|||
Ok(posts)
|
||||
}
|
||||
|
||||
/// All posts with visibility (for sync protocol)
|
||||
/// All posts with visibility (for sync protocol and export).
|
||||
/// Includes control/profile posts — they need to propagate through the
|
||||
/// CDN like any other post.
|
||||
pub fn list_posts_with_visibility(&self) -> anyhow::Result<Vec<(PostId, Post, PostVisibility)>> {
|
||||
self.list_posts_reverse_chron()
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, author, content, attachments, timestamp_ms, visibility FROM posts ORDER BY timestamp_ms DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let id_bytes: Vec<u8> = row.get(0)?;
|
||||
let author_bytes: Vec<u8> = row.get(1)?;
|
||||
let content: String = row.get(2)?;
|
||||
let attachments_json: String = row.get(3)?;
|
||||
let timestamp_ms: i64 = row.get(4)?;
|
||||
let vis_json: String = row.get(5)?;
|
||||
Ok((id_bytes, author_bytes, content, attachments_json, timestamp_ms, vis_json))
|
||||
})?;
|
||||
let mut posts = Vec::new();
|
||||
for row in rows {
|
||||
let (id_bytes, author_bytes, content, attachments_json, timestamp_ms, vis_json) = row?;
|
||||
let attachments: Vec<Attachment> = serde_json::from_str(&attachments_json).unwrap_or_default();
|
||||
let visibility: PostVisibility = serde_json::from_str(&vis_json).unwrap_or_default();
|
||||
posts.push((
|
||||
blob_to_postid(id_bytes)?,
|
||||
Post {
|
||||
author: blob_to_nodeid(author_bytes)?,
|
||||
content,
|
||||
attachments,
|
||||
timestamp_ms: timestamp_ms as u64,
|
||||
},
|
||||
visibility,
|
||||
));
|
||||
}
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
// ---- Follows ----
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue