use crate::external_data; use crate::server::AppState; use crate::server::XrpcErrorResponse; use crate::store; use crate::xrpc::get_blob_uri; use axum::{Json, extract::State}; use jacquard::deps::fluent_uri::Uri; use jacquard::types::string::AtIdentifier; use jacquard::types::string::RecordKey; use jacquard::xrpc::XrpcExt; use jacquard_api::app_bsky::actor::get_profile::GetProfileOutput; use jacquard_api::app_bsky::actor::get_profile::GetProfileRequest; use jacquard_api::app_bsky::actor::get_profiles::GetProfilesOutput; use jacquard_api::app_bsky::actor::get_profiles::GetProfilesRequest; use jacquard_api::app_bsky::actor::profile::Profile; use jacquard_api::app_bsky::actor::{ProfileViewBasic, ProfileViewDetailed}; use jacquard_api::com_atproto::repo::get_record::GetRecord; use jacquard_axum::ExtractXrpc; use jacquard_axum::service_auth::ExtractOptionalServiceAuth; use jacquard_common::IntoStatic; use jacquard_common::types::handle; use jacquard_identity::JacquardResolver; use jacquard_identity::resolver::IdentityResolver; use serde::Deserialize; use std::str::FromStr; pub async fn app_bsky_actor_get_profile( State(state): State, ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, ExtractXrpc(req): ExtractXrpc, ) -> Result>, XrpcErrorResponse> { let profile = get_profile(state.resolver, state.pool, req.actor) .await .map_err(|err| err)?; Ok(Json(GetProfileOutput { value: profile, extra_data: None, })) } pub async fn app_bsky_actor_get_profiles( State(state): State, ExtractOptionalServiceAuth(_auth): ExtractOptionalServiceAuth, ExtractXrpc(req): ExtractXrpc, ) -> Result>, XrpcErrorResponse> { let mut results = Vec::new(); for actor in req.actors { let profile = get_profile(state.resolver.clone(), state.pool.clone(), actor) .await .map_err(|err| err)?; results.push(profile); } Ok(Json(GetProfilesOutput { profiles: results, extra_data: None, })) } async fn get_profile<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, actor: AtIdentifier<'a>, ) -> Result, XrpcErrorResponse> { let did = match actor { AtIdentifier::Did(did) => did.into_static(), AtIdentifier::Handle(h) => resolver.resolve_handle(&h).await.map_err(|err| { println!("error resolving handle: {err}"); XrpcErrorResponse::internal_server_error() })?, }; let did_doc = external_data::get_did_doc(resolver.clone(), &pool, &did.to_string()) .await .map_err(|err| { println!("getting did doc {err}"); XrpcErrorResponse::internal_server_error() })?; let handle = handle::Handle::from_str(&did_doc.handle).map_err(|err| { println!("parsing handle {err}"); XrpcErrorResponse::internal_server_error() })?; let profile = get_profile_from_pds(pool.clone(), did.to_string(), did_doc.pds_endpoint.clone()) .await .map_err(|err| { println!("getting profile: {err}"); XrpcErrorResponse::internal_server_error() })?; let avatar_uri = get_blob_uri( profile.avatar, did.to_string(), did_doc.pds_endpoint.clone(), ); let banner_uri = get_blob_uri( profile.banner, did.to_string(), did_doc.pds_endpoint.clone(), ); let follows = store::get_follows(&pool).await; Ok(ProfileViewDetailed { associated: None, avatar: avatar_uri, banner: banner_uri, created_at: None, did: did, debug: None, description: profile.description, display_name: profile.display_name, followers_count: None, // TODO: probably use constellation for backlink follows follows_count: Some(follows.len() as i64), handle: handle, indexed_at: None, joined_via_starter_pack: None, labels: None, pinned_post: profile.pinned_post, posts_count: None, pronouns: profile.pronouns, status: None, verification: None, viewer: None, website: profile.website, extra_data: None, }) } pub async fn get_profile_basic<'a>( resolver: JacquardResolver, pool: sqlx::SqlitePool, did: String, ) -> Result, XrpcErrorResponse> { let did_doc = external_data::get_did_doc(resolver.clone(), &pool, &did) .await .map_err(|err| { println!("getting did doc {err}"); XrpcErrorResponse::internal_server_error() })?; let at_id = AtIdentifier::from_str(&did_doc.did).map_err(|err| { println!("parsing did {err}"); XrpcErrorResponse::internal_server_error() })?; let did = match at_id { AtIdentifier::Did(did) => did, AtIdentifier::Handle(h) => resolver.resolve_handle(&h).await.map_err(|err| { println!("error resolving handle: {err}"); XrpcErrorResponse::internal_server_error() //TODO: ignore this? })?, }; let profile = get_profile_from_pds(pool, did.to_string(), did_doc.pds_endpoint.clone()) .await .map_err(|err| { println!("getting profile--: {err}"); XrpcErrorResponse::internal_server_error() })?; let avatar_uri = get_blob_uri(profile.avatar, did_doc.did, did_doc.pds_endpoint.clone()); let handle = handle::Handle::from_str(&did_doc.handle).map_err(|err| { println!("parsing handle {err}"); XrpcErrorResponse::internal_server_error() })?; Ok(ProfileViewBasic { avatar: avatar_uri, created_at: None, did: did, display_name: profile.display_name, handle: handle, labels: None, extra_data: None, pronouns: None, associated: None, debug: None, status: None, verification: None, viewer: None, }) } async fn get_profile_from_pds( pool: sqlx::SqlitePool, did: String, pds_endpoint: String, ) -> anyhow::Result> { let profile_blob = store::get_profile_blob_did(&pool, &did).await; if let Some(blob) = profile_blob { let profile: Profile<'_> = serde_json::from_slice(&blob)?; return Ok(profile.into_static()); } let rkey = RecordKey::from_str(&"self".to_string()).map_err(|err| err)?; let req = GetRecord::new() .repo(did.clone()) .collection("app.bsky.actor.profile".to_string()) .rkey(rkey) .build(); let http = reqwest::Client::new(); let base = Uri::parse(pds_endpoint) .to_owned() .map_err(|err| anyhow::Error::msg(err.1))?; let response = http.xrpc(base.to_owned()).send(&req).await?; let record = response.into_output()?; let raw_json = serde_json::to_vec(&record.value)?; store::insert_profile(&pool, did.clone(), raw_json).await; let profile: Profile = Profile::deserialize(record.value).map_err(|err| { println!("decoding profile: {err}"); err })?; Ok(profile) }