A better Rust ATProto crate
79 4 0

Clone this repository

https://tangled.org/zbrox.com/jacquard
git@knot.attic.zbrox.com:zbrox.com/jacquard

For self-hosted knots, clone URLs may differ based on your setup.

README.md

Jacquard#

A suite of Rust crates for the AT Protocol. Docs

Goals and Features#

  • Validated, spec-compliant, easy to work with, and performant baseline types
  • Batteries-included, but easily replaceable batteries.
    • Easy to extend with custom lexicons using code generation or handwritten api types
    • Straightforward OAuth
    • stateless options (or options where you handle the state) for rolling your own
    • all the building blocks of the convenient abstractions are available
  • lexicon Value type for working with unknown atproto data (dag-cbor or json)
  • order of magnitude less boilerplate than some existing crates
  • use as much or as little from the crates as you need

Example#

Dead simple API client. Logs in with OAuth and prints the latest 5 posts from your timeline.

// Note: this requires the `loopback` feature enabled (it is currently by default)
use clap::Parser;
use jacquard::CowStr;
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
use jacquard::client::{Agent, FileAuthStore};
use jacquard::oauth::atproto::AtprotoClientMetadata;
use jacquard::oauth::client::OAuthClient;
use jacquard::oauth::loopback::LoopbackConfig;
use jacquard::oauth::scopes::Scope;
use jacquard::types::xrpc::XrpcClient;
use miette::IntoDiagnostic;

#[derive(Parser, Debug)]
#[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")]
struct Args {
    /// Handle (e.g., alice.bsky.social), DID, or PDS URL
    input: CowStr<'static>,

    /// Path to auth store file (will be created if missing)
    #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
    store: String,
}

#[tokio::main]
async fn main() -> miette::Result<()> {
    let args = Args::parse();

    // File-backed auth store for testing
    let store = FileAuthStore::new(&args.store);
    let client_data = jacquard_oauth::session::ClientData {
        keyset: None,
        // Default sets normal localhost redirect URIs and "atproto transition:generic" scopes.
        // The localhost helper will ensure you have at least "atproto" and will fix urls
        config: AtprotoClientMetadata::default_localhost()
    };

    // Build an OAuth client
    let oauth = OAuthClient::new(store, client_data);
    // Authenticate with a PDS, using a loopback server to handle the callback flow
    let session = oauth
        .login_with_local_server(
            args.input.clone(),
            Default::default(),
            LoopbackConfig::default(),
        )
        .await?;
    // Wrap in Agent and fetch the timeline
    let agent: Agent<_> = Agent::from(session);
    let timeline = agent
        .send(&GetTimeline::new().limit(5).build())
        .await?
        .into_output()?;
    for (i, post) in timeline.feed.iter().enumerate() {
        println!("\n{}. by {}", i + 1, post.post.author.handle);
        println!(
            "   {}",
            serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
        );
    }

    Ok(())
}

Component crates#

Jacquard is broken up into several crates for modularity. The correct one to use is generally jacquard itself, as it re-exports the others.

  • jacquard: Main crate Crates.io Documentation
  • jacquard-common: Foundation crate Crates.io Documentation
  • jacquard-api: Autogenerated API bindings Crates.io Documentation
  • jacquard-oauth: atproto OAuth implementation Crates.io Documentation
  • jacquard-identity: Identity resolution Crates.io Documentation
  • jacquard-lexicon: Lexicon parsing and code generation Crates.io Documentation
  • jacquard-derive: Derive macros for lexicon types Crates.io Documentation

Development#

This repo uses Flakes from the get-go.

# Dev shell
nix develop

# or run via cargo
nix develop -c cargo run

# build
nix build

There's also a justfile for Makefile-esque commands to be run inside of the devShell, and you can generally cargo ... or just ... whatever just fine if you don't want to use Nix and have the prerequisites installed.

License