8802779ebcb7d4726342c28944c06e17b4a38e82 / frogue / src / activitypub / webfinger.rs
Raw
//! Webfinger endpoint for ActivityPub actor discovery.
//!
//! `GET /.well-known/webfinger?resource=acct:{user}@{host}` returns a JRD
//! document pointing to the actor URL.
use actix_web::HttpResponse;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use serde::Serialize;
use crate::entities::prelude::Users;
use crate::entities::users;
use super::ApUrls;
/// JRD (JSON Resource Descriptor) for webfinger.
#[derive(Serialize)]
struct Jrd {
subject: String,
links: Vec<Link>,
}
#[derive(Serialize)]
struct Link {
rel: String,
r#type: String,
href: String,
}
/// The actual webfinger logic, separated for testability.
pub async fn webfinger(
db: &DatabaseConnection,
resource: &str,
public_url: &str,
) -> actix_web::HttpResponse {
if !enabled_from_url(public_url) {
return actix_web::HttpResponse::NotFound().finish();
}
let urls = ApUrls::new(public_url);
let host = urls.host();
// Parse the resource parameter: must be "acct:{user}@{host}".
let Some(user_and_host) = resource.strip_prefix("acct:") else {
return HttpResponse::BadRequest().body("resource must start with acct:");
};
let Some((user, resource_host)) = user_and_host.split_once('@') else {
return HttpResponse::BadRequest().body("resource must be acct:user@host");
};
// Host must match.
if resource_host != host {
return HttpResponse::NotFound().finish();
}
// Look up the user.
let user_model = match Users::find()
.filter(users::Column::Name.eq(user))
.filter(users::Column::DeletedAt.is_null())
.one(db)
.await
{
Ok(Some(user)) => user,
Ok(None) => return HttpResponse::NotFound().finish(),
Err(_) => return HttpResponse::InternalServerError().finish(),
};
let actor_url = urls.actor(&user_model.name);
let jrd = Jrd {
subject: format!("acct:{}@{}", user, host),
links: vec![Link {
rel: "self".to_string(),
r#type: "application/activity+json".to_string(),
href: actor_url,
}],
};
HttpResponse::Ok()
.content_type("application/jrd+json")
.json(jrd)
}
fn enabled_from_url(public_url: &str) -> bool {
!public_url.is_empty()
}