164 lines
5.1 KiB
Rust
164 lines
5.1 KiB
Rust
use axum::extract::{Query, State};
|
|
use axum::http::StatusCode;
|
|
use axum::response::IntoResponse;
|
|
use axum::Json;
|
|
use hyper::header;
|
|
use magnetar_common::config::MagnetarConfig;
|
|
use magnetar_common::util::{lenient_parse_acct_decode, FediverseTag};
|
|
use magnetar_core::web_model::acct::Acct;
|
|
use magnetar_core::web_model::content_type::{
|
|
ContentActivityJson, ContentActivityStreams, ContentHtml, ContentJrdJson,
|
|
};
|
|
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
|
|
use magnetar_model::CalckeyModel;
|
|
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
|
|
use serde::Deserialize;
|
|
use tracing::error;
|
|
use url::Url;
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct WebFingerQuery {
|
|
resource: WebFingerSubject,
|
|
rel: Option<Vec<String>>,
|
|
}
|
|
|
|
// TODO: We don't have this endpoint in Magnetar yet, but make sure it's not hardcoded when
|
|
// we do
|
|
const USER_AP_ENDPOINT: &str = "/users/";
|
|
|
|
// TODO: Filter by rel
|
|
pub async fn handle_webfinger(
|
|
Query(WebFingerQuery { resource, .. }): Query<WebFingerQuery>,
|
|
State((config, ck)): State<(&'static MagnetarConfig, CalckeyModel)>,
|
|
) -> Result<impl IntoResponse, StatusCode> {
|
|
let resource = match resource {
|
|
acct @ WebFingerSubject::Acct(_) => acct,
|
|
// Leniently re-add the acct
|
|
WebFingerSubject::Url(url) if !url.starts_with("http:") && !url.starts_with("https:") => {
|
|
WebFingerSubject::Acct(Acct::new(url.into()))
|
|
}
|
|
other => other,
|
|
};
|
|
|
|
let user = match resource {
|
|
WebFingerSubject::Acct(acct) => {
|
|
let tag = lenient_parse_acct_decode(&acct).map_err(|e| {
|
|
error!("Failed to parse tag: {e}");
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
})?;
|
|
|
|
ck.get_user_by_tag(
|
|
tag.name.as_ref(),
|
|
tag.host
|
|
.as_ref()
|
|
.filter(|host| host.to_string() != config.networking.host),
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Data error: {e}");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?
|
|
}
|
|
WebFingerSubject::Url(url) => {
|
|
let object_url = url.parse::<Url>().map_err(|e| {
|
|
error!("URL parse error: {e}");
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
})?;
|
|
|
|
// FIXME: Jank
|
|
let path = object_url.path().strip_prefix(USER_AP_ENDPOINT);
|
|
match path {
|
|
Some(user_id) if !user_id.is_empty() && user_id.chars().all(|c| c != '/') => {
|
|
ck.get_user_by_id(user_id).await.map_err(|e| {
|
|
error!("Data error: {e}");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?
|
|
}
|
|
_ => ck.get_user_by_uri(&url).await.map_err(|e| {
|
|
error!("Data error: {e}");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?,
|
|
}
|
|
}
|
|
};
|
|
|
|
let Some(user) = user else {
|
|
return Err(StatusCode::NOT_FOUND);
|
|
};
|
|
|
|
let tag = FediverseTag::from_parts((
|
|
&user.username,
|
|
user.host.as_deref().or(Some(&config.networking.host)),
|
|
))
|
|
.map_err(|e| {
|
|
error!("URL parse error: {e}");
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
})?;
|
|
|
|
let mut links = Vec::new();
|
|
let mut aliases = Vec::new();
|
|
|
|
if user.host.is_some() {
|
|
if let Some(uri) = user.uri {
|
|
links.push(WebFingerRel::RelSelf {
|
|
rel: RelSelf,
|
|
content_type: ContentActivityStreams,
|
|
href: uri.clone(),
|
|
});
|
|
|
|
links.push(WebFingerRel::RelSelfAlt {
|
|
rel: RelSelf,
|
|
content_type: ContentActivityJson,
|
|
href: uri,
|
|
});
|
|
}
|
|
} else {
|
|
links.push(WebFingerRel::RelOStatusSubscribe {
|
|
rel: RelOStatusSubscribe,
|
|
template: format!(
|
|
"{}://{}/authorize-follow?acct={{uri}}",
|
|
config.networking.protocol, config.networking.host
|
|
),
|
|
});
|
|
|
|
let user_url = format!(
|
|
"{}://{}/@{}",
|
|
config.networking.protocol, config.networking.host, tag.name
|
|
);
|
|
|
|
links.push(WebFingerRel::RelWebFingerProfilePage {
|
|
rel: RelWebFingerProfilePage,
|
|
content_type: ContentHtml,
|
|
href: user_url.clone(),
|
|
});
|
|
|
|
aliases.push(WebFingerSubject::Url(user_url));
|
|
|
|
let self_url = format!(
|
|
"{}://{}{}{}",
|
|
config.networking.protocol, config.networking.host, USER_AP_ENDPOINT, user.id
|
|
);
|
|
|
|
links.push(WebFingerRel::RelSelf {
|
|
rel: RelSelf,
|
|
content_type: ContentActivityStreams,
|
|
href: self_url.clone(),
|
|
});
|
|
|
|
links.push(WebFingerRel::RelSelfAlt {
|
|
rel: RelSelf,
|
|
content_type: ContentActivityJson,
|
|
href: self_url,
|
|
});
|
|
}
|
|
|
|
Ok((
|
|
[(header::CONTENT_TYPE, ContentJrdJson.as_ref())],
|
|
Json(WebFinger {
|
|
subject: WebFingerSubject::Acct(tag.into()),
|
|
aliases: Some(aliases),
|
|
links,
|
|
}),
|
|
))
|
|
}
|