diff --git a/Cargo.toml b/Cargo.toml index 4803fb8..ae65a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" uuid = { version = "1.8.0", features = ["v4"] } jrd = "0.1" +# nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! +nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } diff --git a/src/activitypub/well_known.rs b/src/activitypub/well_known.rs index 26ad8e2..c098aa7 100644 --- a/src/activitypub/well_known.rs +++ b/src/activitypub/well_known.rs @@ -1,8 +1,86 @@ -use axum::{extract::{Query, State}, http::StatusCode, response::{IntoResponse, Response}}; +use axum::{extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, Json}; use jrd::{JsonResourceDescriptor, JsonResourceDescriptorLink}; -use sea_orm::EntityTrait; +use sea_orm::{EntityTrait, PaginatorTrait}; + +use crate::{model, server::Context}; + +#[derive(serde::Serialize)] +pub struct NodeInfoDiscovery { + pub links: Vec, +} + +#[derive(serde::Serialize)] +pub struct NodeInfoDiscoveryRel { + pub rel: String, + pub href: String, +} + +pub async fn nodeinfo_discovery(State(ctx): State) -> Json { + Json(NodeInfoDiscovery { + links: vec![ + NodeInfoDiscoveryRel { + rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".into(), + href: format!("{}/nodeinfo/2.0.json", ctx.base()), + }, + NodeInfoDiscoveryRel { + rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".into(), + href: format!("{}/nodeinfo/2.1.json", ctx.base()), + }, + ], + }) +} + +pub async fn nodeinfo(State(ctx): State, Path(version): Path) -> Result, StatusCode> { + // TODO it's unsustainable to count these every time, especially comments since it's a complex + // filter! keep these numbers caches somewhere, maybe db, so that we can just look them up + let total_users = model::user::Entity::find().count(ctx.db()).await.ok(); + let total_posts = None; + let total_comments = None; + let (software, version) = match version.as_str() { + "2.0.json" => ( + nodeinfo::types::Software { + name: "μpub".to_string(), + version: Some(env!("CARGO_PKG_VERSION").into()), + repository: None, + homepage: None, + }, + "2.0".to_string() + ), + "2.1.json" => ( + nodeinfo::types::Software { + name: "μpub".to_string(), + version: Some(env!("CARGO_PKG_VERSION").into()), + repository: Some("https://git.alemi.dev/upub.git/".into()), + homepage: None, + }, + "2.1".to_string() + ), + _ => return Err(StatusCode::NOT_IMPLEMENTED), + }; + Ok(Json( + nodeinfo::NodeInfoOwned { + version, + software, + open_registrations: false, + protocols: vec!["activitypub".into()], + services: nodeinfo::types::Services { + inbound: vec![], + outbound: vec![], + }, + usage: nodeinfo::types::Usage { + local_posts: total_posts, + local_comments: total_comments, + users: Some(nodeinfo::types::Users { + active_month: None, + active_halfyear: None, + total: total_users.map(|x| x as i64), + }), + }, + metadata: serde_json::Map::default(), + } + )) +} -use crate::{server::Context, model}; #[derive(Debug, serde::Deserialize)] pub struct WebfingerQuery { @@ -12,7 +90,7 @@ pub struct WebfingerQuery { pub struct JsonRD(pub T); impl IntoResponse for JsonRD { fn into_response(self) -> Response { - ([("Content-Type", "application/jrd+json")], axum::Json(self.0)).into_response() + ([("Content-Type", "application/jrd+json")], Json(self.0)).into_response() } } diff --git a/src/server.rs b/src/server.rs index 040313e..3388be5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -80,6 +80,8 @@ pub async fn serve(db: DatabaseConnection, domain: String) { // .well-known and discovery .route("/.well-known/webfinger", get(ap::well_known::webfinger)) .route("/.well-known/host-meta", get(ap::well_known::host_meta)) + .route("/.well-known/nodeinfo", get(ap::well_known::nodeinfo_discovery)) + .route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) // actor routes .route("/users/:id", get(ap::user::view)) .route("/users/:id/inbox", post(ap::user::inbox))