use crate::{ config::RequestData, error::{Error, Error::WebfingerResolveFailed}, fetch::{fetch_object_http, object_id::ObjectId}, traits::{Actor, ApubObject}, APUB_JSON_CONTENT_TYPE, }; use anyhow::anyhow; use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tracing::debug; use url::Url; /// 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, ) -> Result::Error> where Kind: ApubObject + Actor + Send + 'static + ApubObject, for<'de2> ::ApubType: serde::Deserialize<'de2>, ::Error: From + From + From + Send + Sync, { let (_, domain) = identifier .splitn(2, '@') .collect_tuple() .ok_or(WebfingerResolveFailed)?; let protocol = if data.config.debug { "http" } else { "https" }; let fetch_url = format!("{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}"); debug!("Fetching webfinger url: {}", &fetch_url); 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() .filter(|link| { if let Some(type_) = &link.kind { type_.starts_with("application/") } else { false } }) .filter_map(|l| l.href.clone()) .collect(); for l in links { let object = ObjectId::::from(l).dereference(data).await; if object.is_ok() { return object; } } Err(WebfingerResolveFailed.into()) } /// Extracts username from a webfinger resource parameter. /// /// 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 where T: Clone, { // TODO: would be nice if we could implement this without regex and remove the dependency let regex = Regex::new(&format!("^acct:([a-zA-Z0-9_]{{3,}})@{}$", data.domain())) .map_err(Error::other)?; Ok(regex .captures(query) .and_then(|c| c.get(1)) .ok_or_else(|| Error::other(anyhow!("Webfinger regex failed to match")))? .as_str() .to_string()) } /// 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::fetch::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, links: vec![ WebfingerLink { rel: Some("http://webfinger.net/rel/profile-page".to_string()), kind: Some("text/html".to_string()), href: Some(url.clone()), properties: Default::default(), }, WebfingerLink { rel: Some("self".to_string()), kind: Some(APUB_JSON_CONTENT_TYPE.to_string()), href: Some(url), 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, skip_serializing_if = "Vec::is_empty")] pub aliases: Vec, /// Additional data about the subject #[serde(default, skip_serializing_if = "HashMap::is_empty")] 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, skip_serializing_if = "HashMap::is_empty")] pub properties: HashMap, } #[cfg(test)] mod tests { use super::*; use crate::{ config::FederationConfig, traits::tests::{DbConnection, DbUser}, }; #[actix_rt::test] async fn test_webfinger() { let config = FederationConfig::builder() .domain("example.com") .app_data(DbConnection) .build() .unwrap(); let data = config.to_request_data(); let res = webfinger_resolve_actor::("LemmyDev@mastodon.social", &data) .await; assert!(res.is_ok()); } }