use super::RpcHandler; use crate::content_server::{self, AccessControl, Availability, ContentItem}; use anyhow::{Context, Result}; use tracing::debug; impl RpcHandler { /// List content I'm sharing. pub(super) async fn handle_content_list_mine( &self, ) -> Result { let catalog = content_server::load_catalog(&self.config.data_dir).await?; Ok(serde_json::json!({ "items": catalog.items })) } /// Add content to my catalog. pub(super) async fn handle_content_add( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let filename = params .get("filename") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing filename"))?; let mime_type = params .get("mime_type") .and_then(|v| v.as_str()) .unwrap_or("application/octet-stream"); let description = params .get("description") .and_then(|v| v.as_str()) .unwrap_or(""); let mut item = ContentItem { id: uuid::Uuid::new_v4().to_string(), filename: filename.to_string(), mime_type: mime_type.to_string(), size_bytes: 0, description: description.to_string(), access: AccessControl::Free, availability: Availability::default(), added_at: chrono::Utc::now().to_rfc3339(), }; // Resolve actual file size from disk let file_path = content_server::content_file_path(&self.config.data_dir, &item); if let Ok(metadata) = std::fs::metadata(&file_path) { item.size_bytes = metadata.len(); } content_server::add_item(&self.config.data_dir, item.clone()).await?; Ok(serde_json::json!({ "item": item })) } /// Remove content from my catalog. pub(super) async fn handle_content_remove( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing id"))?; content_server::remove_item(&self.config.data_dir, id).await?; Ok(serde_json::json!({ "removed": true })) } /// Set pricing for a content item. pub(super) async fn handle_content_set_pricing( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing id"))?; let access_type = params .get("access") .and_then(|v| v.as_str()) .unwrap_or("free"); let access = match access_type { "free" => AccessControl::Free, "peers_only" => AccessControl::PeersOnly, "paid" => { let price = params .get("price_sats") .and_then(|v| v.as_u64()) .unwrap_or(0); if price == 0 { return Err(anyhow::anyhow!("Paid content requires price_sats > 0")); } AccessControl::Paid { price_sats: price } } _ => return Err(anyhow::anyhow!("Invalid access type: {}", access_type)), }; content_server::set_access(&self.config.data_dir, id, access).await?; Ok(serde_json::json!({ "updated": true })) } /// Set availability for a content item. pub(super) async fn handle_content_set_availability( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing id"))?; let availability_type = params .get("availability") .and_then(|v| v.as_str()) .unwrap_or("all_peers"); let availability = match availability_type { "nobody" => Availability::Nobody, "all_peers" => Availability::AllPeers, "specific" => { let peers = params .get("peers") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect::>() }) .unwrap_or_default(); Availability::Specific { peers } } _ => return Err(anyhow::anyhow!("Invalid availability: {}", availability_type)), }; content_server::set_availability(&self.config.data_dir, id, availability).await?; Ok(serde_json::json!({ "updated": true })) } /// Browse a peer's content catalog over Tor. pub(super) async fn handle_content_browse_peer( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let onion = params .get("onion") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing onion address"))?; // Validate onion address format if !onion.ends_with(".onion") || onion.len() < 10 { return Err(anyhow::anyhow!("Invalid onion address")); } // Connect via Tor SOCKS proxy to the peer's content catalog endpoint let socks_proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050") .context("Failed to create SOCKS proxy")?; let client = reqwest::Client::builder() .proxy(socks_proxy) .timeout(std::time::Duration::from_secs(30)) .build() .context("Failed to build Tor HTTP client")?; let url = format!("http://{}/content", onion); debug!("Browsing peer content at {}", url); let response = client .get(&url) .send() .await .context("Failed to connect to peer over Tor")?; if !response.status().is_success() { return Err(anyhow::anyhow!( "Peer returned error: {}", response.status() )); } let body: serde_json::Value = response .json() .await .context("Failed to parse peer catalog")?; Ok(body) } }