Phase 2c: remove audience + PostPush + PostNotification + AudienceRequest/Response

v0.6.2 wire fork: every persona-identifying direct push is gone. Public posts
propagate only through the CDN (pull + header-diff neighbor propagation).
Encrypted posts propagate only through pull with merged author-or-recipient
match. There is no remaining sender→recipient traffic correlation signal on
the wire for content.

Protocol (network-breaking):
- Retire MessageType 0x42 (PostNotification), 0x43 (PostPush),
  0x44 (AudienceRequest), 0x45 (AudienceResponse). Their payload structs are
  deleted along with the handlers and senders.
- SocialDisconnectNotice (0x71) / SocialAddressUpdate (0x70) sender
  functions targeting audience are deleted; the existing handlers stay
  (both already dead code on the send side).

Core removals:
- `push_to_audience`, `notify_post`, `push_delete`,
  `push_disconnect_to_audience`, `push_address_update_to_audience`,
  `send_audience_request`, `send_audience_response`, `send_to_audience` —
  all gone from network.rs.
- `handle_post_notification` removed from connection.rs.
- `request_audience`, `approve_audience`, `deny_audience`,
  `remove_audience`, `list_audience_members`, `list_audience` removed from
  Node.
- `audience_pushed` step removed from post creation.
- `AudienceDirection`, `AudienceStatus`, `AudienceRecord`,
  `AudienceApprovalMode` removed from types.
- Storage: `store_audience`, `list_audience`, `list_audience_members`,
  `remove_audience`, `row_to_audience_record`, `audience_crud` test, the
  `audience` CREATE TABLE, and the audience-dependent social route rebuild
  branch all removed. Upgraded DBs retain the orphan `audience` table;
  nothing touches it.

Follow-on cleanups:
- `SocialRelation::Audience` + `::Mutual` collapsed into just `Follow`.
  The Display/FromStr impl accepts legacy "audience"/"mutual" strings from
  pre-v0.6.2 DBs and maps them to Follow.
- Blob-eviction priority function drops the audience factor; relationship
  is now own-author vs followed vs other. Tests updated accordingly.
- `CommentPermission::AudienceOnly` → `FollowersOnly`. Check uses the
  author's public follows (`list_public_follows`) rather than a separate
  audience table. `ModerationMode::AudienceOnly` similarly renamed.
- Follow/unfollow routines simplified: no audience downgrade logic;
  unfollow removes the social route entirely.

UI:
- CLI: `audience*` commands removed.
- Tauri: `AudienceDto`, `list_audience`, `list_audience_outbound`,
  `request_audience`, `approve_audience`, `remove_audience` commands
  removed from invoke_handler. Frontend: audience panel and audience/mutual
  badges removed; compose permission dropdown shows "Followers" instead of
  "Audience"; `loadAudience` is a no-op stub that hides any leftover DOM.

Tests: 111 / 111 core tests pass.

Breaking change: v0.6.2 nodes won't interoperate with v0.6.1 for delete
propagation, visibility updates, direct post push, post notifications, or
audience requests. Upgrade both ends.
This commit is contained in:
Scott Reimers 2026-04-22 22:20:02 -04:00
parent 36b6a466d2
commit eabdb7ba4f
10 changed files with 98 additions and 1140 deletions

View file

@ -12,10 +12,10 @@ use crate::crypto;
use crate::network::Network;
use crate::storage::StoragePool;
use crate::types::{
Attachment, AudienceDirection, AudienceRecord, AudienceStatus, Circle, DeleteRecord,
Attachment, Circle,
DeviceProfile, DeviceRole, NodeId, PeerRecord, PeerSlotKind, PeerWithAddress, Post, PostId,
PostVisibility, PublicProfile, ReachMethod, RevocationMode, SessionReachMethod, SocialRelation,
SocialRouteEntry, SocialStatus, VisibilityIntent, VisibilityUpdate, WormResult,
SocialRouteEntry, SocialStatus, VisibilityIntent, WormResult,
};
/// Built-in default anchor — always available as a bootstrap fallback.
@ -1002,12 +1002,10 @@ impl Node {
}
}
// For public posts, push to audience members. Encrypted posts propagate
// via the CDN (ManifestPush + header-diff) to eliminate the sender→recipient
// traffic signal.
let audience_pushed = self.network.push_to_audience(&post_id, &post, &visibility).await;
info!(post_id = hex::encode(post_id), audience_pushed, "Created new post");
// v0.6.2: posts propagate ONLY via the CDN (pull + header-diff
// neighbor propagation). Persona-signed direct pushes (PostPush,
// PostNotification) are gone — they exposed sender→recipient traffic.
info!(post_id = hex::encode(post_id), "Created new post");
Ok((post_id, post, visibility))
}
@ -1186,9 +1184,7 @@ impl Node {
let storage = self.storage.get().await;
storage.add_follow(node_id)?;
// Upsert social route
let is_audience = storage.list_audience_members()?.contains(node_id);
let relation = if is_audience { SocialRelation::Mutual } else { SocialRelation::Follow };
// Upsert social route. v0.6.2: audience removed; only Follow exists.
let addresses = storage.get_peer_record(node_id)?
.map(|r| r.addresses).unwrap_or_default();
let peer_addresses = storage.build_peer_addresses_for(node_id)?;
@ -1199,7 +1195,7 @@ impl Node {
node_id: *node_id,
addresses,
peer_addresses,
relation,
relation: SocialRelation::Follow,
status: if connected { SocialStatus::Online } else { SocialStatus::Disconnected },
last_connected_ms: 0,
last_seen_ms: now,
@ -1213,19 +1209,8 @@ impl Node {
pub async fn unfollow(&self, node_id: &NodeId) -> anyhow::Result<()> {
let storage = self.storage.get().await;
storage.remove_follow(node_id)?;
// Downgrade or remove social route
let is_audience = storage.list_audience_members()?.contains(node_id);
if is_audience {
// Downgrade from Mutual to Audience
if let Some(mut route) = storage.get_social_route(node_id)? {
route.relation = SocialRelation::Audience;
storage.upsert_social_route(&route)?;
}
} else {
storage.remove_social_route(node_id)?;
}
// v0.6.2: audience removed; unfollow drops the social route entirely.
storage.remove_social_route(node_id)?;
Ok(())
}
@ -2086,12 +2071,11 @@ impl Node {
let staleness_ms = 3600 * 1000;
let (candidates, follows, audience_members) = {
let (candidates, follows) = {
let storage = self.storage.get().await;
let candidates = storage.get_eviction_candidates(staleness_ms)?;
let follows = storage.list_follows().unwrap_or_default();
let audience = storage.list_audience_members().unwrap_or_default();
(candidates, follows, audience)
(candidates, follows)
};
if candidates.is_empty() {
@ -2111,7 +2095,7 @@ impl Node {
let mut min_priority = f64::MAX;
let mut min_created_at = u64::MAX;
for c in &non_elevated {
let priority = self.compute_blob_priority(c, &follows, &audience_members, now);
let priority = self.compute_blob_priority(c, &follows, now);
if priority < min_priority {
min_priority = priority;
min_created_at = c.created_at;
@ -3416,98 +3400,6 @@ impl Node {
storage.list_social_routes()
}
// ---- Audience ----
pub async fn request_audience(&self, node_id: &NodeId) -> anyhow::Result<()> {
{
let storage = self.storage.get().await;
storage.store_audience(node_id, AudienceDirection::Outbound, AudienceStatus::Pending)?;
}
// Send the request (persistent if available, ephemeral otherwise)
if let Err(e) = self.network.send_audience_request(node_id).await {
warn!(peer = hex::encode(node_id), error = %e, "Failed to send audience request");
}
info!(peer = hex::encode(node_id), "Requested audience membership");
Ok(())
}
pub async fn approve_audience(&self, node_id: &NodeId) -> anyhow::Result<()> {
let connected = self.network.is_connected(node_id).await;
{
let storage = self.storage.get().await;
storage.store_audience(node_id, AudienceDirection::Inbound, AudienceStatus::Approved)?;
// Upsert social route (Audience or Mutual)
let is_follow = storage.list_follows()?.contains(node_id);
let relation = if is_follow { SocialRelation::Mutual } else { SocialRelation::Audience };
let addresses = storage.get_peer_record(node_id)?
.map(|r| r.addresses).unwrap_or_default();
let peer_addresses = storage.build_peer_addresses_for(node_id)?;
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default().as_millis() as u64;
let preferred_tree = storage.build_preferred_tree_for(node_id).unwrap_or_default();
storage.upsert_social_route(&SocialRouteEntry {
node_id: *node_id,
addresses,
peer_addresses,
relation,
status: if connected { SocialStatus::Online } else { SocialStatus::Disconnected },
last_connected_ms: 0,
last_seen_ms: now,
reach_method: ReachMethod::Direct,
preferred_tree,
})?;
}
// Send approval response (persistent if available, ephemeral otherwise)
if let Err(e) = self.network.send_audience_response(node_id, true).await {
warn!(peer = hex::encode(node_id), error = %e, "Failed to send audience approval");
}
info!(peer = hex::encode(node_id), "Approved audience request");
Ok(())
}
pub async fn deny_audience(&self, node_id: &NodeId) -> anyhow::Result<()> {
let storage = self.storage.get().await;
storage.store_audience(node_id, AudienceDirection::Inbound, AudienceStatus::Denied)?;
Ok(())
}
pub async fn remove_audience(&self, node_id: &NodeId) -> anyhow::Result<()> {
let storage = self.storage.get().await;
storage.remove_audience(node_id, AudienceDirection::Inbound)?;
// Downgrade or remove social route
let is_follow = storage.list_follows()?.contains(node_id);
if is_follow {
if let Some(mut route) = storage.get_social_route(node_id)? {
route.relation = SocialRelation::Follow;
storage.upsert_social_route(&route)?;
}
} else {
storage.remove_social_route(node_id)?;
}
Ok(())
}
pub async fn list_audience_members(&self) -> anyhow::Result<Vec<NodeId>> {
let storage = self.storage.get().await;
storage.list_audience_members()
}
pub async fn list_audience(
&self,
direction: AudienceDirection,
status: Option<AudienceStatus>,
) -> anyhow::Result<Vec<AudienceRecord>> {
let storage = self.storage.get().await;
storage.list_audience(direction, status)
}
// ---- Blob Eviction ----
/// Compute priority score for a blob. Higher score = keep longer.
@ -3515,10 +3407,9 @@ impl Node {
&self,
candidate: &crate::storage::EvictionCandidate,
follows: &[NodeId],
audience_members: &[NodeId],
now_ms: u64,
) -> f64 {
compute_blob_priority_standalone(candidate, &self.node_id, follows, audience_members, now_ms)
compute_blob_priority_standalone(candidate, &self.node_id, follows, now_ms)
}
/// Delete a blob locally. BlobDeleteNotice was removed in v0.6.2; remote
@ -3552,18 +3443,17 @@ impl Node {
// 1-hour staleness for replica counts
let staleness_ms = 3600 * 1000;
let (candidates, follows, audience_members) = {
let (candidates, follows) = {
let storage = self.storage.get().await;
let candidates = storage.get_eviction_candidates(staleness_ms)?;
let follows = storage.list_follows().unwrap_or_default();
let audience = storage.list_audience_members().unwrap_or_default();
(candidates, follows, audience)
(candidates, follows)
};
// Score and sort ascending (lowest priority first)
let mut scored: Vec<(f64, &crate::storage::EvictionCandidate)> = candidates
.iter()
.map(|c| (self.compute_blob_priority(c, &follows, &audience_members, now), c))
.map(|c| (self.compute_blob_priority(c, &follows, now), c))
.collect();
scored.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
@ -4455,7 +4345,6 @@ pub fn compute_blob_priority_standalone(
candidate: &crate::storage::EvictionCandidate,
our_node_id: &NodeId,
follows: &[NodeId],
audience_members: &[NodeId],
now_ms: u64,
) -> f64 {
let pin_boost = if candidate.pinned { 1000.0 } else { 0.0 };
@ -4470,14 +4359,11 @@ pub fn compute_blob_priority_standalone(
0.0
};
// v0.6.2: audience removed. Relationship is author-of-ours vs followed vs other.
let relationship = if candidate.author == *our_node_id {
5.0
} else if follows.contains(&candidate.author) && audience_members.contains(&candidate.author) {
3.0
} else if follows.contains(&candidate.author) {
2.0
} else if audience_members.contains(&candidate.author) {
1.0
} else {
0.1
};
@ -4669,39 +4555,36 @@ mod tests {
let candidate = make_candidate(our_id, true, now - 86400_000, now, 0);
let score = compute_blob_priority_standalone(
&candidate, &our_id, &[], &[], now,
&candidate, &our_id, &[], now,
);
// Should be 1000 + (5.0 * 1.0 * ~0.5 * 1.0) = ~1002.5
assert!(score > 1000.0, "own pinned should score >1000, got {}", score);
}
#[test]
fn follow_recent_scores_higher_than_audience_stale() {
fn follow_recent_scores_higher_than_stranger_stale() {
let our_id = make_node_id(1);
let follow_id = make_node_id(2);
let audience_id = make_node_id(3);
let stranger_id = make_node_id(3);
let now = 10_000_000_000u64;
// Follow: recently accessed, 1-day-old post
let follow_candidate = make_candidate(follow_id, false, now - 86400_000, now, 0);
let follow_score = compute_blob_priority_standalone(
&follow_candidate, &our_id, &[follow_id], &[], now,
&follow_candidate, &our_id, &[follow_id], now,
);
// Audience: stale (20 days no access), 10-day-old post, 5 copies
let audience_candidate = make_candidate(
audience_id, false,
let stranger_candidate = make_candidate(
stranger_id, false,
now - 10 * 86400_000,
now - 20 * 86400_000,
5,
);
let audience_score = compute_blob_priority_standalone(
&audience_candidate, &our_id, &[], &[audience_id], now,
let stranger_score = compute_blob_priority_standalone(
&stranger_candidate, &our_id, &[], now,
);
assert!(follow_score > audience_score,
"follow recent ({}) should score higher than audience stale ({})",
follow_score, audience_score);
assert!(follow_score > stranger_score,
"follow recent ({}) should score higher than stranger stale ({})",
follow_score, stranger_score);
}
#[test]
@ -4710,7 +4593,6 @@ mod tests {
let stranger = make_node_id(99);
let now = 10_000_000_000u64;
// Stale stranger post with many copies
let candidate = make_candidate(
stranger, false,
now - 30 * 86400_000,
@ -4718,10 +4600,9 @@ mod tests {
10,
);
let score = compute_blob_priority_standalone(
&candidate, &our_id, &[], &[], now,
&candidate, &our_id, &[], now,
);
// 0.1 relationship * 0.0 heart_recency * ~0.03 freshness / 11 = ~0
assert!(score < 0.01, "stranger stale should score near 0, got {}", score);
}
@ -4729,26 +4610,18 @@ mod tests {
fn priority_ordering() {
let our_id = make_node_id(1);
let follow_id = make_node_id(2);
let audience_id = make_node_id(3);
let stranger_id = make_node_id(4);
let now = 10_000_000_000u64;
// Own pinned (highest)
let own = make_candidate(our_id, true, now - 86400_000, now, 0);
// Follow recent
let follow = make_candidate(follow_id, false, now - 86400_000, now, 0);
// Audience stale
let audience = make_candidate(audience_id, false, now - 10 * 86400_000, now - 20 * 86400_000, 5);
// Stranger
let stranger = make_candidate(stranger_id, false, now - 30 * 86400_000, now - 30 * 86400_000, 10);
let own_score = compute_blob_priority_standalone(&own, &our_id, &[follow_id], &[audience_id], now);
let follow_score = compute_blob_priority_standalone(&follow, &our_id, &[follow_id], &[audience_id], now);
let audience_score = compute_blob_priority_standalone(&audience, &our_id, &[follow_id], &[audience_id], now);
let stranger_score = compute_blob_priority_standalone(&stranger, &our_id, &[follow_id], &[audience_id], now);
let own_score = compute_blob_priority_standalone(&own, &our_id, &[follow_id], now);
let follow_score = compute_blob_priority_standalone(&follow, &our_id, &[follow_id], now);
let stranger_score = compute_blob_priority_standalone(&stranger, &our_id, &[follow_id], now);
assert!(own_score > follow_score, "own ({}) > follow ({})", own_score, follow_score);
assert!(follow_score > audience_score, "follow ({}) > audience ({})", follow_score, audience_score);
assert!(audience_score > stranger_score, "audience ({}) > stranger ({})", audience_score, stranger_score);
assert!(follow_score > stranger_score, "follow ({}) > stranger ({})", follow_score, stranger_score);
}
}