use super::{actor::ID, util, CLIENT, DOMAIN}; use anyhow::anyhow; use axum::{ extract::{Form, Query, RequestParts}, response::{IntoResponse, Redirect, Response}, Json, }; use hyper::{header::ACCEPT, Body, Request, StatusCode}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; static ACCT: Lazy = Lazy::new(|| format!("blog@{}", *DOMAIN)); pub async fn handle(req: Request) -> Response { if is_resource_blog(req).await { Json(WebfingerResponse::new()).into_response() } else { // mastodon responds with 400 and an empty body, so we do too StatusCode::BAD_REQUEST.into_response() } } pub async fn handle_interact( Form(InteractParams { acct, permalink }): Form, ) -> Response { if let Ok(response) = query(&acct).await { for link in response.links { if let Link { payload: LinkPayload::OStatusSubscribe { template }, .. } = link { let url = template.replace("{uri}", &format!("https://{}{}", &*DOMAIN, permalink)); return Redirect::to(&url).into_response(); } } } ( StatusCode::BAD_REQUEST, "Unable to find remote subscribe URL", ) .into_response() } async fn is_resource_blog(req: Request) -> bool { let mut parts = RequestParts::new(req); if let Ok(Query(params)) = parts.extract::>().await { let resource = if params.resource.starts_with("acct:") { ¶ms.resource[5..] } else { ¶ms.resource }; resource == ACCT.as_str() } else { false } } async fn query(acct: &str) -> anyhow::Result { let (_, domain) = util::parse_acct(acct).ok_or(anyhow!("invalid acct"))?; let response = CLIENT .get(format!( "https://{}/.well-known/webfinger?resource={}", domain, acct )) .header(ACCEPT, "application/json") .send() .await?; Ok(response.json().await?) } #[derive(Deserialize)] pub struct InteractParams { permalink: String, #[serde(rename = "remote_follow[acct]")] acct: String, } #[derive(Deserialize)] pub struct Params { resource: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct WebfingerResponse { subject: String, links: Vec, } impl WebfingerResponse { fn new() -> Self { Self { subject: ACCT.to_owned(), links: vec![Link { rel: "self".to_owned(), payload: LinkPayload::Webfinger { r#type: "application/activity+json".to_owned(), href: ID.to_string(), }, }], } } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Link { rel: String, #[serde(flatten)] payload: LinkPayload, } #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum LinkPayload { Webfinger { r#type: String, href: String }, OStatusSubscribe { template: String }, } #[cfg(test)] mod tests { use super::*; #[test] fn test_deserialize_link_payloads() { let json = r#" { "subject": "acct:Gargron@mastodon.social", "aliases": [ "https://mastodon.social/@Gargron", "https://mastodon.social/users/Gargron" ], "links": [ { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://mastodon.social/@Gargron" }, { "rel": "self", "type": "application/activity+json", "href": "https://mastodon.social/users/Gargron" }, { "rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://mastodon.social/authorize_interaction?uri={uri}" } ] } "#; let response: WebfingerResponse = serde_json::from_str(&json).unwrap(); assert_eq!( response, WebfingerResponse { subject: "acct:Gargron@mastodon.social".to_owned(), links: vec![ Link { rel: "http://webfinger.net/rel/profile-page".to_owned(), payload: LinkPayload::Webfinger { r#type: "text/html".to_owned(), href: "https://mastodon.social/@Gargron".to_owned() } }, Link { rel: "self".to_owned(), payload: LinkPayload::Webfinger { r#type: "application/activity+json".to_owned(), href: "https://mastodon.social/users/Gargron".to_owned() } }, Link { rel: "http://ostatus.org/schema/1.0/subscribe".to_owned(), payload: LinkPayload::OStatusSubscribe { template: "https://mastodon.social/authorize_interaction?uri={uri}" .to_owned() } }, ] } ); } }