Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215//! Server-side bridge for SSR and Dioxus server functions.//!//! Normal SSR/server-function reads should extract the inbound OAuth session//! from the current Axum request and call appview helpers directly in-process.//! The `x-jacquard-session` utility below is only an escape hatch for the rare//! server-side outbound XRPC call that cannot be avoided.
use axum::extract::FromRequestParts;use dioxus::prelude::{ServerFnError, dioxus_fullstack::FullstackContext};use jacquard::identity::PublicResolver;use jacquard::oauth::client::OAuthSession;use jacquard_axum::oauth::ExtractOptionalOAuthSession;use jacquard_common::types::ident::AtIdentifier;use polymodel_api::space_polymodel::actor::ProfileViewUnion;use polymodel_api::space_polymodel::actor::get_session::GetSessionOutput;use polymodel_api::space_polymodel::library::FeedView;
use super::actor;use super::error::clamp_limit;use super::state::AppState;use super::views::{self, ModelDetail};use crate::oauth::SqliteAuthStore;
/// Maximum time to wait for the Bluesky profile fallback during SSR before/// degrading to an error state. The polymodel-first path is pure SQLite and/// needs no timeout; this only bounds the outbound `public.api.bsky.app` call.const SSR_BSKY_PROFILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
pub type PolymodelOAuthSession = OAuthSession<PublicResolver, SqliteAuthStore>;
/// Extract the current request's optional OAuth session and resolve the/// `getSession` appview output without issuing a same-origin XRPC request.pub async fn get_session_output_from_fullstack_context() -> Result<GetSessionOutput, ServerFnError>{ let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; actor::get_session_output(&state, session.as_ref()) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))}
// ---------------------------------------------------------------------------// Per-read "from fullstack context" helpers//// Each helper resolves the viewer from the request's OAuth session and calls// the same in-process `views::`/`actor::` helper that the corresponding// `/xrpc/*` handler calls. Parity is at the helper level: both paths share the// same query/view logic.// ---------------------------------------------------------------------------
/// Resolve a thing (with its models) server-side, same path as `getThing`.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_thing_from_fullstack_context( uri: &str,) -> Result< ( polymodel_api::space_polymodel::library::ThingView, Vec<polymodel_api::space_polymodel::library::ModelView>, ), ServerFnError,> { let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; let viewer = views::viewer_did(session.as_ref()).await; let viewer = viewer.as_ref().map(|d| d.borrow()); views::get_thing(&state, uri, viewer) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))}
/// Resolve a model (with its parts and parent thing) server-side, same path as/// `getModel`.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_model_from_fullstack_context( uri: &str,) -> Result<ModelDetail, ServerFnError> { let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; let viewer = views::viewer_did(session.as_ref()).await; let viewer = viewer.as_ref().map(|d| d.borrow()); views::get_model(&state, uri, viewer) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))}
/// Resolve a feed server-side, same path as `getFeed`. The `algorithm` string/// dispatches exactly as the XRPC handler.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_feed_from_fullstack_context( algorithm: &str, limit: i64,) -> Result<FeedView, ServerFnError> { let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; let viewer = views::viewer_did(session.as_ref()).await; let viewer = viewer.as_ref().map(|d| d.borrow()); let limit = clamp_limit(Some(limit)); let feed = match algorithm { "recent" => views::feed_recent(&state, limit, None, viewer).await, "hot" => views::feed_hot(&state, limit, None, viewer).await, "following" => match viewer { Some(viewer) => views::feed_following(&state, viewer, limit, None).await, None => Ok(views::empty_feed()), }, other => return Err(ServerFnError::new(format!("unknown algorithm: {other}"))), }; feed.map_err(|error| ServerFnError::new(format!("{error:?}")))}
/// Resolve a profile union server-side, same path as `getProfile`. Polymodel/// profile is tried first (pure SQLite); the Bluesky fallback is bounded by/// [`SSR_BSKY_PROFILE_TIMEOUT`] so a slow upstream cannot block first paint./// On timeout or error the caller receives an `Err`, which the route maps to/// its existing error state.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_profile_union_from_fullstack_context( actor_ident: &AtIdentifier,) -> Result<ProfileViewUnion, ServerFnError> { let (_context, state) = context_and_state()?; let did = actor::resolve_actor(&state, actor_ident) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))?;
if let Some(profile) = views::polymodel_profile_view(&state, &did) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))? { return Ok(ProfileViewUnion::ProfileView(Box::new(profile))); }
match tokio::time::timeout( SSR_BSKY_PROFILE_TIMEOUT, actor::fetch_bsky_profile(&state, actor_ident), ) .await { Ok(Ok(detailed)) => Ok(ProfileViewUnion::ProfileViewDetailed(Box::new(detailed))), Ok(Err(error)) => Err(ServerFnError::new(format!( "Profile request failed: {error:?}" ))), Err(_) => Err(ServerFnError::new( "Profile request timed out waiting for Bluesky fallback", )), }}
/// Resolve an actor's things server-side, same path as `getAuthorThings`. The/// actor identifier is canonicalized to a DID before querying, exactly as the/// XRPC handler does.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_author_things_from_fullstack_context( actor_ident: &AtIdentifier, limit: i64, cursor: Option<&str>,) -> Result<FeedView, ServerFnError> { let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; let viewer = views::viewer_did(session.as_ref()).await; let viewer = viewer.as_ref().map(|d| d.borrow()); let did = actor::resolve_actor(&state, actor_ident) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))?; views::author_things( &state, did.as_ref(), clamp_limit(Some(limit)), cursor, viewer, ) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))}
/// Search things server-side, same path as `searchThings`.#[cfg_attr(not(feature = "server"), allow(dead_code))]pub(crate) async fn get_search_from_fullstack_context( query: &str, cursor: Option<&str>, limit: i64,) -> Result<FeedView, ServerFnError> { let (context, state) = context_and_state()?; let session = extract_optional_oauth_session(&context, &state).await?; let viewer = views::viewer_did(session.as_ref()).await; let viewer = viewer.as_ref().map(|d| d.borrow()); views::search_things(&state, query, clamp_limit(Some(limit)), cursor, viewer) .await .map_err(|error| ServerFnError::new(format!("{error:?}")))}
// ---------------------------------------------------------------------------// Internals// ---------------------------------------------------------------------------
fn context_and_state() -> Result<(FullstackContext, AppState), ServerFnError> { let context = FullstackContext::current() .ok_or_else(|| ServerFnError::new("missing Dioxus fullstack request context"))?; let state = context .extension::<AppState>() .ok_or_else(|| ServerFnError::new("missing Polymodel AppState request extension"))?; Ok((context, state))}
pub(crate) async fn extract_optional_oauth_session( context: &FullstackContext, state: &AppState,) -> Result<Option<PolymodelOAuthSession>, ServerFnError> { let mut parts = context.parts_mut().clone(); ExtractOptionalOAuthSession::<PublicResolver, SqliteAuthStore>::from_request_parts( &mut parts, state, ) .await .map(|ExtractOptionalOAuthSession(session)| session) .map_err(|error| ServerFnError::new(error.to_string()))}