c768b12c05b0fc8c40b3fec6daf3c6b5cda7d097 / frogue / src / webui / nodeinfo.rs
Raw
use std::sync::Arc;
use actix_web::{HttpResponse, web};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
use serde::Serialize;
use tokio::sync::RwLock;
use tracing::instrument;
use crate::entities::{ap_notes, comments, patch_proposals, repositories, tickets, users};
use crate::state_read;
use crate::{AppState, activitypub, registration};
/// NodeInfo 2.1 JSON document structure
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct NodeInfo {
version: String,
software: Software,
protocols: Vec<String>,
services: Services,
open_registrations: bool,
usage: Usage,
metadata: Metadata,
}
#[derive(Serialize)]
struct Software {
name: String,
version: String,
repository: Option<String>,
}
#[derive(Serialize)]
struct Services {
inbound: Vec<String>,
outbound: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Usage {
users: UserStats,
local_posts: u64,
local_comments: u64,
}
#[derive(Serialize)]
struct UserStats {
total: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Metadata {
repository_count: u64,
}
/// NodeInfo discovery document (JRD)
#[derive(Serialize)]
struct NodeInfoDiscovery {
links: Vec<NodeInfoLink>,
}
#[derive(Serialize)]
struct NodeInfoLink {
rel: String,
href: String,
}
/// GET /.well-known/nodeinfo
///
/// Returns a JSON Resource Descriptor pointing to the NodeInfo 2.1 document.
#[actix_web::get("/.well-known/nodeinfo")]
#[instrument(skip(data))]
pub async fn get_nodeinfo_jdd(data: web::Data<Arc<RwLock<AppState>>>) -> HttpResponse {
let state = state_read(&data).await;
let public_url = state.public_url().to_string();
drop(state);
let href = if public_url.is_empty() {
"/nodeinfo/2.1".to_string()
} else {
format!("{}/nodeinfo/2.1", public_url.trim_end_matches('/'))
};
let jrd = NodeInfoDiscovery {
links: vec![NodeInfoLink {
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1#".to_string(),
href,
}],
};
HttpResponse::Ok()
.content_type("application/jrd+json")
.json(jrd)
}
/// GET /nodeinfo/2.1
///
/// Returns the NodeInfo 2.1 document with statistics about this instance.
#[actix_web::get("/nodeinfo/2.1")]
#[instrument(skip(data))]
pub async fn get_nodeinfo_21(data: web::Data<Arc<RwLock<AppState>>>) -> HttpResponse {
let state = state_read(&data).await;
let db = state.db.clone();
let federation_enabled = activitypub::enabled(&state);
drop(state);
// Count users (excluding soft-deleted)
let user_count = users::Entity::find()
.filter(users::Column::DeletedAt.is_null())
.count(&db)
.await
.unwrap_or(0);
// Count repositories (excluding soft-deleted)
let repo_count = repositories::Entity::find()
.filter(repositories::Column::DeletedAt.is_null())
.count(&db)
.await
.unwrap_or(0);
// Count tickets
let ticket_count = tickets::Entity::find().count(&db).await.unwrap_or(0);
// Count patch proposals
let patch_count = patch_proposals::Entity::find()
.count(&db)
.await
.unwrap_or(0);
// Count notes
let note_count = ap_notes::Entity::find().count(&db).await.unwrap_or(0);
// Count comments
let comment_count = comments::Entity::find().count(&db).await.unwrap_or(0);
// Determine registration mode
let open_registrations = matches!(
registration::registration_mode(),
registration::RegistrationMode::Open
);
// Build protocols list
let mut protocols = vec!["forgefed".to_string()];
if federation_enabled {
protocols.push("activitypub".to_string());
}
let nodeinfo = NodeInfo {
version: "2.1".to_string(),
software: Software {
name: "frogue".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
repository: Some(env!("CARGO_PKG_REPOSITORY").to_string()),
},
protocols,
services: Services {
inbound: vec![],
outbound: vec![],
},
open_registrations,
usage: Usage {
users: UserStats { total: user_count },
local_posts: ticket_count + patch_count + note_count,
local_comments: comment_count,
},
metadata: Metadata {
repository_count: repo_count,
},
};
HttpResponse::Ok()
.content_type("application/json")
.json(nodeinfo)
}