From eabdb7ba4f04ec4f914afb3bb0ef8280a1438ec7 Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Wed, 22 Apr 2026 22:20:02 -0400 Subject: [PATCH] Phase 2c: remove audience + PostPush + PostNotification + AudienceRequest/Response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/cli/src/main.rs | 90 ------------ crates/core/src/connection.rs | 260 ++-------------------------------- crates/core/src/network.rs | 128 ++--------------- crates/core/src/node.rs | 195 +++++-------------------- crates/core/src/protocol.rs | 41 +----- crates/core/src/storage.rs | 219 +++------------------------- crates/core/src/types.rs | 57 ++------ crates/tauri-app/src/lib.rs | 112 +-------------- frontend/app.js | 123 ++-------------- frontend/index.html | 13 +- 10 files changed, 98 insertions(+), 1140 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d52655e..d74eceb 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -138,11 +138,6 @@ async fn main() -> anyhow::Result<()> { println!(" revoke [mode] Revoke access (mode: sync|reencrypt)"); println!(" revoke-circle [m] Revoke circle access for a node"); println!(" redundancy Show replica counts for your posts"); - println!(" audience List audience members"); - println!(" audience-request Request to join peer's audience"); - println!(" audience-pending Show pending audience requests"); - println!(" audience-approve Approve audience request"); - println!(" audience-remove Remove from audience"); println!(" worm Worm lookup (find peer beyond 3-hop map)"); println!(" connections Show mesh connections"); println!(" social-routes Show social routing cache"); @@ -713,91 +708,6 @@ async fn main() -> anyhow::Result<()> { } } - "audience" => { - match node.list_audience_members().await { - Ok(members) => { - if members.is_empty() { - println!("(no audience members)"); - } else { - println!("Audience members ({}):", members.len()); - for nid in members { - let name = node.get_display_name(&nid).await.unwrap_or(None); - let label = name.unwrap_or_else(|| hex::encode(&nid)[..12].to_string()); - println!(" {}", label); - } - } - } - Err(e) => println!("Error: {}", e), - } - } - - "audience-request" => { - if let Some(id_hex) = arg { - match itsgoin_core::parse_node_id_hex(id_hex) { - Ok(nid) => { - match node.request_audience(&nid).await { - Ok(()) => println!("Audience request sent"), - Err(e) => println!("Error: {}", e), - } - } - Err(e) => println!("Invalid node ID: {}", e), - } - } else { - println!("Usage: audience-request "); - } - } - - "audience-pending" => { - use itsgoin_core::types::{AudienceDirection, AudienceStatus}; - match node.list_audience(AudienceDirection::Inbound, Some(AudienceStatus::Pending)).await { - Ok(records) => { - if records.is_empty() { - println!("(no pending audience requests)"); - } else { - println!("Pending audience requests ({}):", records.len()); - for rec in records { - let name = node.get_display_name(&rec.node_id).await.unwrap_or(None); - let label = name.unwrap_or_else(|| hex::encode(&rec.node_id)[..12].to_string()); - println!(" {}", label); - } - } - } - Err(e) => println!("Error: {}", e), - } - } - - "audience-approve" => { - if let Some(id_hex) = arg { - match itsgoin_core::parse_node_id_hex(id_hex) { - Ok(nid) => { - match node.approve_audience(&nid).await { - Ok(()) => println!("Approved audience member"), - Err(e) => println!("Error: {}", e), - } - } - Err(e) => println!("Invalid node ID: {}", e), - } - } else { - println!("Usage: audience-approve "); - } - } - - "audience-remove" => { - if let Some(id_hex) = arg { - match itsgoin_core::parse_node_id_hex(id_hex) { - Ok(nid) => { - match node.remove_audience(&nid).await { - Ok(()) => println!("Removed from audience"), - Err(e) => println!("Error: {}", e), - } - } - Err(e) => println!("Invalid node ID: {}", e), - } - } else { - println!("Usage: audience-remove "); - } - } - "worm" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { diff --git a/crates/core/src/connection.rs b/crates/core/src/connection.rs index 9d9cc79..f764b1a 100644 --- a/crates/core/src/connection.rs +++ b/crates/core/src/connection.rs @@ -13,12 +13,11 @@ use crate::crypto; use crate::protocol::{ read_message_type, read_payload, write_typed_message, AnchorReferral, AnchorReferralRequestPayload, AnchorReferralResponsePayload, AnchorRegisterPayload, - AudienceRequestPayload, AudienceResponsePayload, BlobHeaderDiffPayload, + BlobHeaderDiffPayload, BlobHeaderRequestPayload, BlobHeaderResponsePayload, BlobRequestPayload, BlobResponsePayload, CircleProfileUpdatePayload, GroupKeyDistributePayload, GroupKeyRequestPayload, GroupKeyResponsePayload, InitialExchangePayload, MeshPreferPayload, MessageType, NodeListUpdatePayload, PostDownstreamRegisterPayload, - PostNotificationPayload, PostPushPayload, ProfileUpdatePayload, PullSyncRequestPayload, PullSyncResponsePayload, RefuseRedirectPayload, RelayIntroducePayload, RelayIntroduceResultPayload, SessionRelayPayload, SocialAddressUpdatePayload, SocialCheckinPayload, SocialDisconnectNoticePayload, @@ -1894,138 +1893,6 @@ impl ConnectionManager { sent } - /// Handle an incoming post notification: if we follow the author, pull the post. - /// `conn` is a fallback connection for ephemeral callers (not persistently connected). - pub async fn handle_post_notification( - &self, - from: &NodeId, - notification: PostNotificationPayload, - conn: Option<&iroh::endpoint::Connection>, - ) -> anyhow::Result { - let dominated = { - let storage = self.storage.get().await; - // Already have this post? - if storage.get_post(¬ification.post_id)?.is_some() { - return Ok(false); - } - // Do we follow the author? - let follows = storage.list_follows()?; - follows.contains(¬ification.author) - }; - - if !dominated { - return Ok(false); - } - - // We follow the author and don't have the post — pull it from the notifier - let pull_conn = match self.connections.get(from) { - Some(pc) => pc.connection.clone(), - None => match conn { - Some(c) => c.clone(), - None => return Ok(false), - }, - }; - - let (our_follows, follows_sync, our_personas) = { - let storage = self.storage.get().await; - ( - storage.list_follows()?, - storage.get_follows_with_last_sync().unwrap_or_default(), - storage.list_posting_identities().unwrap_or_default(), - ) - }; - - // Merged pull: include every posting identity so DMs match recipient. - let mut query_list = our_follows; - for pi in &our_personas { - if !query_list.contains(&pi.node_id) { - query_list.push(pi.node_id); - } - } - - let (mut send, mut recv) = pull_conn.open_bi().await?; - let request = PullSyncRequestPayload { - follows: query_list, - have_post_ids: vec![], // v4: empty, using since_ms instead - since_ms: follows_sync, - }; - write_typed_message(&mut send, MessageType::PullSyncRequest, &request).await?; - send.finish()?; - - let _resp_type = read_message_type(&mut recv).await?; - let response: PullSyncResponsePayload = - read_payload(&mut recv, MAX_PAYLOAD).await?; - - let now_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - let mut stored = false; - let mut new_post_ids: Vec = Vec::new(); - let mut synced_authors: HashSet = HashSet::new(); - - // Brief lock 1: store posts - { - let storage = self.storage.get().await; - for sp in &response.posts { - if verify_post_id(&sp.id, &sp.post) && !storage.is_deleted(&sp.id)? { - match crate::control::receive_post(&storage, &sp.id, &sp.post, &sp.visibility, sp.intent.as_ref()) { - Ok(_) => { - new_post_ids.push(sp.id); - synced_authors.insert(sp.post.author); - if sp.id == notification.post_id { - stored = true; - } - } - Err(e) => { - warn!(post_id = hex::encode(sp.id), error = %e, "rejecting post"); - } - } - } - } - } - // Lock RELEASED - - // Brief lock 2: upstream + last_sync + visibility updates - { - let storage = self.storage.get().await; - for pid in &new_post_ids { - let _ = storage.touch_file_holder( - pid, - from, - &[], - crate::storage::HolderDirection::Received, - ); - } - for author in &synced_authors { - let _ = storage.update_follow_last_sync(author, now_ms); - } - for vu in &response.visibility_updates { - if let Some(post) = storage.get_post(&vu.post_id)? { - if post.author == vu.author { - let _ = storage.update_post_visibility(&vu.post_id, &vu.visibility); - } - } - } - } - - // Register as downstream for new posts (cap at 50 to avoid flooding) - if !new_post_ids.is_empty() { - let reg_conn = pull_conn.clone(); - tokio::spawn(async move { - for post_id in new_post_ids.into_iter().take(50) { - let payload = PostDownstreamRegisterPayload { post_id }; - if let Ok(mut send) = reg_conn.open_uni().await { - let _ = write_typed_message(&mut send, MessageType::PostDownstreamRegister, &payload).await; - let _ = send.finish(); - } - } - }); - } - - Ok(stored) - } - /// Pull posts from a connected peer. pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result { let pc = self @@ -4987,110 +4854,6 @@ impl ConnectionManager { } } } - MessageType::PostNotification => { - let notification: PostNotificationPayload = - read_payload(recv, MAX_PAYLOAD).await?; - info!( - peer = hex::encode(remote_node_id), - post_id = hex::encode(notification.post_id), - author = hex::encode(notification.author), - "Received post notification" - ); - let cm = conn_mgr.lock().await; - match cm.handle_post_notification(&remote_node_id, notification, None).await { - Ok(true) => { - info!(peer = hex::encode(remote_node_id), "Pulled post from notification"); - } - Ok(false) => { - info!(peer = hex::encode(remote_node_id), "Post notification ignored (not following or already have)"); - } - Err(e) => { - warn!(peer = hex::encode(remote_node_id), error = %e, "Post notification pull failed"); - } - } - } - MessageType::PostPush => { - let push: PostPushPayload = read_payload(recv, MAX_PAYLOAD).await?; - // Encrypted posts are no longer accepted via direct push — they propagate - // via the CDN to eliminate the sender→recipient traffic signal. - if !matches!(push.post.visibility, crate::types::PostVisibility::Public) { - debug!( - peer = hex::encode(remote_node_id), - post_id = hex::encode(push.post.id), - "Ignoring non-public PostPush" - ); - } else { - let cm = conn_mgr.lock().await; - let storage = cm.storage.get().await; - if !storage.is_deleted(&push.post.id)? - && storage.get_post(&push.post.id)?.is_none() - && crate::content::verify_post_id(&push.post.id, &push.post.post) - { - match crate::control::receive_post( - &storage, - &push.post.id, - &push.post.post, - &push.post.visibility, - push.post.intent.as_ref(), - ) { - Ok(_) => { - let _ = storage.touch_file_holder( - &push.post.id, - &remote_node_id, - &[], - crate::storage::HolderDirection::Received, - ); - info!( - peer = hex::encode(remote_node_id), - post_id = hex::encode(push.post.id), - "Received direct post push" - ); - } - Err(e) => { - warn!(post_id = hex::encode(push.post.id), error = %e, "rejecting pushed post"); - } - } - } - } - } - MessageType::AudienceRequest => { - let req: AudienceRequestPayload = read_payload(recv, MAX_PAYLOAD).await?; - info!( - peer = hex::encode(remote_node_id), - requester = hex::encode(req.requester), - "Received audience request" - ); - let cm = conn_mgr.lock().await; - let storage = cm.storage.get().await; - // Store as inbound pending request - let _ = storage.store_audience( - &req.requester, - crate::types::AudienceDirection::Inbound, - crate::types::AudienceStatus::Pending, - ); - } - MessageType::AudienceResponse => { - let resp: AudienceResponsePayload = read_payload(recv, MAX_PAYLOAD).await?; - let status = if resp.approved { "approved" } else { "denied" }; - info!( - peer = hex::encode(remote_node_id), - responder = hex::encode(resp.responder), - status, - "Received audience response" - ); - let cm = conn_mgr.lock().await; - let storage = cm.storage.get().await; - let new_status = if resp.approved { - crate::types::AudienceStatus::Approved - } else { - crate::types::AudienceStatus::Denied - }; - let _ = storage.store_audience( - &resp.responder, - crate::types::AudienceDirection::Outbound, - new_status, - ); - } MessageType::SocialAddressUpdate => { let payload: SocialAddressUpdatePayload = read_payload(recv, MAX_PAYLOAD).await?; let cm = conn_mgr.lock().await; @@ -6280,18 +6043,18 @@ impl ConnectionManager { async fn handle_blob_header_diff(&self, payload: BlobHeaderDiffPayload, sender: NodeId) { use crate::types::BlobHeaderDiffOp; - // Gather policy + audience data + holders, then drop lock immediately. + // Gather policy + followers set + holders, then drop lock immediately. // Remote peer clearly holds this post — record them as a holder. - let (policy, approved_audience, holders) = { + // v0.6.2: `AudienceOnly` → `FollowersOnly`; checked against our public + // follows list rather than a separate audience table. + let (policy, followers_set, holders) = { let storage = self.storage.get().await; let policy = storage.get_comment_policy(&payload.post_id) .ok() .flatten() .unwrap_or_default(); - let approved = storage.list_audience( - crate::types::AudienceDirection::Inbound, - Some(crate::types::AudienceStatus::Approved), - ).unwrap_or_default(); + let follows: std::collections::HashSet = + storage.list_public_follows().unwrap_or_default().into_iter().collect(); let _ = storage.touch_file_holder( &payload.post_id, &sender, @@ -6303,12 +6066,9 @@ impl ConnectionManager { .into_iter() .map(|(nid, _addrs)| nid) .collect(); - (policy, approved, holders) + (policy, follows, holders) }; - // Filter ops using gathered data (no lock held) - let audience_set: std::collections::HashSet = approved_audience.iter().map(|a| a.node_id).collect(); - // Apply ops in a short lock acquisition { let storage = self.storage.get().await; @@ -6344,8 +6104,8 @@ impl ConnectionManager { } match policy.allow_comments { crate::types::CommentPermission::None => continue, - crate::types::CommentPermission::AudienceOnly => { - if !audience_set.contains(&comment.author) { + crate::types::CommentPermission::FollowersOnly => { + if !followers_set.contains(&comment.author) { continue; } } diff --git a/crates/core/src/network.rs b/crates/core/src/network.rs index 5b0e9bd..4b54188 100644 --- a/crates/core/src/network.rs +++ b/crates/core/src/network.rs @@ -12,15 +12,14 @@ use crate::blob::BlobStore; use crate::connection::{initial_exchange_accept, initial_exchange_connect, ConnHandle, ConnectionActor, ConnectionManager, ExchangeResult}; use crate::content::verify_post_id; use crate::protocol::{ - read_message_type, read_payload, write_typed_message, AudienceRequestPayload, - AudienceResponsePayload, BlobRequestPayload, BlobResponsePayload, DeleteRecordPayload, - MessageType, PostNotificationPayload, PostPushPayload, ProfileUpdatePayload, + read_message_type, read_payload, write_typed_message, BlobRequestPayload, BlobResponsePayload, + MessageType, ProfileUpdatePayload, PullSyncRequestPayload, PullSyncResponsePayload, RefuseRedirectPayload, - SocialAddressUpdatePayload, SocialDisconnectNoticePayload, SyncPost, ALPN_V2, + ALPN_V2, }; use crate::storage::StoragePool; use crate::types::{ - DeleteRecord, DeviceProfile, DeviceRole, NodeId, PeerSlotKind, PeerWithAddress, Post, PostId, + DeviceProfile, DeviceRole, NodeId, PeerSlotKind, Post, PostId, PostVisibility, PublicProfile, SessionReachMethod, WormResult, }; @@ -893,16 +892,7 @@ impl Network { Ok(sent) } - /// Send a post notification to all audience members (ephemeral-capable). - pub async fn notify_post(&self, post_id: &crate::types::PostId, author: &NodeId) -> usize { - let payload = PostNotificationPayload { - post_id: *post_id, - author: *author, - }; - self.send_to_audience(MessageType::PostNotification, &payload).await - } - - /// Push a profile update to all audience members (ephemeral-capable). +/// Push a profile update to all audience members (ephemeral-capable). pub async fn push_profile(&self, profile: &PublicProfile) -> usize { // v0.6.1: profiles broadcast on the wire are keyed by the network // NodeId. They carry ONLY routing metadata (anchors, recent_peers, @@ -959,38 +949,7 @@ impl Network { sent } - /// Push a delete record to all audience members (ephemeral-capable). - pub async fn push_delete(&self, record: &DeleteRecord) -> usize { - let payload = DeleteRecordPayload { - records: vec![record.clone()], - }; - self.send_to_audience(MessageType::DeleteRecord, &payload).await - } - - /// Push a disconnect notice to all audience members (ephemeral-capable). - pub async fn push_disconnect_to_audience(&self, disconnected_peer: &NodeId) -> usize { - let payload = SocialDisconnectNoticePayload { - node_id: *disconnected_peer, - }; - self.send_to_audience(MessageType::SocialDisconnectNotice, &payload).await - } - - /// Push a social address update to all audience members (ephemeral-capable). - pub async fn push_address_update_to_audience( - &self, - node_id: &NodeId, - addresses: &[String], - peer_addresses: &[PeerWithAddress], - ) -> usize { - let payload = SocialAddressUpdatePayload { - node_id: *node_id, - addresses: addresses.to_vec(), - peer_addresses: peer_addresses.to_vec(), - }; - self.send_to_audience(MessageType::SocialAddressUpdate, &payload).await - } - - /// Push a visibility update to all connected peers. +/// Push a visibility update to all connected peers. /// Gets connections snapshot, sends I/O outside the lock. pub async fn push_visibility(&self, update: &crate::types::VisibilityUpdate) -> usize { use crate::protocol::{VisibilityUpdatePayload, write_typed_message, MessageType}; @@ -1074,61 +1033,7 @@ impl Network { } } - /// Send an audience request to a peer (persistent if available, ephemeral otherwise). - pub async fn send_audience_request(&self, target: &NodeId) -> anyhow::Result<()> { - let payload = AudienceRequestPayload { - requester: self.our_node_id, - }; - self.send_to_peer_uni(target, MessageType::AudienceRequest, &payload).await - } - - /// Send an audience response to a peer (persistent if available, ephemeral otherwise). - pub async fn send_audience_response(&self, target: &NodeId, approved: bool) -> anyhow::Result<()> { - let payload = AudienceResponsePayload { - responder: self.our_node_id, - approved, - }; - self.send_to_peer_uni(target, MessageType::AudienceResponse, &payload).await - } - - /// Push a public post to audience members (persistent if available, ephemeral otherwise). - pub async fn push_to_audience( - &self, - post_id: &crate::types::PostId, - post: &Post, - visibility: &PostVisibility, - ) -> usize { - if !matches!(visibility, PostVisibility::Public) { - return 0; - } - - let audience_members: Vec = { - match self.storage.get().await.list_audience_members() { - Ok(m) => m, - Err(_) => return 0, - } - }; - - let payload = PostPushPayload { - post: SyncPost { - id: *post_id, - post: post.clone(), - visibility: visibility.clone(), - intent: None, // PostPush is only for public posts; no intent carried - }, - }; - - let mut pushed = 0; - for member in &audience_members { - if self.send_to_peer_uni(member, MessageType::PostPush, &payload).await.is_ok() { - pushed += 1; - } - } - - pushed - } - - /// Push a group key to a specific peer (uni-stream). +/// Push a group key to a specific peer (uni-stream). pub async fn push_group_key( &self, peer: &NodeId, @@ -1672,24 +1577,7 @@ impl Network { } } - // ---- Audience-targeted + ephemeral helpers ---- - - /// Send a uni-stream message to all audience members (persistent if available, ephemeral otherwise). - async fn send_to_audience(&self, msg_type: MessageType, payload: &T) -> usize { - let audience: Vec = match self.storage.get().await.list_audience_members() { - Ok(m) => m, - Err(_) => return 0, - }; - let mut sent = 0; - for member in &audience { - if self.send_to_peer_uni(member, msg_type, payload).await.is_ok() { - sent += 1; - } - } - sent - } - - /// Pull posts from a peer (persistent if available, ephemeral otherwise). +/// Pull posts from a peer (persistent if available, ephemeral otherwise). pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result { let conn = self.get_connection(peer_id).await?; let (our_follows, follows_sync, our_personas) = { diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index aa423df..f0b4937 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -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> { - let storage = self.storage.get().await; - storage.list_audience_members() - } - - pub async fn list_audience( - &self, - direction: AudienceDirection, - status: Option, - ) -> anyhow::Result> { - 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); } } diff --git a/crates/core/src/protocol.rs b/crates/core/src/protocol.rs index 8d84cd9..fa3f5cf 100644 --- a/crates/core/src/protocol.rs +++ b/crates/core/src/protocol.rs @@ -32,10 +32,9 @@ pub enum MessageType { RefuseRedirect = 0x05, PullSyncRequest = 0x40, PullSyncResponse = 0x41, - PostNotification = 0x42, - PostPush = 0x43, - AudienceRequest = 0x44, - AudienceResponse = 0x45, + // 0x42 (PostNotification), 0x43 (PostPush), 0x44 (AudienceRequest), + // 0x45 (AudienceResponse) retired in v0.6.2: persona-signed direct pushes + // are gone. Public posts propagate via the CDN; encrypted posts via pull. ProfileUpdate = 0x50, DeleteRecord = 0x51, VisibilityUpdate = 0x52, @@ -90,10 +89,6 @@ impl MessageType { 0x05 => Some(Self::RefuseRedirect), 0x40 => Some(Self::PullSyncRequest), 0x41 => Some(Self::PullSyncResponse), - 0x42 => Some(Self::PostNotification), - 0x43 => Some(Self::PostPush), - 0x44 => Some(Self::AudienceRequest), - 0x45 => Some(Self::AudienceResponse), 0x50 => Some(Self::ProfileUpdate), 0x51 => Some(Self::DeleteRecord), 0x52 => Some(Self::VisibilityUpdate), @@ -241,32 +236,6 @@ pub struct VisibilityUpdatePayload { pub updates: Vec, } -/// Post notification: lightweight push when a new post is created -#[derive(Debug, Serialize, Deserialize)] -pub struct PostNotificationPayload { - pub post_id: PostId, - pub author: NodeId, -} - -/// Audience request: ask a peer to join their audience -#[derive(Debug, Serialize, Deserialize)] -pub struct AudienceRequestPayload { - pub requester: NodeId, -} - -/// Audience response: approve or deny an audience request -#[derive(Debug, Serialize, Deserialize)] -pub struct AudienceResponsePayload { - pub responder: NodeId, - pub approved: bool, -} - -/// Post push: full post content pushed directly to a recipient -#[derive(Debug, Serialize, Deserialize)] -pub struct PostPushPayload { - pub post: SyncPost, -} - /// Address resolution request (bi-stream: ask reporter for a hop-2 peer's address) #[derive(Debug, Serialize, Deserialize)] pub struct AddressRequestPayload { @@ -770,10 +739,6 @@ mod tests { MessageType::RefuseRedirect, MessageType::PullSyncRequest, MessageType::PullSyncResponse, - MessageType::PostNotification, - MessageType::PostPush, - MessageType::AudienceRequest, - MessageType::AudienceResponse, MessageType::ProfileUpdate, MessageType::DeleteRecord, MessageType::VisibilityUpdate, diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 68c3f02..a5605c6 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -4,7 +4,7 @@ use std::path::Path; use rusqlite::{params, Connection}; use crate::types::{ - Attachment, AudienceDirection, AudienceRecord, AudienceStatus, Circle, CircleProfile, + Attachment, Circle, CircleProfile, CommentPolicy, DeleteRecord, FollowVisibility, GossipPeerInfo, GroupEpoch, GroupId, GroupKeyRecord, GroupMemberKey, InlineComment, ManifestEntry, NodeId, PeerRecord, PeerSlotKind, PeerWithAddress, Post, PostId, PostVisibility, PostingIdentity, @@ -212,14 +212,8 @@ impl Storage { PRIMARY KEY (peer_id, neighbor_id) ); CREATE INDEX IF NOT EXISTS idx_peer_neighbors_neighbor ON peer_neighbors(neighbor_id); - CREATE TABLE IF NOT EXISTS audience ( - node_id BLOB NOT NULL, - direction TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - requested_at INTEGER NOT NULL, - approved_at INTEGER, - PRIMARY KEY (node_id, direction) - ); + -- v0.6.2: audience table removed. Upgraded DBs still have the + -- orphan table; it's untouched by new code. New DBs don't get it. CREATE TABLE IF NOT EXISTS worm_cooldowns ( target_id BLOB PRIMARY KEY, failed_at INTEGER NOT NULL @@ -2856,111 +2850,6 @@ impl Storage { Ok(count > 0) } - // ---- Audience ---- - - /// Store an audience relationship. - pub fn store_audience( - &self, - node_id: &NodeId, - direction: AudienceDirection, - status: AudienceStatus, - ) -> anyhow::Result<()> { - let now = now_ms(); - let dir_str = match direction { - AudienceDirection::Inbound => "inbound", - AudienceDirection::Outbound => "outbound", - }; - let status_str = match status { - AudienceStatus::Pending => "pending", - AudienceStatus::Approved => "approved", - AudienceStatus::Denied => "denied", - }; - let approved_at = if status == AudienceStatus::Approved { - Some(now) - } else { - None - }; - self.conn.execute( - "INSERT INTO audience (node_id, direction, status, requested_at, approved_at) - VALUES (?1, ?2, ?3, ?4, ?5) - ON CONFLICT(node_id, direction) DO UPDATE SET - status = ?3, approved_at = COALESCE(?5, audience.approved_at)", - params![node_id.as_slice(), dir_str, status_str, now, approved_at], - )?; - Ok(()) - } - - /// Get audience members by direction and status. - pub fn list_audience( - &self, - direction: AudienceDirection, - status: Option, - ) -> anyhow::Result> { - let dir_str = match direction { - AudienceDirection::Inbound => "inbound", - AudienceDirection::Outbound => "outbound", - }; - let (query, bind_status) = match status { - Some(s) => { - let s_str = match s { - AudienceStatus::Pending => "pending", - AudienceStatus::Approved => "approved", - AudienceStatus::Denied => "denied", - }; - ( - "SELECT node_id, direction, status, requested_at, approved_at FROM audience WHERE direction = ?1 AND status = ?2", - Some(s_str), - ) - } - None => ( - "SELECT node_id, direction, status, requested_at, approved_at FROM audience WHERE direction = ?1", - None, - ), - }; - - let mut records = Vec::new(); - if let Some(status_str) = bind_status { - let mut stmt = self.conn.prepare(query)?; - let mut rows = stmt.query(params![dir_str, status_str])?; - while let Some(row) = rows.next()? { - records.push(row_to_audience_record(row)?); - } - } else { - let mut stmt = self.conn.prepare(query)?; - let mut rows = stmt.query(params![dir_str])?; - while let Some(row) = rows.next()? { - records.push(row_to_audience_record(row)?); - } - } - Ok(records) - } - - /// Get approved inbound audience members (nodes we push posts to). - pub fn list_audience_members(&self) -> anyhow::Result> { - let records = self.list_audience( - AudienceDirection::Inbound, - Some(AudienceStatus::Approved), - )?; - Ok(records.into_iter().map(|r| r.node_id).collect()) - } - - /// Remove an audience relationship. - pub fn remove_audience( - &self, - node_id: &NodeId, - direction: AudienceDirection, - ) -> anyhow::Result<()> { - let dir_str = match direction { - AudienceDirection::Inbound => "inbound", - AudienceDirection::Outbound => "outbound", - }; - self.conn.execute( - "DELETE FROM audience WHERE node_id = ?1 AND direction = ?2", - params![node_id.as_slice(), dir_str], - )?; - Ok(()) - } - // ---- Reach: N2/N3 ---- /// Replace a peer's entire N1 set in reachable_n2 (their N1 share → our N2). @@ -3607,32 +3496,18 @@ impl Storage { Ok(count > 0) } - /// Bulk-populate social_routes from follows + audience + peers. + /// Bulk-populate social_routes from follows + peers. /// Returns the number of routes created/updated. pub fn rebuild_social_routes(&self) -> anyhow::Result { let now = now_ms() as u64; let mut count = 0; - // Collect follows + // v0.6.2: audience removed; social routes are built purely from follows. let follows: std::collections::HashSet = self.list_follows()?.into_iter().collect(); - // Collect approved audience members (inbound = they are in our audience) - let audience_members: std::collections::HashSet = - self.list_audience_members()?.into_iter().collect(); - - // Union of all social contacts - let mut all_contacts: std::collections::HashSet = std::collections::HashSet::new(); - all_contacts.extend(&follows); - all_contacts.extend(&audience_members); - - for nid in all_contacts { - let relation = match (follows.contains(&nid), audience_members.contains(&nid)) { - (true, true) => SocialRelation::Mutual, - (true, false) => SocialRelation::Follow, - (false, true) => SocialRelation::Audience, - (false, false) => continue, - }; + for nid in follows { + let relation = SocialRelation::Follow; // Look up addresses from peers table let addresses: Vec = self @@ -4900,30 +4775,6 @@ fn now_ms() -> i64 { .as_millis() as i64 } -fn row_to_audience_record(row: &rusqlite::Row) -> anyhow::Result { - let node_id = blob_to_nodeid(row.get(0)?)?; - let dir_str: String = row.get(1)?; - let status_str: String = row.get(2)?; - let requested_at = row.get::<_, i64>(3)? as u64; - let approved_at: Option = row.get(4)?; - let direction = match dir_str.as_str() { - "inbound" => AudienceDirection::Inbound, - _ => AudienceDirection::Outbound, - }; - let status = match status_str.as_str() { - "approved" => AudienceStatus::Approved, - "denied" => AudienceStatus::Denied, - _ => AudienceStatus::Pending, - }; - Ok(AudienceRecord { - node_id, - direction, - status, - requested_at, - approved_at: approved_at.map(|v| v as u64), - }) -} - fn row_to_peer_record(row: &rusqlite::Row) -> anyhow::Result { let node_id = blob_to_nodeid(row.get(0)?)?; let addrs_json: String = row.get(1)?; @@ -5282,30 +5133,7 @@ mod tests { assert_eq!(s.count_mesh_peers_by_kind(PeerSlotKind::Local).unwrap(), 0); } - #[test] - fn audience_crud() { - use crate::types::{AudienceDirection, AudienceStatus}; - let s = temp_storage(); - let nid = make_node_id(1); - - s.store_audience(&nid, AudienceDirection::Inbound, AudienceStatus::Pending).unwrap(); - let pending = s.list_audience(AudienceDirection::Inbound, Some(AudienceStatus::Pending)).unwrap(); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].status, AudienceStatus::Pending); - - // Approve - s.store_audience(&nid, AudienceDirection::Inbound, AudienceStatus::Approved).unwrap(); - let members = s.list_audience_members().unwrap(); - assert_eq!(members.len(), 1); - assert_eq!(members[0], nid); - - // Remove - s.remove_audience(&nid, AudienceDirection::Inbound).unwrap(); - let members = s.list_audience_members().unwrap(); - assert!(members.is_empty()); - } - - // ---- Social routes tests ---- +// ---- Social routes tests ---- #[test] fn social_route_crud() { @@ -5354,28 +5182,21 @@ mod tests { #[test] fn social_route_rebuild() { - use crate::types::{AudienceDirection, AudienceStatus, SocialRelation}; + use crate::types::SocialRelation; let s = temp_storage(); - let follow_nid = make_node_id(1); - let audience_nid = make_node_id(2); - let mutual_nid = make_node_id(3); + let follow_a = make_node_id(1); + let follow_b = make_node_id(2); - s.add_follow(&follow_nid).unwrap(); - s.add_follow(&mutual_nid).unwrap(); - s.store_audience(&audience_nid, AudienceDirection::Inbound, AudienceStatus::Approved).unwrap(); - s.store_audience(&mutual_nid, AudienceDirection::Inbound, AudienceStatus::Approved).unwrap(); + s.add_follow(&follow_a).unwrap(); + s.add_follow(&follow_b).unwrap(); let count = s.rebuild_social_routes().unwrap(); - assert_eq!(count, 3); + assert_eq!(count, 2); - let follow_route = s.get_social_route(&follow_nid).unwrap().unwrap(); - assert_eq!(follow_route.relation, SocialRelation::Follow); - - let audience_route = s.get_social_route(&audience_nid).unwrap().unwrap(); - assert_eq!(audience_route.relation, SocialRelation::Audience); - - let mutual_route = s.get_social_route(&mutual_nid).unwrap().unwrap(); - assert_eq!(mutual_route.relation, SocialRelation::Mutual); + let route_a = s.get_social_route(&follow_a).unwrap().unwrap(); + assert_eq!(route_a.relation, SocialRelation::Follow); + let route_b = s.get_social_route(&follow_b).unwrap().unwrap(); + assert_eq!(route_b.relation, SocialRelation::Follow); } #[test] @@ -6252,7 +6073,7 @@ mod tests { assert!(s.get_comment_policy(&post_id).unwrap().is_none()); let policy = CommentPolicy { - allow_comments: CommentPermission::AudienceOnly, + allow_comments: CommentPermission::FollowersOnly, allow_reacts: ReactPermission::Public, moderation: ModerationMode::AuthorBlocklist, blocklist: vec![make_node_id(99)], @@ -6260,7 +6081,7 @@ mod tests { s.set_comment_policy(&post_id, &policy).unwrap(); let loaded = s.get_comment_policy(&post_id).unwrap().unwrap(); - assert_eq!(loaded.allow_comments, CommentPermission::AudienceOnly); + assert_eq!(loaded.allow_comments, CommentPermission::FollowersOnly); assert_eq!(loaded.allow_reacts, ReactPermission::Public); assert_eq!(loaded.blocklist.len(), 1); diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 49d8533..5a9c680 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -157,42 +157,6 @@ pub struct WormResult { pub blob_holder: Option, } -/// Audience relationship direction -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum AudienceDirection { - /// They are in our audience (we push to them) - Inbound, - /// We are in their audience (they push to us) - Outbound, -} - -/// Audience membership status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum AudienceStatus { - Pending, - Approved, - Denied, -} - -/// An audience membership record -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AudienceRecord { - pub node_id: NodeId, - pub direction: AudienceDirection, - pub status: AudienceStatus, - pub requested_at: u64, - pub approved_at: Option, -} - -/// Audience approval mode setting -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum AudienceApprovalMode { - /// Auto-accept all audience join requests - PublicApprove, - /// Queue requests for manual review - ApprovalQueue, -} - // --- Encryption / Circles --- /// Circle name (unique per node) @@ -647,20 +611,17 @@ impl std::str::FromStr for ReachMethod { } } -/// Social relationship type +/// Social relationship type. v0.6.2: audience removed; only `Follow` remains. +/// Kept as an enum for forward compatibility (future persona-level relations). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SocialRelation { Follow, - Audience, - Mutual, } impl std::fmt::Display for SocialRelation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SocialRelation::Follow => write!(f, "follow"), - SocialRelation::Audience => write!(f, "audience"), - SocialRelation::Mutual => write!(f, "mutual"), } } } @@ -670,8 +631,8 @@ impl std::str::FromStr for SocialRelation { fn from_str(s: &str) -> Result { match s { "follow" => Ok(SocialRelation::Follow), - "audience" => Ok(SocialRelation::Audience), - "mutual" => Ok(SocialRelation::Mutual), + // Legacy DB values from v0.6.1 and earlier — map to Follow. + "audience" | "mutual" => Ok(SocialRelation::Follow), _ => Err(anyhow::anyhow!("unknown social relation: {}", s)), } } @@ -822,8 +783,9 @@ pub struct InlineComment { pub enum CommentPermission { /// Anyone can comment Public, - /// Only people in author's audience can comment - AudienceOnly, + /// Only people the author follows publicly can comment. + /// Renamed from `AudienceOnly` in v0.6.2 when audience was removed. + FollowersOnly, /// Comments disabled None, } @@ -858,8 +820,9 @@ impl Default for ReactPermission { pub enum ModerationMode { /// Author maintains a blocklist of users AuthorBlocklist, - /// Only audience members can engage - AudienceOnly, + /// Only people the author follows publicly can engage. + /// Renamed from `AudienceOnly` in v0.6.2. + FollowersOnly, } impl Default for ModerationMode { diff --git a/crates/tauri-app/src/lib.rs b/crates/tauri-app/src/lib.rs index f9b8984..154bcf2 100644 --- a/crates/tauri-app/src/lib.rs +++ b/crates/tauri-app/src/lib.rs @@ -1356,111 +1356,6 @@ async fn list_known_anchors(state: State<'_, AppNode>) -> Result, - direction: String, - status: String, - requested_at: u64, - approved_at: Option, -} - -#[tauri::command] -async fn list_audience(state: State<'_, AppNode>) -> Result, String> { - let node = get_node(&state).await; - let records = node - .list_audience( - itsgoin_core::types::AudienceDirection::Inbound, - None, - ) - .await - .map_err(|e| e.to_string())?; - let mut dtos = Vec::with_capacity(records.len()); - for r in &records { - let display_name = node.get_display_name(&r.node_id).await.unwrap_or(None); - let direction = match r.direction { - itsgoin_core::types::AudienceDirection::Inbound => "inbound", - itsgoin_core::types::AudienceDirection::Outbound => "outbound", - }; - let status = match r.status { - itsgoin_core::types::AudienceStatus::Pending => "pending", - itsgoin_core::types::AudienceStatus::Approved => "approved", - itsgoin_core::types::AudienceStatus::Denied => "denied", - }; - dtos.push(AudienceDto { - node_id: hex::encode(r.node_id), - display_name, - direction: direction.to_string(), - status: status.to_string(), - requested_at: r.requested_at, - approved_at: r.approved_at, - }); - } - Ok(dtos) -} - -#[tauri::command] -async fn list_audience_outbound(state: State<'_, AppNode>) -> Result, String> { - let node = get_node(&state).await; - let records = node - .list_audience( - itsgoin_core::types::AudienceDirection::Outbound, - None, - ) - .await - .map_err(|e| e.to_string())?; - let mut dtos = Vec::with_capacity(records.len()); - for r in &records { - let display_name = node.get_display_name(&r.node_id).await.unwrap_or(None); - let status = match r.status { - itsgoin_core::types::AudienceStatus::Pending => "pending", - itsgoin_core::types::AudienceStatus::Approved => "approved", - itsgoin_core::types::AudienceStatus::Denied => "denied", - }; - dtos.push(AudienceDto { - node_id: hex::encode(r.node_id), - display_name, - direction: "outbound".to_string(), - status: status.to_string(), - requested_at: r.requested_at, - approved_at: r.approved_at, - }); - } - Ok(dtos) -} - -#[tauri::command] -async fn request_audience( - state: State<'_, AppNode>, - node_id_hex: String, -) -> Result<(), String> { - let node = get_node(&state).await; - let nid = parse_node_id(&node_id_hex)?; - node.request_audience(&nid).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn approve_audience( - state: State<'_, AppNode>, - node_id_hex: String, -) -> Result<(), String> { - let node = get_node(&state).await; - let nid = parse_node_id(&node_id_hex)?; - node.approve_audience(&nid).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn remove_audience( - state: State<'_, AppNode>, - node_id_hex: String, -) -> Result<(), String> { - let node = get_node(&state).await; - let nid = parse_node_id(&node_id_hex)?; - node.remove_audience(&nid).await.map_err(|e| e.to_string()) -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct WormResultDto { @@ -2401,7 +2296,7 @@ async fn set_comment_policy( let node = get_node(&state).await; let pid = hex_to_postid(&post_id)?; let comment_perm = match allow_comments.as_str() { - "audience_only" => itsgoin_core::types::CommentPermission::AudienceOnly, + "followers_only" | "audience_only" => itsgoin_core::types::CommentPermission::FollowersOnly, "none" => itsgoin_core::types::CommentPermission::None, _ => itsgoin_core::types::CommentPermission::Public, }; @@ -3011,11 +2906,6 @@ pub fn run() { set_anchors, list_anchor_peers, list_known_anchors, - list_audience, - list_audience_outbound, - request_audience, - approve_audience, - remove_audience, list_connections, worm_lookup, list_social_routes, diff --git a/frontend/app.js b/frontend/app.js index 64b8aab..65ad834 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1422,14 +1422,8 @@ async function loadPeerBios(container) { async function loadFollows() { try { - const [follows, outbound, inbound] = await Promise.all([ - invoke('list_follows'), - invoke('list_audience_outbound'), - invoke('list_audience'), - ]); - const outboundSet = new Set(outbound.map(r => r.nodeId)); - const approvedSet = new Set(outbound.filter(r => r.status === 'approved').map(r => r.nodeId)); - const inboundApprovedSet = new Set(inbound.filter(r => r.status === 'approved').map(r => r.nodeId)); + // v0.6.2: audience removed. No more audience/mutual badges or request flow. + const follows = await invoke('list_follows'); // Filter out self before rendering const others = follows.filter(f => f.nodeId !== myNodeId); @@ -1443,34 +1437,21 @@ async function loadFollows() { const label = escapeHtml(peerLabel(f.nodeId, f.displayName)); const isSelf = f.nodeId === myNodeId; - let audienceBadge = ''; - let mutualBadge = ''; let lastSeenHtml = ''; let actions = ''; if (isSelf) { actions = '(you)'; } else { - if (inboundApprovedSet.has(f.nodeId)) { - mutualBadge = 'mutual'; - } - if (approvedSet.has(f.nodeId)) { - audienceBadge = 'audience'; - } else if (outboundSet.has(f.nodeId)) { - audienceBadge = 'requested'; - } if (!f.isOnline && f.lastActivityMs > 0) { lastSeenHtml = `Last online: ${formatTimeAgo(f.lastActivityMs)}`; } - const audienceBtn = !approvedSet.has(f.nodeId) && !outboundSet.has(f.nodeId) - ? `` - : ''; const syncBtn = ``; const msgBtn = ``; const unfollowBtn = ``; - actions = `${audienceBtn} ${syncBtn} ${msgBtn} ${unfollowBtn}`; + actions = `${syncBtn} ${msgBtn} ${unfollowBtn}`; } return `
-
${icon} ${label} ${mutualBadge} ${audienceBadge}
+
${icon} ${label}
${lastSeenHtml ? `
${lastSeenHtml}
` : ''}
${actions}
@@ -1562,22 +1543,6 @@ async function loadFollows() { }); }); - // Attach audience request handlers - followsList.querySelectorAll('.request-audience-btn').forEach(btn => { - btn.addEventListener('click', async () => { - btn.disabled = true; - try { - await invoke('request_audience', { nodeIdHex: btn.dataset.nodeId }); - toast('Audience request sent!'); - loadFollows(); - } catch (e) { - toast('Error: ' + e); - } finally { - btn.disabled = false; - } - }); - }); - // Lazy-load bios loadPeerBios(followsList); } @@ -1702,81 +1667,13 @@ async function loadRedundancy() { } } -// --- Audience management --- +// v0.6.2: audience removed. loadAudience is a no-op kept so existing call +// sites don't break; DOM panels (if still in markup) are hidden. async function loadAudience() { - try { - const records = await invoke('list_audience'); - const pending = records.filter(r => r.status === 'pending'); - const approved = records.filter(r => r.status === 'approved'); - - if (pending.length === 0) { - audiencePendingList.innerHTML = '

No pending requests

'; - } else { - audiencePendingList.innerHTML = pending.map(r => { - const label = escapeHtml(peerLabel(r.nodeId, r.displayName)); - const icon = generateIdenticon(r.nodeId, 18); - return `
-
${icon} ${label}
-
${relativeTime(r.requestedAt)}
-
- - -
-
`; - }).join(''); - - audiencePendingList.querySelectorAll('.approve-audience-btn').forEach(btn => { - btn.addEventListener('click', async () => { - btn.disabled = true; - try { - await invoke('approve_audience', { nodeIdHex: btn.dataset.nodeId }); - toast('Audience approved'); - loadAudience(); - } catch (e) { toast('Error: ' + e); } - }); - }); - audiencePendingList.querySelectorAll('.deny-audience-btn').forEach(btn => { - btn.addEventListener('click', async () => { - btn.disabled = true; - try { - await invoke('remove_audience', { nodeIdHex: btn.dataset.nodeId }); - toast('Audience denied'); - loadAudience(); - } catch (e) { toast('Error: ' + e); } - }); - }); - } - - if (approved.length === 0) { - audienceApprovedList.innerHTML = '

No approved audience members

'; - } else { - audienceApprovedList.innerHTML = approved.map(r => { - const label = escapeHtml(peerLabel(r.nodeId, r.displayName)); - const icon = generateIdenticon(r.nodeId, 18); - return `
-
${icon} ${label}
-
Approved ${r.approvedAt ? relativeTime(r.approvedAt) : ''}
-
- -
-
`; - }).join(''); - - audienceApprovedList.querySelectorAll('.remove-audience-btn').forEach(btn => { - btn.addEventListener('click', async () => { - if (!confirm('Remove this audience member?')) return; - btn.disabled = true; - try { - await invoke('remove_audience', { nodeIdHex: btn.dataset.nodeId }); - toast('Audience member removed'); - loadAudience(); - } catch (e) { toast('Error: ' + e); } - }); - }); - } - } catch (e) { - audiencePendingList.innerHTML = `

Error: ${e}

`; - } + if (audiencePendingList) audiencePendingList.style.display = 'none'; + if (audienceApprovedList) audienceApprovedList.style.display = 'none'; + const headings = document.querySelectorAll('.audience-section, #audience-section'); + headings.forEach(el => { el.style.display = 'none'; }); } // --- Network diagnostics --- diff --git a/frontend/index.html b/frontend/index.html index aede0b9..8abc84b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -96,7 +96,7 @@