fix: NodeInfo 2.0 schema compliance, bump 0.4.5
Some checks failed
CI / fmt (push) Successful in 42s
CI / test (push) Has been cancelled
CI / clippy (push) Has been cancelled

Remove invalid $schema, add required services/metadata fields.
Configurable via with_nodeinfo_services/with_nodeinfo_metadata.

Closes GKaszewski/movies-diary#2
This commit is contained in:
2026-07-16 09:44:05 +02:00
parent bb7db58848
commit 4d7117b123
6 changed files with 58 additions and 14 deletions

View File

@@ -1,5 +1,19 @@
# Changelog # Changelog
## [0.4.5] — 2026-07-16
### Bug fixes
- Remove invalid `$schema` field from NodeInfo 2.0 response — the field is not allowed by the NodeInfo 2.0 schema (`additionalProperties: false`)
- Add missing required `services` and `metadata` fields to NodeInfo 2.0 response
### New features
- `FederationData::with_nodeinfo_services(inbound, outbound)` — configure NodeInfo `services.inbound`/`services.outbound`
- `FederationData::with_nodeinfo_metadata(value)` — set arbitrary NodeInfo `metadata`
---
## [0.4.4] — 2026-07-11 ## [0.4.4] — 2026-07-11
### Bug fixes ### Bug fixes

2
Cargo.lock generated
View File

@@ -1368,7 +1368,7 @@ dependencies = [
[[package]] [[package]]
name = "k-ap" name = "k-ap"
version = "0.4.4" version = "0.4.5"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"anyhow", "anyhow",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "k-ap" name = "k-ap"
version = "0.4.4" version = "0.4.5"
edition = "2024" edition = "2024"
description = "Generic ActivityPub protocol layer" description = "Generic ActivityPub protocol layer"
license = "MIT" license = "MIT"

View File

@@ -71,6 +71,9 @@ pub struct FederationData {
pub(crate) software_name: String, pub(crate) software_name: String,
pub(crate) event_publisher: Option<Arc<dyn EventPublisher>>, pub(crate) event_publisher: Option<Arc<dyn EventPublisher>>,
pub(crate) actor_cache_ttl: std::time::Duration, pub(crate) actor_cache_ttl: std::time::Duration,
pub(crate) nodeinfo_services_inbound: Vec<String>,
pub(crate) nodeinfo_services_outbound: Vec<String>,
pub(crate) nodeinfo_metadata: serde_json::Value,
} }
impl FederationData { impl FederationData {
@@ -110,6 +113,20 @@ impl FederationData {
software_name, software_name,
event_publisher, event_publisher,
actor_cache_ttl, actor_cache_ttl,
nodeinfo_services_inbound: vec![],
nodeinfo_services_outbound: vec![],
nodeinfo_metadata: serde_json::json!({}),
} }
} }
pub fn with_nodeinfo_services(mut self, inbound: Vec<String>, outbound: Vec<String>) -> Self {
self.nodeinfo_services_inbound = inbound;
self.nodeinfo_services_outbound = outbound;
self
}
pub fn with_nodeinfo_metadata(mut self, metadata: serde_json::Value) -> Self {
self.nodeinfo_metadata = metadata;
self
}
} }

View File

@@ -6,7 +6,6 @@ use crate::data::FederationData;
use crate::error::Error; use crate::error::Error;
const NODEINFO_2_0_REL: &str = "http://nodeinfo.diaspora.software/ns/schema/2.0"; const NODEINFO_2_0_REL: &str = "http://nodeinfo.diaspora.software/ns/schema/2.0";
const NODEINFO_2_0_SCHEMA: &str = "http://nodeinfo.diaspora.software/ns/schema/2.0#";
#[derive(Serialize)] #[derive(Serialize)]
pub struct NodeInfoWellKnown { pub struct NodeInfoWellKnown {
@@ -37,16 +36,22 @@ pub struct NodeInfoUsers {
pub total: usize, pub total: usize,
} }
#[derive(Serialize)]
pub struct NodeInfoServices {
pub inbound: Vec<String>,
pub outbound: Vec<String>,
}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NodeInfo { pub struct NodeInfo {
#[serde(rename = "$schema")]
pub schema: String,
pub version: String, pub version: String,
pub software: NodeInfoSoftware, pub software: NodeInfoSoftware,
pub protocols: Vec<String>, pub protocols: Vec<String>,
pub usage: NodeInfoUsage, pub services: NodeInfoServices,
pub open_registrations: bool, pub open_registrations: bool,
pub usage: NodeInfoUsage,
pub metadata: serde_json::Value,
} }
pub async fn nodeinfo_well_known_handler( pub async fn nodeinfo_well_known_handler(
@@ -66,18 +71,22 @@ pub async fn nodeinfo_handler(data: Data<FederationData>) -> Result<Json<NodeInf
let local_posts = data.content_reader.count_local_posts().await.unwrap_or(0); let local_posts = data.content_reader.count_local_posts().await.unwrap_or(0);
Ok(Json(NodeInfo { Ok(Json(NodeInfo {
schema: NODEINFO_2_0_SCHEMA.to_string(),
version: "2.0".to_string(), version: "2.0".to_string(),
software: NodeInfoSoftware { software: NodeInfoSoftware {
name: data.software_name.clone(), name: data.software_name.clone(),
version: env!("CARGO_PKG_VERSION").to_string(), version: env!("CARGO_PKG_VERSION").to_string(),
}, },
protocols: vec!["activitypub".to_string()], protocols: vec!["activitypub".to_string()],
services: NodeInfoServices {
inbound: data.nodeinfo_services_inbound.clone(),
outbound: data.nodeinfo_services_outbound.clone(),
},
open_registrations: data.allow_registration,
usage: NodeInfoUsage { usage: NodeInfoUsage {
users: NodeInfoUsers { total: user_count }, users: NodeInfoUsers { total: user_count },
local_posts, local_posts,
}, },
open_registrations: data.allow_registration, metadata: data.nodeinfo_metadata.clone(),
})) }))
} }

View File

@@ -19,27 +19,31 @@ fn nodeinfo_well_known_serializes_correctly() {
#[test] #[test]
fn nodeinfo_serializes_camel_case() { fn nodeinfo_serializes_camel_case() {
let doc = NodeInfo { let doc = NodeInfo {
schema: "http://nodeinfo.diaspora.software/ns/schema/2.0#".to_string(),
version: "2.0".to_string(), version: "2.0".to_string(),
software: NodeInfoSoftware { software: NodeInfoSoftware {
name: "my-app".to_string(), name: "my-app".to_string(),
version: "0.1.0".to_string(), version: "0.1.0".to_string(),
}, },
protocols: vec!["activitypub".to_string()], protocols: vec!["activitypub".to_string()],
services: NodeInfoServices {
inbound: vec![],
outbound: vec![],
},
open_registrations: false,
usage: NodeInfoUsage { usage: NodeInfoUsage {
users: NodeInfoUsers { total: 3 }, users: NodeInfoUsers { total: 3 },
local_posts: 42, local_posts: 42,
}, },
open_registrations: false, metadata: serde_json::json!({}),
}; };
let json = serde_json::to_value(&doc).unwrap(); let json = serde_json::to_value(&doc).unwrap();
assert_eq!( assert!(json.get("$schema").is_none());
json["$schema"],
"http://nodeinfo.diaspora.software/ns/schema/2.0#"
);
assert_eq!(json["version"], "2.0"); assert_eq!(json["version"], "2.0");
assert_eq!(json["software"]["name"], "my-app"); assert_eq!(json["software"]["name"], "my-app");
assert_eq!(json["usage"]["users"]["total"], 3); assert_eq!(json["usage"]["users"]["total"], 3);
assert_eq!(json["usage"]["localPosts"], 42); assert_eq!(json["usage"]["localPosts"], 42);
assert_eq!(json["openRegistrations"], false); assert_eq!(json["openRegistrations"], false);
assert_eq!(json["services"]["inbound"], serde_json::json!([]));
assert_eq!(json["services"]["outbound"], serde_json::json!([]));
assert_eq!(json["metadata"], serde_json::json!({}));
} }