types.rs 219 lignes · 8176 octets
//! JSON types serialised on federation endpoints (Person, WebFinger JRD,
//! NodeInfo). All field names are stabilised to match the de facto Mastodon
//! / W3C ActivityStreams contracts.

use serde::{Deserialize, Serialize};
use url::Url;

/// JSON-LD `@context` shared by every actor we publish.
///
/// Includes: ActivityStreams 2.0 core vocabulary, W3C security vocabulary
/// (`publicKey` namespace), and Mastodon's `toot:` extension namespace
/// (covers `discoverable`, `indexable`, `Emoji`).
pub fn actor_context() -> serde_json::Value {
    serde_json::json!([
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        {
            "toot": "http://joinmastodon.org/ns#",
            "discoverable": "toot:discoverable",
            "indexable": "toot:indexable",
            "Emoji": "toot:Emoji"
        }
    ])
}

/// ActivityPub Person actor (W3C ActivityStreams 2.0 + Security V1 +
/// Mastodon extensions).
///
/// Field `public_key` is `None` until [RFC 0005](../../rfcs/0005-actor-key-storage.md)
/// (`actor-key-storage`) lands. Until then, the actor is **discoverable** but
/// not **federable** — Mastodon will refuse to deliver activities to an actor
/// without a published `publicKey`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonJson {
    /// JSON-LD context, see [`actor_context`].
    #[serde(rename = "@context")]
    pub context: serde_json::Value,
    /// Always the literal string `"Person"`.
    #[serde(rename = "type")]
    pub kind: String,
    /// Canonical actor URL, e.g. `https://example.org/users/alice`.
    pub id: Url,
    /// Local username (the `alice` part of `@alice@example.org`).
    pub preferred_username: String,
    /// Optional display name shown in clients.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// HTML bio (sanitised before insertion into DB; rendered as-is here).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    /// Profile page URL (HTML).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<Url>,
    /// Inbox endpoint where remote servers POST activities for this actor.
    pub inbox: Url,
    /// Outbox endpoint listing this actor's published activities.
    pub outbox: Url,
    /// Followers collection URL.
    pub followers: Url,
    /// Following collection URL.
    pub following: Url,
    /// Mastodon-compatible shared inbox endpoint (instance-wide).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub endpoints: Option<EndpointsJson>,
    /// Public RSA key used to verify HTTP signatures. `None` until RFC 0005.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub public_key: Option<PublicKeyJson>,
    /// When `true`, follow requests await manual approval.
    #[serde(default)]
    pub manually_approves_followers: bool,
    /// Mastodon `toot:discoverable` flag.
    #[serde(default = "default_true")]
    pub discoverable: bool,
}

fn default_true() -> bool {
    true
}

/// Public RSA key embedded in a [`PersonJson`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyJson {
    /// Key identifier, conventionally `{actor_id}#main-key`.
    pub id: Url,
    /// URL of the actor that owns this key.
    pub owner: Url,
    /// PEM-encoded SubjectPublicKeyInfo (RSA-2048+).
    pub public_key_pem: String,
}

/// Mastodon-compatible `endpoints` object on a Person actor.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointsJson {
    /// Shared inbox URL (one per instance, used for fan-out optimisation).
    pub shared_inbox: Url,
}

// ───────────────────────── WebFinger (RFC 7033) ─────────────────────────

/// WebFinger JRD response document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebFingerResponse {
    /// Subject URI, must echo the `resource` query parameter.
    pub subject: String,
    /// Optional alternative URIs for the same subject.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub aliases: Vec<String>,
    /// Typed links pointing at the various representations of the subject.
    pub links: Vec<WebFingerLink>,
}

/// Single typed link inside a [`WebFingerResponse`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebFingerLink {
    /// Link relation, e.g. `"self"` or
    /// `"http://webfinger.net/rel/profile-page"`.
    pub rel: String,
    /// MIME type of the resource.
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,
    /// Concrete URL of the linked resource (mutually exclusive with
    /// `template`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub href: Option<String>,
    /// URI template (RFC 6570) when the relation is interaction-driven.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template: Option<String>,
}

// ───────────────────────── NodeInfo (Diaspora) ──────────────────────────

/// NodeInfo discovery document (`/.well-known/nodeinfo`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfoDiscovery {
    /// Links pointing at concrete NodeInfo schema versions.
    pub links: Vec<NodeInfoLink>,
}

/// Single link in a [`NodeInfoDiscovery`] document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfoLink {
    /// Schema relation, e.g.
    /// `http://nodeinfo.diaspora.software/ns/schema/2.1`.
    pub rel: String,
    /// URL of the concrete NodeInfo resource.
    pub href: String,
}

/// NodeInfo 2.1 detail document (`/nodeinfo/2.1`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeInfo {
    /// Always `"2.1"`.
    pub version: String,
    /// Software name + version metadata.
    pub software: NodeInfoSoftware,
    /// Federation protocols supported, always at least `["activitypub"]`.
    pub protocols: Vec<String>,
    /// Inbound/outbound services bridged by this instance (empty for now).
    pub services: NodeInfoServices,
    /// Aggregated usage counters.
    pub usage: NodeInfoUsage,
    /// Whether new account registration is open to the public.
    pub open_registrations: bool,
    /// Free-form metadata (instance description, contact, …).
    #[serde(default)]
    pub metadata: serde_json::Value,
}

/// Software identification block of a [`NodeInfo`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfoSoftware {
    /// Lowercase software name, e.g. `"rustwarden"`.
    pub name: String,
    /// SemVer software version.
    pub version: String,
    /// Optional VCS repository URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repository: Option<String>,
    /// Optional homepage URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub homepage: Option<String>,
}

/// Services bridge advertised by NodeInfo.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeInfoServices {
    /// Inbound bridges (empty for Rustwarden).
    #[serde(default)]
    pub inbound: Vec<String>,
    /// Outbound bridges (empty for Rustwarden).
    #[serde(default)]
    pub outbound: Vec<String>,
}

/// Aggregated usage counters in [`NodeInfo`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeInfoUsage {
    /// User counters split by activity window.
    pub users: NodeInfoUsageUsers,
    /// Total number of locally-authored posts.
    pub local_posts: u64,
}

/// User-count split returned in [`NodeInfoUsage`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeInfoUsageUsers {
    /// Total registered users.
    pub total: u64,
    /// Users active in the last 30 days.
    pub active_month: u64,
    /// Users active in the last 180 days.
    pub active_halfyear: u64,
}