2026-03-09 07:43:12 +00:00
|
|
|
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<serde_json::Value> {
|
|
|
|
|
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<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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("");
|
|
|
|
|
|
2026-03-13 02:20:55 +00:00
|
|
|
let mut item = ContentItem {
|
2026-03-09 07:43:12 +00:00
|
|
|
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(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-13 02:20:55 +00:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 07:43:12 +00:00
|
|
|
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<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.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<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|