Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177//! XRPC read endpoints (appview query layer).//!//! All read endpoints are backed by the SQLite projection — records in//! `record_json`, handles in `identities`, profiles in `profiles` — so the read//! path needs no Hydrant handle. Endpoints mount via `jacquard-axum`'s//! [`IntoRouter`] and share [`AppState`] (SQLite pool + identity resolver + bsky//! client).//!//! In addition to the `space.polymodel.*` endpoints below, this module mounts a//! fixed, structural allowlist of non-polymodel XRPC methods as a single-origin//! passthrough proxy (PM-47): the direct `com.atproto.repo.*` record operations//! plus `com.atproto.identity.resolveHandle` and `app.bsky.actor.getProfile`,//! dispatched over their generated typed structs to the user's PDS / public//! AppView. Every other NSID has no route and 404s, and upstream XRPC errors are//! forwarded with their real status + body.
pub mod error;pub mod ssr;pub mod state;pub mod views;
mod actor;mod content_type;mod downloads;mod drafts;mod graph;mod ldraw;mod library;mod proxy;mod writes;
#[cfg(test)]mod tests;
use axum::Router;use jacquard_axum::IntoRouter;use jacquard_common::xrpc::XrpcEndpoint;use polymodel_api::space_polymodel::{ actor::{ bootstrap_profile::BootstrapProfileRequest, get_profile::GetProfileRequest, get_session::GetSessionRequest, }, graph::{ create_follow::CreateFollowRequest, create_like::CreateLikeRequest, create_save::CreateSaveRequest, create_tag::CreateTagRequest, delete_follow::DeleteFollowRequest, delete_like::DeleteLikeRequest, delete_save::DeleteSaveRequest, delete_tag::DeleteTagRequest, get_list::GetListRequest, }, library::{ delete_thing::DeleteThingRequest, get_author_things::GetAuthorThingsRequest, get_feed::GetFeedRequest, get_model::GetModelRequest, get_part_file::GetPartFileRequest, get_thing::GetThingRequest, publish_thing::PublishThingRequest, search_things::SearchThingsRequest, stage_file::StageFileRequest, update_thing::UpdateThingRequest, },};
use polymodel_api::app_bsky::actor::get_profile::GetProfileRequest as AppBskyGetProfileRequest;use polymodel_api::com_atproto::identity::resolve_handle::ResolveHandleRequest;use polymodel_api::com_atproto::repo::{ create_record::CreateRecordRequest, delete_record::DeleteRecordRequest, describe_repo::DescribeRepoRequest, get_record::GetRecordRequest, list_records::ListRecordsRequest, put_record::PutRecordRequest, upload_blob::UploadBlobRequest,};
use self::state::AppState;
/// Build the appview XRPC router with the given state applied.////// Returns a state-bound `Router<AppState>`; merged with the OAuth routes and the Dioxus SSR router in `main`, where `.with_state` is applied once.pub fn router() -> Router<AppState> { Router::new() .merge(GetThingRequest::into_router(library::get_thing)) .merge(GetModelRequest::into_router(library::get_model)) .merge(GetAuthorThingsRequest::into_router( library::get_author_things, )) .merge(GetFeedRequest::into_router(library::get_feed)) .merge(SearchThingsRequest::into_router(library::search_things)) .merge(GetListRequest::into_router(graph::get_list)) .merge(GetProfileRequest::into_router(actor::get_profile)) .merge(BootstrapProfileRequest::into_router( writes::bootstrap_profile, )) .route( StageFileRequest::PATH, axum::routing::post(writes::stage_file), ) .merge(PublishThingRequest::into_router(writes::publish_thing)) .merge(UpdateThingRequest::into_router(writes::update_thing)) .merge(DeleteThingRequest::into_router(writes::delete_thing)) .merge(CreateFollowRequest::into_router(writes::create_follow)) .merge(DeleteFollowRequest::into_router(writes::delete_follow)) .merge(CreateLikeRequest::into_router(writes::create_like)) .merge(DeleteLikeRequest::into_router(writes::delete_like)) .merge(CreateSaveRequest::into_router(writes::create_save)) .merge(DeleteSaveRequest::into_router(writes::delete_save)) .merge(CreateTagRequest::into_router(writes::create_tag)) .merge(DeleteTagRequest::into_router(writes::delete_tag)) // PM-47 passthrough proxy: a structural allowlist of non-polymodel XRPC // methods forwarded to the user's PDS / public AppView over their // generated typed request structs. Any other NSID has no route → 404. .merge(GetRecordRequest::into_router(proxy::repo_get_record)) .merge(ListRecordsRequest::into_router(proxy::repo_list_records)) .merge(DescribeRepoRequest::into_router(proxy::repo_describe_repo)) .merge(CreateRecordRequest::into_router(proxy::repo_create_record)) .merge(PutRecordRequest::into_router(proxy::repo_put_record)) .merge(DeleteRecordRequest::into_router(proxy::repo_delete_record)) .merge(UploadBlobRequest::into_router(proxy::repo_upload_blob)) .merge(ResolveHandleRequest::into_router( proxy::identity_resolve_handle, )) .merge(AppBskyGetProfileRequest::into_router( proxy::actor_get_profile, )) // getSession takes no parameters, so it bypasses ExtractXrpc (which // decodes the query string; a unit-struct request from an empty query is // rejected by serde_html_form). It uses Jacquard's API/headless optional // extractor so the documented x-jacquard-session fallback is confined to // this session bridge surface; other public read endpoints keep the // browser-oriented optional extractor semantics. .route( GetSessionRequest::PATH, axum::routing::get(actor::get_session), ) .merge(GetPartFileRequest::into_router(downloads::get_part_file)) .route( "/xrpc/space.polymodel.library.mintLdrawResource", axum::routing::post(ldraw::mint), ) .route( "/xrpc/space.polymodel.library.getLdrawResource", axum::routing::get(ldraw::fetch), ) // PM-43 app-internal draft store + image upload. Not federated lexicons, // so these use a plain `/app/*` namespace (cookie-authenticated, // same-origin) rather than `/xrpc/space.polymodel.*`. .route( "/app/drafts", axum::routing::post(drafts::create_draft).get(drafts::list_drafts_handler), ) .route( "/app/drafts/{draft_id}", axum::routing::put(drafts::put_draft) .get(drafts::get_draft_handler) .delete(drafts::delete_draft_handler), ) .route( "/app/drafts/{draft_id}/publish", axum::routing::post(drafts::publish_draft), ) .route("/app/images", axum::routing::post(drafts::upload_image)) // Per-request timing: logs total server-side handling time for every // appview request, *including* the OAuth session-restore extractor that // runs before each handler body. Use this to localize latency (server // handler vs. extractor vs. a client firing the request late). .layer(axum::middleware::from_fn(time_request))}
async fn time_request( req: axum::extract::Request, next: axum::middleware::Next,) -> axum::response::Response { let method = req.method().clone(); let path = req.uri().path().to_string(); let start = std::time::Instant::now(); let response = next.run(req).await; let elapsed_ms = start.elapsed().as_millis(); let status = response.status().as_u16(); if elapsed_ms >= 500 { tracing::warn!(%method, path, status, elapsed_ms, "slow appview request"); } else { tracing::info!(%method, path, status, elapsed_ms, "appview request"); } response}