Files
k-ap/src/service/lookup.rs

154 lines
5.3 KiB
Rust

use url::Url;
use crate::{actors::DbActor, data::FederationData, repository::BlockedDomain};
use super::ActivityPubService;
struct ParsedHandle<'a> {
username: &'a str,
domain: &'a str,
}
fn parse_handle(handle: &str) -> anyhow::Result<ParsedHandle<'_>> {
let normalized = handle.trim_start_matches('@');
let separator_index = normalized
.rfind('@')
.ok_or_else(|| anyhow::anyhow!("handle must be user@domain"))?;
Ok(ParsedHandle {
username: &normalized[..separator_index],
domain: &normalized[separator_index + 1..],
})
}
fn webfinger_url(handle: &ParsedHandle<'_>) -> String {
format!(
"https://{}/.well-known/webfinger?resource=acct:{}@{}",
handle.domain, handle.username, handle.domain
)
}
async fn fetch_webfinger(url: &str) -> anyhow::Result<serde_json::Value> {
let parsed = Url::parse(url)?;
crate::security::validate_url(&parsed).await?;
crate::tls::ensure_crypto_provider();
Ok(reqwest::Client::new()
.get(url)
.header("Accept", "application/jrd+json, application/json")
.send()
.await?
.json()
.await?)
}
fn extract_actor_href(webfinger: &serde_json::Value) -> anyhow::Result<String> {
webfinger["links"]
.as_array()
.and_then(|links| {
links.iter().find(|link| {
link["rel"].as_str() == Some("self")
&& link["type"].as_str() == Some(crate::urls::AP_CONTENT_TYPE)
})
})
.and_then(|link| link["href"].as_str())
.map(|href| href.to_owned())
.ok_or_else(|| anyhow::anyhow!("no self link in WebFinger response"))
}
impl ActivityPubService {
// ── Pass-through wrappers ───────────────────────────────────────────
pub async fn mark_follower_accepted(
&self,
user_id: uuid::Uuid,
actor_url: &str,
) -> anyhow::Result<()> {
let data = self.federation_config.to_request_data();
data.follow_repo
.update_follower_status(
user_id,
actor_url,
crate::repository::FollowerStatus::Accepted,
)
.await
}
pub async fn mark_follower_rejected(
&self,
user_id: uuid::Uuid,
actor_url: &str,
) -> anyhow::Result<()> {
let data = self.federation_config.to_request_data();
data.follow_repo.remove_follower(user_id, actor_url).await
}
pub async fn add_blocked_domain(
&self,
domain: &str,
reason: Option<&str>,
) -> anyhow::Result<()> {
let data = self.federation_config.to_request_data();
data.blocklist_repo.add_blocked_domain(domain, reason).await
}
pub async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()> {
let data = self.federation_config.to_request_data();
data.blocklist_repo.remove_blocked_domain(domain).await
}
pub async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
let data = self.federation_config.to_request_data();
data.blocklist_repo.get_blocked_domains().await
}
// ── WebFinger / actor resolution ────────────────────────────────────
pub async fn lookup_actor_by_handle(
&self,
handle: &str,
) -> anyhow::Result<crate::user::LookedUpActor> {
tracing::info!(handle, "looking up remote actor");
let data = self.federation_config.to_request_data();
let actor = self
.webfinger_https(handle, &data)
.await
.inspect_err(|error| tracing::warn!(handle, %error, "actor lookup failed"))?;
let domain = actor.ap_id.host_str().unwrap_or("").to_string();
tracing::info!(handle = format!("{}@{}", actor.username, domain), ap_url = %actor.ap_id, "remote actor resolved");
Ok(crate::user::LookedUpActor {
handle: format!("{}@{}", actor.username, domain),
display_name: actor.display_name,
bio: actor.bio,
avatar_url: actor.avatar_url,
banner_url: actor.banner_url,
ap_url: actor.ap_id,
outbox_url: Some(actor.outbox_url),
followers_url: Some(actor.followers_url),
following_url: Some(actor.following_url),
also_known_as: actor.also_known_as,
profile_url: actor.profile_url,
attachment: actor.attachment,
})
}
pub(super) async fn webfinger_https(
&self,
handle: &str,
data: &activitypub_federation::config::Data<FederationData>,
) -> anyhow::Result<DbActor> {
let parsed_handle = parse_handle(handle)?;
let url = webfinger_url(&parsed_handle);
tracing::debug!(handle, webfinger_url = %url, "resolving webfinger");
let webfinger_response = fetch_webfinger(&url).await?;
let actor_href = extract_actor_href(&webfinger_response)?;
tracing::debug!(handle, actor_href, "webfinger resolved, fetching actor");
let actor: DbActor =
activitypub_federation::fetch::object_id::ObjectId::from(Url::parse(&actor_href)?)
.dereference(data)
.await?;
Ok(actor)
}
}