nodeinfo.rs
197 lignes · 6590 octets
//! NodeInfo discovery + 2.1 detail handlers. //! //! `GET /.well-known/nodeinfo` returns a discovery document linking to the //! concrete schema versions we expose. `GET /nodeinfo/2.1` returns the //! full metadata document used by Fediverse directories. use axum::{Extension, Json}; use super::error::FediverseError; use super::state::FediverseState; use super::types::{ NodeInfo, NodeInfoDiscovery, NodeInfoLink, NodeInfoServices, NodeInfoSoftware, NodeInfoUsage, NodeInfoUsageUsers, }; const NODEINFO_2_1_REL: &str = "http://nodeinfo.diaspora.software/ns/schema/2.1"; /// `GET /.well-known/nodeinfo`. pub async fn discovery( Extension(state): Extension<FediverseState>, ) -> Result<Json<NodeInfoDiscovery>, FediverseError> { let href = format!("{}nodeinfo/2.1", state.config.base_url()); Ok(Json(NodeInfoDiscovery { links: vec![NodeInfoLink { rel: NODEINFO_2_1_REL.to_string(), href, }], })) } /// `GET /nodeinfo/2.1`. pub async fn detail( Extension(state): Extension<FediverseState>, ) -> Result<Json<NodeInfo>, FediverseError> { let total = state.resolver.count_users_total().await?; let active_month = state.resolver.count_users_active_since(30).await?; let active_halfyear = state.resolver.count_users_active_since(180).await?; let local_posts = state.resolver.count_local_posts().await?; Ok(Json(NodeInfo { version: "2.1".to_string(), software: NodeInfoSoftware { name: "rustwarden".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), repository: Some("https://github.com/EBO/rustwarden".to_string()), homepage: None, }, protocols: vec!["activitypub".to_string()], services: NodeInfoServices::default(), usage: NodeInfoUsage { users: NodeInfoUsageUsers { total, active_month, active_halfyear, }, local_posts, }, // RFC 0004 will source this from `app_settings.allow_signup`. For now // a conservative default: closed registration. open_registrations: false, metadata: serde_json::json!({}), })) } #[cfg(test)] mod tests { use super::*; use crate::config::FediverseConfig; use crate::resolver::{LocalUserResolver, UserSummary}; use async_trait::async_trait; use axum::Router; use axum::body::Body; use axum::http::{Request, StatusCode}; use axum::routing::get; use std::sync::Arc; use tower::ServiceExt; struct StubResolver { total: u64, active_month: u64, active_halfyear: u64, } #[async_trait] impl LocalUserResolver for StubResolver { async fn find_local_user( &self, _username: &str, ) -> Result<Option<UserSummary>, rustwarden_core::error::AppError> { Ok(None) } async fn count_users_total(&self) -> Result<u64, rustwarden_core::error::AppError> { Ok(self.total) } async fn count_users_active_since( &self, days: u32, ) -> Result<u64, rustwarden_core::error::AppError> { Ok(if days <= 30 { self.active_month } else { self.active_halfyear }) } async fn count_local_posts(&self) -> Result<u64, rustwarden_core::error::AppError> { Ok(0) } } fn router(domain: &str, total: u64, active_month: u64, active_halfyear: u64) -> Router { let state = FediverseState { config: FediverseConfig::for_tests(domain), resolver: Arc::new(StubResolver { total, active_month, active_halfyear, }), }; Router::new() .route("/.well-known/nodeinfo", get(discovery)) .route("/nodeinfo/2.1", get(detail)) .layer(Extension(state)) } async fn body_json(response: axum::response::Response) -> serde_json::Value { let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024) .await .expect("body bytes"); serde_json::from_slice(&bytes).expect("body is valid JSON") } #[tokio::test] async fn nodeinfo_discovery_links_to_2_1() { let app = router("example.org", 1, 0, 0); let response = app .oneshot( Request::builder() .uri("/.well-known/nodeinfo") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_json(response).await; let link = body["links"] .as_array() .expect("links") .iter() .find(|l| l["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.1") .expect("2.1 schema link"); assert_eq!(link["href"], "https://example.org/nodeinfo/2.1"); } #[tokio::test] async fn nodeinfo_detail_returns_software_metadata() { let app = router("example.org", 0, 0, 0); let response = app .oneshot( Request::builder() .uri("/nodeinfo/2.1") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_json(response).await; assert_eq!(body["version"], "2.1"); assert_eq!(body["software"]["name"], "rustwarden"); assert_eq!( body["software"]["version"], env!("CARGO_PKG_VERSION"), "software.version must mirror CARGO_PKG_VERSION" ); assert_eq!(body["protocols"][0], "activitypub"); } #[tokio::test] async fn nodeinfo_detail_counts_users_via_resolver() { let app = router("example.org", 42, 7, 23); let response = app .oneshot( Request::builder() .uri("/nodeinfo/2.1") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let body = body_json(response).await; assert_eq!(body["usage"]["users"]["total"], 42); assert_eq!(body["usage"]["users"]["activeMonth"], 7); assert_eq!(body["usage"]["users"]["activeHalfyear"], 23); assert_eq!(body["usage"]["localPosts"], 0); } }
GitRust