🐸 Frogue(Fed)

~ololduck/Frogue

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::{AppState, activitypub, registration, state_read};

/// 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)
}