]> Untitled Git - lemmy.git/blob - crates/apub/src/fetcher/webfinger.rs
80dd3bc6ebc8129dc961a6bdc07e778b6b0da84e
[lemmy.git] / crates / apub / src / fetcher / webfinger.rs
1 use crate::{local_instance, ActorType};
2 use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
3 use anyhow::anyhow;
4 use itertools::Itertools;
5 use lemmy_db_schema::newtypes::DbUrl;
6 use lemmy_utils::error::LemmyError;
7 use lemmy_websocket::LemmyContext;
8 use serde::{Deserialize, Serialize};
9 use tracing::debug;
10 use url::Url;
11
12 #[derive(Serialize, Deserialize, Debug)]
13 pub struct WebfingerLink {
14   pub rel: Option<String>,
15   #[serde(rename = "type")]
16   pub kind: Option<String>,
17   pub href: Option<Url>,
18 }
19
20 #[derive(Serialize, Deserialize, Debug)]
21 pub struct WebfingerResponse {
22   pub subject: String,
23   pub links: Vec<WebfingerLink>,
24 }
25
26 /// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
27 /// using webfinger.
28 #[tracing::instrument(skip_all)]
29 pub(crate) async fn webfinger_resolve_actor<Kind>(
30   identifier: &str,
31   context: &LemmyContext,
32   request_counter: &mut i32,
33 ) -> Result<DbUrl, LemmyError>
34 where
35   Kind: ApubObject<DataType = LemmyContext, Error = LemmyError> + ActorType + Send + 'static,
36   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
37 {
38   let protocol = context.settings().get_protocol_string();
39   let (_, domain) = identifier
40     .splitn(2, '@')
41     .collect_tuple()
42     .ok_or_else(|| LemmyError::from_message("Invalid webfinger query, missing domain"))?;
43   let fetch_url = format!(
44     "{}://{}/.well-known/webfinger?resource=acct:{}",
45     protocol, domain, identifier
46   );
47   debug!("Fetching webfinger url: {}", &fetch_url);
48
49   *request_counter += 1;
50   if *request_counter > context.settings().federation.http_fetch_retry_limit {
51     return Err(LemmyError::from_message("Request retry limit reached"));
52   }
53
54   let response = context.client().get(&fetch_url).send().await?;
55
56   let res: WebfingerResponse = response.json().await.map_err(LemmyError::from)?;
57
58   let links: Vec<Url> = res
59     .links
60     .iter()
61     .filter(|link| {
62       if let Some(type_) = &link.kind {
63         type_.starts_with("application/")
64       } else {
65         false
66       }
67     })
68     .filter_map(|l| l.href.clone())
69     .collect();
70   for l in links {
71     let object = ObjectId::<Kind>::new(l)
72       .dereference(context, local_instance(context), request_counter)
73       .await;
74     if object.is_ok() {
75       return object.map(|o| o.actor_id().into());
76     }
77   }
78   let err = anyhow!("Failed to resolve actor for {}", identifier);
79   Err(LemmyError::from_error_message(err, "failed_to_resolve"))
80 }