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 config; 25mod db; 26mod handlers; 27mod labels; 28mod review; 29mod state; 30mod xrpc; 31 32pub use state::{AppError, AppState}; 33 34#[tokio::main] 35async fn main() -> anyhow::Result<()> { 36 tracing_subscriber::fmt() 37 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 38 .with_target(false) 39 .init(); 40 41 let config = config::Config::from_env()?; 42 let auth_token = config.auth_token.clone(); 43 44 // Initialize labeler components if configured 45 let (db, signer, label_tx) = if config.labeler_enabled() { 46 let db = db::LabelDb::connect(config.database_url.as_ref().unwrap()).await?; 47 db.migrate().await?; 48 info!("labeler database connected and migrated"); 49 50 let signer = labels::LabelSigner::from_hex( 51 config.labeler_signing_key.as_ref().unwrap(), 52 config.labeler_did.as_ref().unwrap(), 53 )?; 54 info!(did = %signer.did(), "labeler signer initialized"); 55 56 let (tx, _) = broadcast::channel::<(i64, labels::Label)>(1024); 57 (Some(db), Some(signer), Some(tx)) 58 } else { 59 warn!("labeler not configured - XRPC endpoints will return 503"); 60 (None, None, None) 61 }; 62 63 let state = AppState { 64 audd_api_token: config.audd_api_token, 65 audd_api_url: config.audd_api_url, 66 db: db.map(Arc::new), 67 signer: signer.map(Arc::new), 68 label_tx, 69 }; 70 71 let app = Router::new() 72 // Landing page 73 .route("/", get(handlers::landing)) 74 // Health check 75 .route("/health", get(handlers::health)) 76 // Sensitive images (public) 77 .route("/sensitive-images", get(handlers::get_sensitive_images)) 78 // AuDD scanning 79 .route("/scan", post(audd::scan)) 80 // Label emission (internal API) 81 .route("/emit-label", post(handlers::emit_label)) 82 // Admin UI and API 83 .route("/admin", get(admin::admin_ui)) 84 .route("/admin/flags", get(admin::list_flagged)) 85 .route("/admin/flags-html", get(admin::list_flagged_html)) 86 .route("/admin/resolve", post(admin::resolve_flag)) 87 .route("/admin/resolve-htmx", post(admin::resolve_flag_htmx)) 88 .route("/admin/context", post(admin::store_context)) 89 .route("/admin/active-labels", post(admin::get_active_labels)) 90 .route("/admin/sensitive-images", post(admin::add_sensitive_image)) 91 .route( 92 "/admin/sensitive-images/remove", 93 post(admin::remove_sensitive_image), 94 ) 95 .route("/admin/batches", post(admin::create_batch)) 96 // Review endpoints (under admin, auth protected) 97 .route("/admin/review/:id", get(review::review_page)) 98 .route("/admin/review/:id/data", get(review::review_data)) 99 .route("/admin/review/:id/submit", post(review::submit_review)) 100 // Static files (CSS, JS for admin UI) 101 .nest_service("/static", ServeDir::new("static")) 102 // ATProto XRPC endpoints (public) 103 .route( 104 "/xrpc/com.atproto.label.queryLabels", 105 get(xrpc::query_labels), 106 ) 107 .route( 108 "/xrpc/com.atproto.label.subscribeLabels", 109 get(xrpc::subscribe_labels), 110 ) 111 .layer(middleware::from_fn(move |req, next| { 112 auth::auth_middleware(req, next, auth_token.clone()) 113 })) 114 .with_state(state); 115 116 let addr: SocketAddr = format!("{}:{}", config.host, config.port) 117 .parse() 118 .map_err(|e| anyhow!("invalid bind addr: {e}"))?; 119 info!(%addr, "moderation service listening"); 120 121 let listener = TcpListener::bind(addr).await?; 122 axum::serve(listener, app).await?; 123 Ok(()) 124}