1//! plyr.fm moderation service 2//! 3//! Provides: 4//! - AuDD audio fingerprinting for copyright detection 5//! - ATProto labeler endpoints (queryLabels, subscribeLabels) 6//! - Label emission for copyright violations 7//! - Admin UI for reviewing and resolving flags 8 9use std::{net::SocketAddr, sync::Arc}; 10 11use anyhow::anyhow; 12use axum::{ 13 middleware, 14 routing::{get, post}, 15 Router, 16}; 17use tokio::{net::TcpListener, sync::broadcast}; 18use tower_http::services::ServeDir; 19use tracing::{info, warn}; 20 21mod admin; 22mod audd; 23mod auth; 24mod claude; 25mod config; 26mod db; 27mod handlers; 28mod labels; 29mod review; 30mod state; 31mod xrpc; 32 33pub use state::{AppError, AppState}; 34 35#[tokio::main] 36async fn main() -> anyhow::Result<()> { 37 tracing_subscriber::fmt() 38 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 39 .with_target(false) 40 .init(); 41 42 let config = config::Config::from_env()?; 43 let auth_token = config.auth_token.clone(); 44 45 // Initialize labeler components if configured 46 let (db, signer, label_tx) = if config.labeler_enabled() { 47 let db = db::LabelDb::connect(config.database_url.as_ref().unwrap()).await?; 48 db.migrate().await?; 49 info!("labeler database connected and migrated"); 50 51 let signer = labels::LabelSigner::from_hex( 52 config.labeler_signing_key.as_ref().unwrap(), 53 config.labeler_did.as_ref().unwrap(), 54 )?; 55 info!(did = %signer.did(), "labeler signer initialized"); 56 57 let (tx, _) = broadcast::channel::<(i64, labels::Label)>(1024); 58 (Some(db), Some(signer), Some(tx)) 59 } else { 60 warn!("labeler not configured - XRPC endpoints will return 503"); 61 (None, None, None) 62 }; 63 64 // Initialize Claude client for image moderation if configured 65 let claude_client = if config.claude_enabled() { 66 let client = claude::ClaudeClient::new( 67 config.claude_api_key.clone().unwrap(), 68 Some(config.claude_model.clone()), 69 ); 70 info!(model = %config.claude_model, "claude image moderation enabled"); 71 Some(client) 72 } else { 73 warn!("claude not configured - /scan-image endpoint will return 503"); 74 None 75 }; 76 77 let state = AppState { 78 audd_api_token: config.audd_api_token, 79 audd_api_url: config.audd_api_url, 80 db: db.map(Arc::new), 81 signer: signer.map(Arc::new), 82 label_tx, 83 claude: claude_client.map(Arc::new), 84 copyright_score_threshold: config.copyright_score_threshold, 85 }; 86 87 let app = Router::new() 88 // Landing page 89 .route("/", get(handlers::landing)) 90 // Health check 91 .route("/health", get(handlers::health)) 92 // Sensitive images (public) 93 .route("/sensitive-images", get(handlers::get_sensitive_images)) 94 // AuDD scanning 95 .route("/scan", post(audd::scan)) 96 // Image moderation via Claude 97 .route("/scan-image", post(handlers::scan_image)) 98 // Label emission (internal API) 99 .route("/emit-label", post(handlers::emit_label)) 100 // Admin UI and API 101 .route("/admin", get(admin::admin_ui)) 102 .route("/admin/flags", get(admin::list_flagged)) 103 .route("/admin/flags-html", get(admin::list_flagged_html)) 104 .route("/admin/resolve", post(admin::resolve_flag)) 105 .route("/admin/resolve-htmx", post(admin::resolve_flag_htmx)) 106 .route("/admin/context", post(admin::store_context)) 107 .route("/admin/active-labels", post(admin::get_active_labels)) 108 .route("/admin/sensitive-images", post(admin::add_sensitive_image)) 109 .route( 110 "/admin/sensitive-images/remove", 111 post(admin::remove_sensitive_image), 112 ) 113 .route("/admin/batches", post(admin::create_batch)) 114 // Review endpoints (under admin, auth protected) 115 .route("/admin/review/:id", get(review::review_page)) 116 .route("/admin/review/:id/data", get(review::review_data)) 117 .route("/admin/review/:id/submit", post(review::submit_review)) 118 // Static files (CSS, JS for admin UI) 119 .nest_service("/static", ServeDir::new("static")) 120 // ATProto XRPC endpoints (public) 121 .route( 122 "/xrpc/com.atproto.label.queryLabels", 123 get(xrpc::query_labels), 124 ) 125 .route( 126 "/xrpc/com.atproto.label.subscribeLabels", 127 get(xrpc::subscribe_labels), 128 ) 129 .layer(middleware::from_fn(move |req, next| { 130 auth::auth_middleware(req, next, auth_token.clone()) 131 })) 132 .with_state(state); 133 134 let addr: SocketAddr = format!("{}:{}", config.host, config.port) 135 .parse() 136 .map_err(|e| anyhow!("invalid bind addr: {e}"))?; 137 info!(%addr, "moderation service listening"); 138 139 let listener = TcpListener::bind(addr).await?; 140 axum::serve(listener, app).await?; 141 Ok(()) 142}