diff --git a/.drone.yml b/.drone.yml index c83035f..a083613 100644 --- a/.drone.yml +++ b/.drone.yml @@ -53,11 +53,11 @@ steps: environment: CARGO_HOME: .cargo commands: - - cargo run --example simple_federation --features actix-web + - cargo run --example local_federation actix-web - name: cargo run axum example image: rust:1.65-bullseye environment: CARGO_HOME: .cargo commands: - - cargo run --example simple_federation --features axum + - cargo run --example local_federation axum diff --git a/Cargo.toml b/Cargo.toml index 875d04e..2a1a494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ hyper = { version = "0.14", optional = true } displaydoc = "0.2.3" [features] -default = [] +default = ["actix-web", "axum"] actix-web = ["dep:actix-web"] axum = ["dep:axum", "dep:tower", "dep:hyper"] diff --git a/README.md b/README.md index 4acd5a1..4606655 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,6 @@ There are two examples included to see how the library altogether: To see how this library is used in production, have a look at the [Lemmy federation code](https://github.com/LemmyNet/lemmy/tree/main/crates/apub). -## Setup - -To use this crate in your project you need a web framework, preferably `actix-web` or `axum`. Be sure to enable the corresponding feature to access the full functionality. You also need a persistent storage such as PostgreSQL. Additionally, `serde` and `serde_json` are required to (de)serialize data which is sent over the network. - ## Federating users This library intentionally doesn't include any predefined data structures for federated data. The reason is that each federated application is different, and needs different data formats. Activitypub also doesn't define any specific data structures, but provides a few mandatory fields and many which are optional. For this reason it works best to let each application define its own data structures, and take advantage of serde for (de)serialization. This means we don't use `json-ld` which Activitypub is based on, but that doesn't cause any problems in practice. @@ -101,7 +97,7 @@ pub struct Person { `PersonType` is an enum with a single variant `Person`. It is used to deserialize objects in a typesafe way: If the JSON type value does not match the string `Person`, deserialization fails. This helps in places where we don't know the exact data type that is being deserialized, as you will see later. -Besides we also need a second struct to represent the data which gets stored in our local database. This is necessary because the data format used by SQL is very different from that used by that from Activitypub. It is organized by an integer primary key instead of a link id. Nested structs are complicated to represent and easier if flattened. Some fields like `type` don't need to be stored at all. On the other hand, the database contains fields which can't be federated, such as the private key and a boolean indicating if the item is local or remote. +Besides we also need a second struct to represent the data which gets stored in our local database (for example PostgreSQL). This is necessary because the data format used by SQL is very different from that used by that from Activitypub. It is organized by an integer primary key instead of a link id. Nested structs are complicated to represent and easier if flattened. Some fields like `type` don't need to be stored at all. On the other hand, the database contains fields which can't be federated, such as the private key and a boolean indicating if the item is local or remote. ```rust # use url::Url; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9af7587 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,13 @@ +# Examples + +## Local Federation + +Creates two instances which run on localhost and federate with each other. This setup is ideal for quick development and well as automated tests. In this case both instances run in the same process and are controlled from the main function. + +In case of Lemmy we are using the same setup for continuous integration tests, only that multiple instances are started with a bash script as different threads, and controlled over the API. + +Use one of the following commands to run the example with the specified web framework: + +`cargo run --example local_federation axum` + +`cargo run --example local_federation actix-web` diff --git a/examples/local_federation/actix_web/http.rs b/examples/local_federation/actix_web/http.rs index 18dc14b..b22866b 100644 --- a/examples/local_federation/actix_web/http.rs +++ b/examples/local_federation/actix_web/http.rs @@ -14,9 +14,11 @@ use activitypub_federation::{ use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer}; use anyhow::anyhow; use serde::Deserialize; +use tracing::info; pub fn listen(config: &FederationConfig) -> Result<(), Error> { let hostname = config.hostname(); + info!("Listening with actix-web on {hostname}"); let config = config.clone(); let server = HttpServer::new(move || { App::new() diff --git a/examples/local_federation/axum/http.rs b/examples/local_federation/axum/http.rs index 95bfa81..f9e9dbe 100644 --- a/examples/local_federation/axum/http.rs +++ b/examples/local_federation/axum/http.rs @@ -20,9 +20,11 @@ use axum::{ use axum_macros::debug_handler; use serde::Deserialize; use std::net::ToSocketAddrs; +use tracing::info; pub fn listen(config: &FederationConfig) -> Result<(), Error> { let hostname = config.hostname(); + info!("Listening with axum on {hostname}"); let config = config.clone(); let app = Router::new() .route("/:user/inbox", post(http_post_user_inbox)) diff --git a/examples/local_federation/instance.rs b/examples/local_federation/instance.rs index 833d007..1c2ecfa 100644 --- a/examples/local_federation/instance.rs +++ b/examples/local_federation/instance.rs @@ -5,7 +5,10 @@ use crate::{ use activitypub_federation::config::{FederationConfig, UrlVerifier}; use anyhow::anyhow; use async_trait::async_trait; -use std::sync::{Arc, Mutex}; +use std::{ + str::FromStr, + sync::{Arc, Mutex}, +}; use url::Url; pub fn new_instance( @@ -48,14 +51,31 @@ impl UrlVerifier for MyUrlVerifier { } } -pub fn listen(config: &FederationConfig) -> Result<(), Error> { - if cfg!(feature = "actix-web") == cfg!(feature = "axum") { - panic!("Exactly one of features \"actix-web\" and \"axum\" must be enabled"); +pub enum Webserver { + Axum, + ActixWeb, +} + +impl FromStr for Webserver { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "axum" => Webserver::Axum, + "actix-web" => Webserver::ActixWeb, + _ => panic!("Invalid webserver parameter, must be either `axum` or `actix-web`"), + }) + } +} + +pub fn listen( + config: &FederationConfig, + webserver: &Webserver, +) -> Result<(), Error> { + match webserver { + Webserver::Axum => crate::axum::http::listen(config)?, + Webserver::ActixWeb => crate::actix_web::http::listen(config)?, } - #[cfg(feature = "actix-web")] - crate::actix_web::http::listen(config)?; - #[cfg(feature = "axum")] - crate::axum::http::listen(config)?; Ok(()) } diff --git a/examples/local_federation/main.rs b/examples/local_federation/main.rs index 680cbc7..6f65ea7 100644 --- a/examples/local_federation/main.rs +++ b/examples/local_federation/main.rs @@ -1,9 +1,10 @@ use crate::{ - instance::{listen, new_instance}, + instance::{listen, new_instance, Webserver}, objects::post::DbPost, utils::generate_object_id, }; use error::Error; +use std::{env::args, str::FromStr}; use tracing::log::{info, LevelFilter}; mod activities; @@ -24,11 +25,16 @@ async fn main() -> Result<(), Error> { .format_timestamp(None) .init(); - info!("Starting local instances alpha and beta on localhost:8001, localhost:8002"); + info!("Start with parameter `axum` or `actix-web` to select the webserver"); + let webserver = args() + .nth(1) + .map(|arg| Webserver::from_str(&arg).unwrap()) + .unwrap_or(Webserver::Axum); + let alpha = new_instance("localhost:8001", "alpha".to_string())?; let beta = new_instance("localhost:8002", "beta".to_string())?; - listen(&alpha)?; - listen(&beta)?; + listen(&alpha, &webserver)?; + listen(&beta, &webserver)?; info!("Local instances started"); info!("Alpha user follows beta user via webfinger"); diff --git a/src/config.rs b/src/config.rs index 4e956b7..8eade7d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,7 @@ use crate::{ core::activity_queue::create_activity_queue, error::Error, traits::ActivityHandler, - utils::verify_domains_match, + protocol::verification::verify_domains_match, }; use async_trait::async_trait; use background_jobs::Manager; @@ -78,6 +78,7 @@ pub struct FederationConfig { } impl FederationConfig { + /// Returns a new config builder with default values. pub fn builder() -> FederationConfigBuilder { FederationConfigBuilder::default() } @@ -135,6 +136,11 @@ impl FederationConfig { )); } + // Urls which use our local domain are not a security risk, no further verification needed + if self.is_local_url(url) { + return Ok(()); + } + self.url_verifier .verify(url) .await @@ -160,6 +166,10 @@ impl FederationConfig { } impl FederationConfigBuilder { + /// Constructs a new config instance with the values supplied to builder. + /// + /// Values which are not explicitly specified use the defaults. Also initializes the + /// queue for outgoing activities, which is stored internally in the config struct. pub fn build(&mut self) -> Result, FederationConfigBuilderError> { let mut config = self.partial_build()?; let queue = create_activity_queue( @@ -190,7 +200,7 @@ impl Deref for FederationConfig { /// /// ``` /// # use async_trait::async_trait; -/// use url::Url; +/// # use url::Url; /// # use activitypub_federation::config::UrlVerifier; /// # #[derive(Clone)] /// # struct DatabaseConnection(); @@ -217,6 +227,7 @@ impl Deref for FederationConfig { /// ``` #[async_trait] pub trait UrlVerifier: DynClone + Send { + /// Should return Ok iff the given url is valid for processing. async fn verify(&self, url: &Url) -> Result<(), &'static str>; } @@ -245,9 +256,12 @@ pub struct RequestData { } impl RequestData { + /// Returns the data which was stored in [FederationConfigBuilder::app_data] pub fn app_data(&self) -> &T { &self.config.app_data } + + /// Returns the domain that was configured in [FederationConfig]. pub fn domain(&self) -> &str { &self.config.domain } @@ -261,10 +275,12 @@ impl Deref for RequestData { } } +/// Middleware for HTTP handlers which provides access to [RequestData] #[derive(Clone)] pub struct ApubMiddleware(pub(crate) FederationConfig); impl ApubMiddleware { + /// Construct a new middleware instance pub fn new(config: FederationConfig) -> Self { ApubMiddleware(config) } diff --git a/src/core/actix_web/inbox.rs b/src/core/actix_web/inbox.rs index 9aa4b2c..d860043 100644 --- a/src/core/actix_web/inbox.rs +++ b/src/core/actix_web/inbox.rs @@ -11,7 +11,9 @@ use actix_web::{web::Bytes, HttpRequest, HttpResponse}; use serde::de::DeserializeOwned; use tracing::debug; -/// Receive an activity and perform some basic checks, including HTTP signature verification. +/// Handles incoming activities, verifying HTTP signatures and other checks +/// +/// After successful validation, activities are passed to respective [trait@ActivityHandler]. pub async fn receive_activity( request: HttpRequest, body: Bytes, diff --git a/src/core/actix_web/middleware.rs b/src/core/actix_web/middleware.rs index 884b729..db8e0f5 100644 --- a/src/core/actix_web/middleware.rs +++ b/src/core/actix_web/middleware.rs @@ -29,6 +29,8 @@ where } } +/// Passes [FederationConfig] to HTTP handlers, converting it to [RequestData] in the process +#[doc(hidden)] pub struct ApubService where S: Service, diff --git a/src/core/actix_web/mod.rs b/src/core/actix_web/mod.rs index c3d734a..b4ca9a6 100644 --- a/src/core/actix_web/mod.rs +++ b/src/core/actix_web/mod.rs @@ -1,2 +1,4 @@ +/// Handles incoming activities, verifying HTTP signatures and other checks pub mod inbox; +#[doc(hidden)] pub mod middleware; diff --git a/src/core/axum/inbox.rs b/src/core/axum/inbox.rs index afd51e1..ad7c892 100644 --- a/src/core/axum/inbox.rs +++ b/src/core/axum/inbox.rs @@ -11,7 +11,9 @@ use crate::{ use serde::de::DeserializeOwned; use tracing::debug; -/// Receive an activity and perform some basic checks, including HTTP signature verification. +/// Handles incoming activities, verifying HTTP signatures and other checks +/// +/// After successful validation, activities are passed to respective [trait@ActivityHandler]. pub async fn receive_activity( activity_data: ActivityData, data: &RequestData, diff --git a/src/core/axum/json.rs b/src/core/axum/json.rs index 34775d8..cf81d83 100644 --- a/src/core/axum/json.rs +++ b/src/core/axum/json.rs @@ -3,8 +3,7 @@ use axum::response::IntoResponse; use http::header; use serde::Serialize; -/// A wrapper struct to respond with [`APUB_JSON_CONTENT_TYPE`] -/// in axum handlers +/// Wrapper struct to respond with `application/activity+json` in axum handlers /// /// ``` /// # use anyhow::Error; diff --git a/src/core/axum/middleware.rs b/src/core/axum/middleware.rs index 5817381..cf4d473 100644 --- a/src/core/axum/middleware.rs +++ b/src/core/axum/middleware.rs @@ -15,6 +15,8 @@ impl Layer for ApubMiddleware { } } +/// Passes [FederationConfig] to HTTP handlers, converting it to [RequestData] in the process +#[doc(hidden)] #[derive(Clone)] pub struct ApubService { inner: S, diff --git a/src/core/axum/mod.rs b/src/core/axum/mod.rs index 21c74b0..8407dc0 100644 --- a/src/core/axum/mod.rs +++ b/src/core/axum/mod.rs @@ -7,8 +7,11 @@ use axum::{ }; use http::{HeaderMap, Method, Uri}; +/// Handles incoming activities, verifying HTTP signatures and other checks pub mod inbox; +/// Wrapper struct to respond with `application/activity+json` in axum handlers pub mod json; +#[doc(hidden)] pub mod middleware; /// Contains everything that is necessary to verify HTTP signatures and receive an diff --git a/src/core/http_signatures.rs b/src/core/http_signatures.rs index 4de9a5d..c131d6e 100644 --- a/src/core/http_signatures.rs +++ b/src/core/http_signatures.rs @@ -23,7 +23,9 @@ static HTTP_SIG_CONFIG: OnceCell = OnceCell::new(); /// A private/public key pair used for HTTP signatures #[derive(Debug, Clone)] pub struct Keypair { + /// Private key in PEM format pub private_key: String, + /// Public key in PEM format pub public_key: String, } diff --git a/src/core/mod.rs b/src/core/mod.rs index 7354b1c..75120b5 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,9 +1,14 @@ +/// Queue for sending outgoing activities pub mod activity_queue; +/// Everything related to creation and verification of HTTP signatures, used to authenticate activities pub mod http_signatures; +/// Typed wrapper for Activitypub Object ID which helps with dereferencing and caching pub mod object_id; +/// Utilities for using this library with axum web framework #[cfg(feature = "axum")] pub mod axum; +/// Utilities for using this library with actix-web framework #[cfg(feature = "actix-web")] pub mod actix_web; diff --git a/src/core/object_id.rs b/src/core/object_id.rs index 423cfaf..a9ac26e 100644 --- a/src/core/object_id.rs +++ b/src/core/object_id.rs @@ -8,7 +8,7 @@ use std::{ }; use url::Url; -/// Typed wrapper for Activitypub Object ID. +/// Typed wrapper for Activitypub Object ID which helps with dereferencing and caching. /// /// It provides convenient methods for fetching the object from remote server or local database. /// Objects are automatically cached locally, so they don't have to be fetched every time. Much of @@ -63,10 +63,12 @@ where Ok(ObjectId(Box::new(url.try_into()?), PhantomData::)) } + /// Returns a reference to the wrapped URL value pub fn inner(&self) -> &Url { &self.0 } + /// Returns the wrapped URL value pub fn into_inner(self) -> Url { *self.0 } diff --git a/src/lib.rs b/src/lib.rs index fb0878f..8199bd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,19 @@ #![doc = include_str!("../README.md")] - -//#![deny(missing_docs)] +#![deny(missing_docs)] /// Configuration for this library pub mod config; /// Contains main library functionality pub mod core; +/// Error messages returned by this library. pub mod error; /// Data structures which help to define federated messages pub mod protocol; /// Traits which need to be implemented for federated data types pub mod traits; +/// Some utility functions pub mod utils; +/// Resolves identifiers of the form `name@example.com` pub mod webfinger; pub use activitystreams_kinds as kinds; diff --git a/src/protocol/context.rs b/src/protocol/context.rs index dd12cda..387c6c3 100644 --- a/src/protocol/context.rs +++ b/src/protocol/context.rs @@ -52,6 +52,7 @@ impl WithContext { WithContext { context, inner } } + /// Returns the inner `T` object which this `WithContext` object is wrapping pub fn inner(&self) -> &T { &self.inner } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 4f4f071..8fcfd7e 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,4 +1,9 @@ +/// Wrapper for federated structs which handles `@context` field pub mod context; +/// Serde deserialization functions which help to receive differently shaped data pub mod helpers; +/// Struct which is used to federate actor key for HTTP signatures pub mod public_key; pub mod values; +/// Verify that received data is valid +pub mod verification; diff --git a/src/protocol/public_key.rs b/src/protocol/public_key.rs index ddc258f..601af40 100644 --- a/src/protocol/public_key.rs +++ b/src/protocol/public_key.rs @@ -1,17 +1,24 @@ use serde::{Deserialize, Serialize}; use url::Url; -/// Public key of actors which is used for HTTP signatures. This needs to be federated in the -/// `public_key` field of all actors. +/// Public key of actors which is used for HTTP signatures. +/// +/// This needs to be federated in the `public_key` field of all actors. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PublicKey { - pub(crate) id: String, - pub(crate) owner: Url, + /// Id of this private key. + pub id: String, + /// ID of the actor that this public key belongs to + pub owner: Url, + /// The actual public key in PEM format pub public_key_pem: String, } impl PublicKey { + /// Create a new [PublicKey] struct for the `owner` with `public_key_pem`. + /// + /// It uses an standard key id of `{actor_id}#main-key` pub fn new(owner: Url, public_key_pem: String) -> Self { let id = main_key_id(&owner); PublicKey { diff --git a/src/protocol/values.rs b/src/protocol/values.rs index fb83b85..c85303e 100644 --- a/src/protocol/values.rs +++ b/src/protocol/values.rs @@ -38,6 +38,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Clone, Debug, Deserialize, Serialize)] pub enum MediaTypeMarkdown { + /// `text/markdown` #[serde(rename = "text/markdown")] Markdown, } @@ -47,6 +48,7 @@ pub enum MediaTypeMarkdown { /// #[derive(Clone, Debug, Deserialize, Serialize)] pub enum MediaTypeHtml { + /// `text/html` #[serde(rename = "text/html")] Html, } @@ -54,8 +56,10 @@ pub enum MediaTypeHtml { /// Media type which allows both markdown and HTML. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub enum MediaTypeMarkdownOrHtml { + /// `text/markdown` #[serde(rename = "text/markdown")] Markdown, + /// `text/html` #[serde(rename = "text/html")] Html, } diff --git a/src/protocol/verification.rs b/src/protocol/verification.rs new file mode 100644 index 0000000..c21115a --- /dev/null +++ b/src/protocol/verification.rs @@ -0,0 +1,38 @@ +use crate::{ + error::Error, +}; +use url::Url; + +/// Check that both urls have the same domain. If not, return UrlVerificationError. +/// +/// ``` +/// # use url::Url; +/// # use activitypub_federation::utils::verify_domains_match; +/// let a = Url::parse("https://example.com/abc")?; +/// let b = Url::parse("https://sample.net/abc")?; +/// assert!(verify_domains_match(&a, &b).is_err()); +/// # Ok::<(), url::ParseError>(()) +/// ``` +pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), Error> { + if a.domain() != b.domain() { + return Err(Error::UrlVerificationError("Domains do not match")); + } + Ok(()) +} + +/// Check that both urls are identical. If not, return UrlVerificationError. +/// +/// ``` +/// # use url::Url; +/// # use activitypub_federation::utils::verify_urls_match; +/// let a = Url::parse("https://example.com/abc")?; +/// let b = Url::parse("https://example.com/123")?; +/// assert!(verify_urls_match(&a, &b).is_err()); +/// # Ok::<(), url::ParseError>(()) +/// ``` +pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> { + if a != b { + return Err(Error::UrlVerificationError("Urls do not match")); + } + Ok(()) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1bd2c41..e987d24 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -52,37 +52,3 @@ pub async fn fetch_object_http( res.json_limited().await } - -/// Check that both urls have the same domain. If not, return UrlVerificationError. -/// -/// ``` -/// # use url::Url; -/// # use activitypub_federation::utils::verify_domains_match; -/// let a = Url::parse("https://example.com/abc")?; -/// let b = Url::parse("https://sample.net/abc")?; -/// assert!(verify_domains_match(&a, &b).is_err()); -/// # Ok::<(), url::ParseError>(()) -/// ``` -pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), Error> { - if a.domain() != b.domain() { - return Err(Error::UrlVerificationError("Domains do not match")); - } - Ok(()) -} - -/// Check that both urls are identical. If not, return UrlVerificationError. -/// -/// ``` -/// # use url::Url; -/// # use activitypub_federation::utils::verify_urls_match; -/// let a = Url::parse("https://example.com/abc")?; -/// let b = Url::parse("https://example.com/123")?; -/// assert!(verify_urls_match(&a, &b).is_err()); -/// # Ok::<(), url::ParseError>(()) -/// ``` -pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> { - if a != b { - return Err(Error::UrlVerificationError("Urls do not match")); - } - Ok(()) -} diff --git a/src/webfinger.rs b/src/webfinger.rs index 72fabc8..064a790 100644 --- a/src/webfinger.rs +++ b/src/webfinger.rs @@ -14,8 +14,10 @@ use std::collections::HashMap; use tracing::debug; use url::Url; -/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, -/// using webfinger. +/// Takes an identifier of the form `name@example.com`, and returns an object of `Kind`. +/// +/// For this the identifier is first resolved via webfinger protocol to an Activitypub ID. This ID +/// is then fetched using [ObjectId::dereference], and the result returned. pub async fn webfinger_resolve_actor( identifier: &str, data: &RequestData, @@ -29,7 +31,7 @@ where let (_, domain) = identifier .splitn(2, '@') .collect_tuple() - .ok_or_else(|| WebfingerResolveFailed)?; + .ok_or(WebfingerResolveFailed)?; let protocol = if data.config.debug { "http" } else { "https" }; let fetch_url = format!("{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}"); @@ -37,6 +39,7 @@ where let res: Webfinger = fetch_object_http(&Url::parse(&fetch_url)?, data).await?; + debug_assert_eq!(res.subject, format!("acct:{identifier}")); let links: Vec = res .links .iter() @@ -60,7 +63,8 @@ where /// Extracts username from a webfinger resource parameter. /// -/// For a parameter of the form `acct:gargron@mastodon.social` it returns `gargron`. +/// Use this method for your HTTP handler at `.well-known/webfinger` to handle incoming webfinger +/// request. For a parameter of the form `acct:gargron@mastodon.social` it returns `gargron`. /// /// Returns an error if query doesn't match local domain. pub fn extract_webfinger_name(query: &str, data: &RequestData) -> Result @@ -78,11 +82,23 @@ where .to_string()) } -/// Builds a basic webfinger response under the assumption that `html` and `activity+json` -/// links are identical. -pub fn build_webfinger_response(resource: String, url: Url) -> Webfinger { +/// Builds a basic webfinger response for the actor. +/// +/// It assumes that the given URL is valid both to the view the actor in a browser as HTML, and +/// for fetching it over Activitypub with `activity+json`. This setup is commonly used for ease +/// of discovery. +/// +/// ``` +/// # use url::Url; +/// # use activitypub_federation::webfinger::build_webfinger_response; +/// let subject = "acct:nutomic@lemmy.ml".to_string(); +/// let url = Url::parse("https://lemmy.ml/u/nutomic")?; +/// build_webfinger_response(subject, url); +/// # Ok::<(), anyhow::Error>(()) +/// ``` +pub fn build_webfinger_response(subject: String, url: Url) -> Webfinger { Webfinger { - subject: resource, + subject, links: vec![ WebfingerLink { rel: Some("http://webfinger.net/rel/profile-page".to_string()), @@ -97,23 +113,39 @@ pub fn build_webfinger_response(resource: String, url: Url) -> Webfinger { properties: Default::default(), }, ], + aliases: vec![], + properties: Default::default(), } } +/// A webfinger response with information about a `Person` or other type of actor. #[derive(Serialize, Deserialize, Debug)] pub struct Webfinger { + /// The actor which is described here, for example `acct:LemmyDev@mastodon.social` pub subject: String, + /// Links where further data about `subject` can be retrieved pub links: Vec, + /// Other Urls which identify the same actor as the `subject` + #[serde(default)] + pub aliases: Vec, + /// Additional data about the subject + #[serde(default)] + pub properties: HashMap, } +/// A single link included as part of a [Webfinger] response. #[derive(Serialize, Deserialize, Debug)] pub struct WebfingerLink { + /// Relationship of the link, such as `self` or `http://webfinger.net/rel/profile-page` pub rel: Option, + /// Media type of the target resource #[serde(rename = "type")] pub kind: Option, + /// Url pointing to the target resource pub href: Option, + /// Additional data about the link #[serde(default)] - pub properties: HashMap, + pub properties: HashMap, } #[cfg(test)]