From bcf83ae3e14cd7fce66a7826e4d8aa7053181db0 Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 4 May 2026 13:24:57 +0300 Subject: [PATCH] ap types and nodeinfo --- cruciverb-server/src/activitypub/nodeinfo.rs | 56 +++++++++++ cruciverb-server/src/activitypub/types.rs | 97 ++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 cruciverb-server/src/activitypub/nodeinfo.rs create mode 100644 cruciverb-server/src/activitypub/types.rs diff --git a/cruciverb-server/src/activitypub/nodeinfo.rs b/cruciverb-server/src/activitypub/nodeinfo.rs new file mode 100644 index 0000000..9835003 --- /dev/null +++ b/cruciverb-server/src/activitypub/nodeinfo.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; +use axum::Json; +use axum::extract::State; +use axum::response::Response; + +use crate::state::AppState; + +pub async fn well_known( + State(state): State>, +) -> Result, Response> { + let ap = state.ap.as_ref().ok_or_else(|| { + (axum::http::StatusCode::NOT_FOUND, "not found").into_response() + })?; + + Ok(Json(serde_json::json!({ + "links": [{ + "rel": "http://nodeinfo/2.1", + "href": format!("https://{}/nodeinfo/2.1", ap.domain), + }] + }))) +} + +use axum::response::IntoResponse; + +pub async fn nodeinfo( + State(state): State>, +) -> Result, Response> { + let ap = state.ap.as_ref().ok_or_else(|| { + (axum::http::StatusCode::NOT_FOUND, "not found").into_response() + })?; + + let dbi = state.db.lock().await; + let users: i64 = dbi.db.query_row("SELECT COUNT(*) FROM sessions", [], |r| r.get(0)).unwrap_or(0); + let puzzles: i64 = dbi.db.query_row("SELECT COUNT(*) FROM puzzles", [], |r| r.get(0)).unwrap_or(0); + + Ok(Json(serde_json::json!({ + "version": "2.1", + "software": { + "name": "cruciverb", + "version": env!("CARGO_PKG_VERSION"), + "repository": "https://git.lashman.live/lashman/cruciverb", + }, + "protocols": ["activitypub"], + "services": { "inbound": [], "outbound": ["atom1.0", "rss2.0"] }, + "openRegistrations": true, + "usage": { + "users": { "total": users }, + "localPosts": puzzles, + }, + "metadata": { + "nodeName": format!("cruciverb @ {}", ap.domain), + "nodeDescription": "self-hosted crossword puzzles", + "features": ["daily_puzzles", "community_clues", "puzzle_editor", "cryptic_mode"], + }, + }))) +} diff --git a/cruciverb-server/src/activitypub/types.rs b/cruciverb-server/src/activitypub/types.rs new file mode 100644 index 0000000..9c42484 --- /dev/null +++ b/cruciverb-server/src/activitypub/types.rs @@ -0,0 +1,97 @@ +use serde::{Deserialize, Serialize}; + +pub const PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public"; +pub const CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; +pub const SECURITY_CONTEXT: &str = "https://w3id.org/security/v1"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Actor { + #[serde(rename = "@context")] + pub context: serde_json::Value, + pub id: String, + #[serde(rename = "type")] + pub actor_type: String, + #[serde(rename = "preferredUsername")] + pub preferred_username: String, + pub name: String, + pub summary: String, + pub inbox: String, + pub outbox: String, + pub followers: String, + #[serde(rename = "publicKey")] + pub public_key: ActorPublicKey, + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoints: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ActorPublicKey { + pub id: String, + pub owner: String, + #[serde(rename = "publicKeyPem")] + pub public_key_pem: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Activity { + #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] + pub context: Option, + pub id: String, + #[serde(rename = "type")] + pub activity_type: String, + pub actor: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub published: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cc: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Note { + #[serde(rename = "type")] + pub note_type: String, + pub id: String, + #[serde(rename = "attributedTo")] + pub attributed_to: String, + pub content: String, + pub published: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + pub to: Vec, + pub cc: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub attachment: Option>, +} + +#[derive(Debug, Serialize)] +pub struct OrderedCollection { + #[serde(rename = "@context")] + pub context: String, + pub id: String, + #[serde(rename = "type")] + pub collection_type: String, + #[serde(rename = "totalItems")] + pub total_items: usize, + #[serde(rename = "orderedItems")] + pub ordered_items: Vec, +} + +#[derive(Debug, Serialize)] +pub struct WebFingerResponse { + pub subject: String, + pub links: Vec, +} + +#[derive(Debug, Serialize)] +pub struct WebFingerLink { + pub rel: String, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub link_type: Option, + pub href: String, +}