Add MemoryCredentialSession and app password example #2

merged
opened by vielle.dev targeting main from vielle.dev/jacquard: main

lmk if things need to move/be changed/etc i am Not Great At Rust™ lol

Changed files
+134 -6
crates
jacquard
examples
+58
crates/jacquard/src/client.rs
···
Self::unauthenticated()
+
+
/// MemoryCredentialSession: credential session with in memory store and identity resolver
+
pub type MemoryCredentialSession = CredentialSession<
+
MemorySessionStore<SessionKey, AtpSession>,
+
jacquard_identity::PublicResolver,
+
>;
+
+
impl MemoryCredentialSession {
+
/// Create an unauthenticated MemoryCredentialSession.
+
///
+
/// Uses an in memory store and a public resolver.
+
/// Equivalent to a BasicClient that isn't wrapped in Agent
+
fn unauthenticated() -> Self {
+
use std::sync::Arc;
+
let http = reqwest::Client::new();
+
let resolver = jacquard_identity::PublicResolver::new(http, Default::default());
+
let store = MemorySessionStore::default();
+
CredentialSession::new(Arc::new(store), Arc::new(resolver))
+
}
+
+
/// Create a MemoryCredentialSession and authenticate with the provided details
+
///
+
/// - `identifier`: handle (preferred), DID, or `https://` PDS base URL.
+
/// - `session_id`: optional session label; defaults to "session".
+
/// - Persists and activates the session, and updates the base endpoint to the user's PDS.
+
///
+
/// # Example
+
/// ```no_run
+
/// # use jacquard::client::BasicClient;
+
/// # use jacquard::types::string::AtUri;
+
/// # use jacquard_api::app_bsky::feed::post::Post;
+
/// use crate::jacquard::client::{Agent, AgentSessionExt};
+
/// # #[tokio::main]
+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
+
/// let (session, _) = MemoryCredentialSession::authenticated(identifier, password, None);
+
/// let agent = Agent::from(session);
+
/// let output = agent.create_record::<Post>(post, None).await?;
+
/// # Ok(())
+
/// # }
+
/// ```
+
async fn authenticated(
+
identifier: CowStr<'_>,
+
password: CowStr<'_>,
+
session_id: Option<CowStr<'_>>,
+
) -> Result<(Self, AtpSession), ClientError> {
+
let session = MemoryCredentialSession::unauthenticated();
+
let auth = session
+
.login(identifier, password, session_id, None, None)
+
.await?;
+
Ok((session, auth))
+
}
+
}
+
+
impl Default for MemoryCredentialSession {
+
fn default() -> Self {
+
MemoryCredentialSession::unauthenticated()
+
}
+
}
+27 -6
crates/jacquard/Cargo.toml
···
# Minimal API bindings
api = ["jacquard-api/minimal"]
# Bluesky API bindings
-
api_bluesky = ["api", "jacquard-api/bluesky" ]
+
api_bluesky = ["api", "jacquard-api/bluesky"]
# Bluesky API bindings, plus a curated selection of community lexicons
-
api_full = ["api", "jacquard-api/bluesky", "jacquard-api/other", "jacquard-api/lexicon_community"]
+
api_full = [
+
"api",
+
"jacquard-api/bluesky",
+
"jacquard-api/other",
+
"jacquard-api/lexicon_community",
+
]
# All captured generated lexicon API bindings
api_all = ["api_full", "jacquard-api/ufos"]
# Propagate loopback to oauth (server + browser helper)
loopback = ["jacquard-oauth/loopback", "jacquard-oauth/browser-open"]
# Enable tracing instrumentation
-
tracing = ["dep:tracing", "jacquard-common/tracing", "jacquard-oauth/tracing", "jacquard-identity/tracing"]
+
tracing = [
+
"dep:tracing",
+
"jacquard-common/tracing",
+
"jacquard-oauth/tracing",
+
"jacquard-identity/tracing",
+
]
dns = ["jacquard-identity/dns"]
streaming = ["jacquard-common/streaming"]
websocket = ["jacquard-common/websocket"]
···
path = "../../examples/streaming_download.rs"
required-features = ["api_bluesky", "streaming"]
+
[[example]]
+
name = "app_password_create_post"
+
path = "../../examples/app_password_create_post.rs"
+
required-features = ["api_bluesky"]
+
[dependencies]
jacquard-api = { version = "0.5", path = "../jacquard-api" }
-
jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] }
+
jacquard-common = { version = "0.5", path = "../jacquard-common", features = [
+
"reqwest-client",
+
] }
jacquard-oauth = { version = "0.5", path = "../jacquard-oauth" }
jacquard-derive = { version = "0.5", path = "../jacquard-derive", optional = true }
jacquard-identity = { version = "0.5", path = "../jacquard-identity" }
···
tracing = { workspace = true, optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-
reqwest = { workspace = true, features = ["http2", "system-proxy", "rustls-tls"] }
+
reqwest = { workspace = true, features = [
+
"http2",
+
"system-proxy",
+
"rustls-tls",
+
] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] }
[target.'cfg(target_family = "wasm")'.dependencies]
···
miette = { workspace = true, features = ["fancy"] }
[package.metadata.docs.rs]
-
features = [ "api_all", "derive", "dns", "loopback" ]
+
features = ["api_all", "derive", "dns", "loopback"]
+49
examples/app_password_create_post.rs
···
+
use clap::Parser;
+
use jacquard::CowStr;
+
use jacquard::api::app_bsky::feed::post::Post;
+
use jacquard::client::{Agent, AgentSessionExt, MemoryCredentialSession};
+
use jacquard::types::string::Datetime;
+
+
#[derive(Parser, Debug)]
+
#[command(author, version, about = "Create a simple post")]
+
struct Args {
+
/// Handle (e.g., alice.bsky.social) or DID
+
input: CowStr<'static>,
+
+
/// App Password
+
password: CowStr<'static>,
+
+
/// Post text
+
#[arg(short, long)]
+
text: String,
+
}
+
+
#[tokio::main]
+
async fn main() -> miette::Result<()> {
+
let args = Args::parse();
+
+
let (session, auth) =
+
MemoryCredentialSession::authenticated(args.input, args.password, None).await?;
+
println!("Signed in as {}", auth.handle);
+
+
let agent: Agent<_> = Agent::from(session);
+
+
// Create a simple text post using the Agent convenience method
+
let post = Post {
+
text: CowStr::from(args.text),
+
created_at: Datetime::now(),
+
embed: None,
+
entities: None,
+
facets: None,
+
labels: None,
+
langs: None,
+
reply: None,
+
tags: None,
+
extra_data: Default::default(),
+
};
+
+
let output = agent.create_record(post, None).await?;
+
println!("✓ Created post: {}", output.uri);
+
+
Ok(())
+
}