use super::build_response; use crate::config::Config; use crate::content_server; use anyhow::Result; use hyper::{Response, StatusCode}; use super::{is_valid_app_id, ApiHandler}; impl ApiHandler { pub(super) async fn handle_content_catalog(config: &Config) -> Result> { match content_server::load_catalog(&config.data_dir).await { Ok(catalog) => { // Only expose public metadata for available items let items: Vec = catalog .items .iter() .filter(|i| !matches!(i.availability, content_server::Availability::Nobody)) .map(|i| { serde_json::json!({ "id": i.id, "filename": i.filename, "mime_type": i.mime_type, "size_bytes": i.size_bytes, "description": i.description, "access": i.access, }) }) .collect(); let body = serde_json::to_vec(&serde_json::json!({ "items": items })).unwrap_or_default(); Ok(build_response( StatusCode::OK, "application/json", hyper::Body::from(body), )) } Err(e) => { let body = serde_json::json!({ "error": e.to_string() }); let body_bytes = serde_json::to_vec(&body).unwrap_or_default(); Ok(build_response( StatusCode::INTERNAL_SERVER_ERROR, "application/json", hyper::Body::from(body_bytes), )) } } } pub(super) async fn handle_content_request( path: &str, headers: &hyper::HeaderMap, config: &Config, ) -> Result> { let content_id = path.strip_prefix("/content/").unwrap_or(""); if content_id.is_empty() || !is_valid_app_id(content_id) { return Ok(build_response( StatusCode::BAD_REQUEST, "text/plain", hyper::Body::from("Invalid content ID"), )); } // Extract payment token from X-Payment-Token header let payment_token = headers .get("x-payment-token") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); // Extract federation peer DID from X-Federation-DID header let peer_did = headers .get("x-federation-did") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); // Parse Range header for streaming support let range = headers .get("range") .and_then(|v| v.to_str().ok()) .and_then(content_server::parse_range_header); match content_server::serve_content( &config.data_dir, content_id, payment_token.as_deref(), peer_did.as_deref(), range, ) .await { Ok(content_server::ServeResult::Ok(bytes, mime_type)) => { let len = bytes.len(); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", mime_type) .header("Content-Length", len.to_string()) .header("Accept-Ranges", "bytes") .body(hyper::Body::from(bytes)) .unwrap()) } Ok(content_server::ServeResult::Partial { bytes, mime_type, start, end, total, }) => Ok(Response::builder() .status(StatusCode::PARTIAL_CONTENT) .header("Content-Type", mime_type) .header("Content-Length", bytes.len().to_string()) .header( "Content-Range", format!("bytes {}-{}/{}", start, end, total), ) .header("Accept-Ranges", "bytes") .body(hyper::Body::from(bytes)) .unwrap()), Ok(content_server::ServeResult::PaymentRequired(price_sats)) => { let body = serde_json::json!({ "error": "Payment required", "price_sats": price_sats, "payment_header": "X-Payment-Token", }); let body_bytes = serde_json::to_vec(&body).unwrap_or_default(); Ok(build_response( StatusCode::PAYMENT_REQUIRED, "application/json", hyper::Body::from(body_bytes), )) } Ok(content_server::ServeResult::Forbidden) => Ok(build_response( StatusCode::FORBIDDEN, "application/json", hyper::Body::from(r#"{"error":"Access denied — federation peer required"}"#), )), Ok(content_server::ServeResult::NotFound) | Err(_) => Ok(build_response( StatusCode::NOT_FOUND, "text/plain", hyper::Body::from("Content not found"), )), } } /// Serve a degraded preview of paid content (blurred image or first 2% of video). pub(super) async fn handle_content_preview( path: &str, config: &Config, ) -> Result> { // Path format: /content/{id}/preview let content_id = path .strip_prefix("/content/") .and_then(|s| s.strip_suffix("/preview")) .unwrap_or(""); if content_id.is_empty() || !is_valid_app_id(content_id) { return Ok(build_response( StatusCode::BAD_REQUEST, "text/plain", hyper::Body::from("Invalid content ID"), )); } match content_server::serve_content_preview(&config.data_dir, content_id).await { Ok(content_server::PreviewResult::FullContent(bytes, mime_type)) => { let len = bytes.len(); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", mime_type) .header("Content-Length", len.to_string()) .body(hyper::Body::from(bytes)) .unwrap()) } Ok(content_server::PreviewResult::BlurPreview(bytes, mime_type)) => { let len = bytes.len(); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", mime_type) .header("Content-Length", len.to_string()) .header("X-Content-Preview", "blur") .body(hyper::Body::from(bytes)) .unwrap()) } Ok(content_server::PreviewResult::TruncatedPreview(bytes, mime_type, total_size)) => { let len = bytes.len(); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", mime_type) .header("Content-Length", len.to_string()) .header("X-Content-Preview", "truncated") .header("X-Content-Total-Size", total_size.to_string()) .body(hyper::Body::from(bytes)) .unwrap()) } Ok(content_server::PreviewResult::NotFound) | Err(_) => Ok(build_response( StatusCode::NOT_FOUND, "text/plain", hyper::Body::from("Preview not available"), )), } } }