1use reqwest::{header::ACCEPT, Client};
6use serde::{Deserialize, Serialize};
7
8mod resolver;
9pub use crate::resolver::*;
10
11#[cfg(feature = "async")]
12mod async_resolver;
13#[cfg(feature = "async")]
14pub use crate::async_resolver::*;
15
16#[cfg(test)]
17mod tests;
18
19#[derive(Debug, Serialize, Deserialize, PartialEq)]
21pub struct Webfinger {
22 pub subject: String,
26
27 #[serde(default)]
29 pub aliases: Vec<String>,
30
31 pub links: Vec<Link>,
33}
34
35#[derive(Debug, Serialize, Deserialize, PartialEq)]
37pub struct Link {
38 pub rel: String,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub href: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub template: Option<String>,
48
49 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
54 pub mime_type: Option<String>,
55}
56
57#[derive(Debug, PartialEq)]
59pub enum WebfingerError {
60 HttpError,
62
63 ParseError,
65
66 JsonError,
68}
69
70#[derive(Debug, PartialEq)]
72pub enum Prefix {
73 Acct,
75 Group,
77 Custom(String),
79}
80
81impl From<&str> for Prefix {
82 fn from(s: &str) -> Prefix {
83 match s.to_lowercase().as_ref() {
84 "acct" => Prefix::Acct,
85 "group" => Prefix::Group,
86 x => Prefix::Custom(x.into()),
87 }
88 }
89}
90
91impl Into<String> for Prefix {
92 fn into(self) -> String {
93 match self {
94 Prefix::Acct => "acct".into(),
95 Prefix::Group => "group".into(),
96 Prefix::Custom(x) => x,
97 }
98 }
99}
100
101pub fn url_for(
110 prefix: Prefix,
111 acct: impl Into<String>,
112 with_https: bool,
113) -> Result<String, WebfingerError> {
114 let acct = acct.into();
115 let scheme = if with_https { "https" } else { "http" };
116
117 let prefix: String = prefix.into();
118 acct.split('@')
119 .nth(1)
120 .ok_or(WebfingerError::ParseError)
121 .map(|instance| {
122 format!(
123 "{}://{}/.well-known/webfinger?resource={}:{}",
124 scheme, instance, prefix, acct
125 )
126 })
127}
128
129pub async fn resolve_with_prefix(
131 prefix: Prefix,
132 acct: impl Into<String>,
133 with_https: bool,
134) -> Result<Webfinger, WebfingerError> {
135 let url = url_for(prefix, acct, with_https)?;
136 Client::new()
137 .get(&url[..])
138 .header(ACCEPT, "application/jrd+json, application/json")
139 .send()
140 .await
141 .map_err(|_| WebfingerError::HttpError)?
142 .json()
143 .await
144 .map_err(|_| WebfingerError::JsonError)
145}
146
147pub async fn resolve(
151 acct: impl Into<String>,
152 with_https: bool,
153) -> Result<Webfinger, WebfingerError> {
154 let acct = acct.into();
155 let mut parsed = acct.splitn(2, ':');
156 let first = parsed.next().ok_or(WebfingerError::ParseError)?;
157
158 if first.contains('@') {
159 resolve_with_prefix(Prefix::Acct, acct, with_https).await
161 } else if let Some(other) = parsed.next() {
162 resolve_with_prefix(Prefix::from(first), other, with_https).await
163 } else {
164 resolve_with_prefix(Prefix::Acct, first, with_https).await
166 }
167}
168
169#[derive(Debug, PartialEq)]
171pub enum ResolverError {
172 InvalidResource,
174
175 WrongDomain,
177
178 NotFound,
180}