Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219//! `space.polymodel.actor.*` read endpoint handlers: `getProfile` (typed union//! of polymodel profileView / app.bsky profileViewDetailed) and `getSession`//! (the session identity primitive).
use axum::extract::State;use jacquard::identity::PublicResolver;use jacquard::identity::resolver::IdentityResolver;use jacquard::oauth::client::OAuthSession;use jacquard_axum::oauth::ExtractOptionalOAuthSession;use jacquard_axum::{ExtractXrpc, XrpcResponse};use jacquard_common::deps::smol_str::SmolStr;use jacquard_common::types::ident::AtIdentifier;use jacquard_common::types::string::{Did, Handle};use jacquard_common::types::value::Data;use jacquard_common::xrpc::XrpcClient;use polymodel_api::app_bsky::actor::ProfileViewDetailed;use polymodel_api::app_bsky::actor::get_profile::GetProfile as BskyGetProfile;use polymodel_api::space_polymodel::actor::{ ProfileView, ProfileViewUnion, get_profile::{GetProfileOutput, GetProfileRequest}, get_session::{ GetSessionOutput, GetSessionRequest, SessionAuthenticated, SessionUnauthenticated, SessionView, },};
use super::error::{AppResult, internal};use super::state::AppState;use super::views;use crate::oauth::SqliteAuthStore;
/// Canonicalize an at-identifier to a DID by matching on its variant (not by/// sniffing its serialized form). DIDs pass through; handles are resolved via/// the identity resolver. A handle that cannot be resolved is a 500 (the/// identifier was already structurally valid).pub(super) async fn resolve_actor(state: &AppState, ident: &AtIdentifier) -> AppResult<Did> { match ident { AtIdentifier::Did(did) => Ok(did.clone()), AtIdentifier::Handle(handle) => state .resolver .resolve_handle(handle) .await .map_err(|e| internal(format!("handle resolution failed: {e}"))), }}
pub(super) async fn get_profile( State(state): State<AppState>, ExtractXrpc(req): ExtractXrpc<GetProfileRequest>,) -> AppResult<XrpcResponse<GetProfileRequest>> { let did = resolve_actor(&state, &req.actor).await?;
// Polymodel variant when the actor authored a polymodel profile record. if let Some(profile) = views::polymodel_profile_view(&state, &did).await? { return Ok(XrpcResponse(GetProfileOutput { value: ProfileViewUnion::ProfileView(Box::new(profile)), extra_data: None, })); }
// Otherwise the bluesky variant, fetched from the public AppView. let detailed = fetch_bsky_profile(&state, &req.actor).await?; Ok(XrpcResponse(GetProfileOutput { value: ProfileViewUnion::ProfileViewDetailed(Box::new(detailed)), extra_data: None, }))}
/// Fetch `app.bsky.actor.getProfile` from `https://public.api.bsky.app` via the/// unauthenticated jacquard client. This is the only network path in the read/// endpoints; tests exercise union-variant construction without it.pub(crate) async fn fetch_bsky_profile( state: &AppState, ident: &AtIdentifier,) -> AppResult<ProfileViewDetailed> { let resp = state .bsky .send(BskyGetProfile { actor: ident.clone(), }) .await .map_err(|e| internal(format!("app.bsky.actor.getProfile failed: {e}")))?; let out = resp .into_output() .map_err(|e| internal(format!("app.bsky.actor.getProfile decode failed: {e}")))?; Ok(out.value)}
pub(super) async fn get_session( State(state): State<AppState>, ExtractOptionalOAuthSession(session): ExtractOptionalOAuthSession< PublicResolver, SqliteAuthStore, >,) -> AppResult<XrpcResponse<GetSessionRequest>> { Ok(XrpcResponse( get_session_output(&state, session.as_ref()).await?, ))}
pub(crate) async fn get_session_output( state: &AppState, session: Option<&OAuthSession<PublicResolver, SqliteAuthStore>>,) -> AppResult<GetSessionOutput> { if let Some(session) = session.as_ref() { let t_session = std::time::Instant::now(); let did = session.session_info().await.0; let session_info_ms = t_session.elapsed().as_millis(); let t_resolve = std::time::Instant::now(); let resolved = views::resolve_handle(state, &did).await; let resolve_handle_ms = t_resolve.elapsed().as_millis(); match resolved { Ok(handle_str) => { let handle = Handle::new_owned(handle_str.clone()) .map_err(|e| internal(format!("invalid session handle: {e}")))?; let t_profile = std::time::Instant::now(); let profile = match views::polymodel_profile_view(state, &did).await? { Some(profile) => profile, None => { // No polymodel profile record: fall back to the actor's // Bluesky profile so the session carries their real avatar // and display name. If that fetch fails, degrade to a // handle-only profile rather than dropping the session. match fetch_bsky_profile(state, &AtIdentifier::Did(did.clone())).await { Ok(detailed) => { bsky_session_profile(detailed, did.clone(), &handle_str) } Err(error) => { tracing::warn!(error = ?error, did = %did, "bsky profile fetch failed for session; using handle-only profile"); minimal_session_profile(did.clone(), handle_str)? } } } }; tracing::info!( session_info_ms, resolve_handle_ms, profile_ms = t_profile.elapsed().as_millis(), "get_session authed timing" ); return Ok(GetSessionOutput { value: SessionView::SessionAuthenticated(Box::new(SessionAuthenticated { authenticated: true, did, handle, profile, extra_data: None, })), extra_data: None, }); } Err(error) => { tracing::warn!(error = ?error, did = %did, "session handle resolution failed; reporting unauthenticated"); } } }
Ok(unauthenticated_session())}
fn unauthenticated_session() -> GetSessionOutput { GetSessionOutput { value: SessionView::SessionUnauthenticated(Box::new(SessionUnauthenticated { authenticated: false, extra_data: None, })), extra_data: None, }}
fn minimal_session_profile(did: Did, handle: String) -> AppResult<ProfileView> { Ok(ProfileView { avatar: None, default_license: None, description: None, did, display_name: None, handle: SmolStr::from(handle), indexed_at: None, links: None, printers: None, pronouns: None, record: Data::Null, follower_count: None, following_count: None, lists: None, thing_count: None, total_likes: None, viewer: None, extra_data: None, })}
/// Build a session `ProfileView` from the actor's Bluesky profile so the/// signed-in chrome can show their real avatar and display name when they have/// no polymodel profile record. Polymodel-specific fields stay absent.fn bsky_session_profile(detailed: ProfileViewDetailed, did: Did, handle: &str) -> ProfileView { ProfileView { avatar: detailed.avatar, default_license: None, description: detailed.description, did, display_name: detailed.display_name, handle: SmolStr::from(handle), indexed_at: detailed.indexed_at, links: None, printers: None, pronouns: detailed.pronouns, record: Data::Null, follower_count: None, following_count: None, lists: None, thing_count: None, total_likes: None, viewer: None, extra_data: None, }}