music on atproto
plyr.fm
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}