From 51b190cd2a5589d9466334b64bb582361df73fd7 Mon Sep 17 00:00:00 2001 From: afterlifepro Date: Thu, 16 Oct 2025 20:47:44 +0100 Subject: [PATCH] add memory credential session type --- crates/jacquard/src/client.rs | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index 29a6f9e..59bcc8f 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -1048,3 +1048,61 @@ impl Default for BasicClient { Self::unauthenticated() } } + +/// MemoryCredentialSession: credential session with in memory store and identity resolver +pub type MemoryCredentialSession = CredentialSession< + MemorySessionStore, + 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> { + /// let (session, _) = MemoryCredentialSession::authenticated(identifier, password, None); + /// let agent = Agent::from(session); + /// let output = agent.create_record::(post, None).await?; + /// # Ok(()) + /// # } + /// ``` + async fn authenticated( + identifier: CowStr<'_>, + password: CowStr<'_>, + session_id: Option>, + ) -> 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() + } +} -- 2.43.0 From 1fd9310a0aaa350be91effb9fe94c3195c7f4c7e Mon Sep 17 00:00:00 2001 From: afterlifepro Date: Thu, 16 Oct 2025 21:08:08 +0100 Subject: [PATCH] add app password based example --- crates/jacquard/Cargo.toml | 33 +++++++++++++++---- examples/app_password_create_post.rs | 49 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 examples/app_password_create_post.rs diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index ce9c10f..3ae3f49 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -17,16 +17,26 @@ derive = ["dep:jacquard-derive"] # 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"] @@ -83,10 +93,17 @@ name = "streaming_download" 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" } @@ -112,7 +129,11 @@ rand_core.workspace = true 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] @@ -123,4 +144,4 @@ clap.workspace = true miette = { workspace = true, features = ["fancy"] } [package.metadata.docs.rs] -features = [ "api_all", "derive", "dns", "loopback" ] +features = ["api_all", "derive", "dns", "loopback"] diff --git a/examples/app_password_create_post.rs b/examples/app_password_create_post.rs new file mode 100644 index 0000000..50701db --- /dev/null +++ b/examples/app_password_create_post.rs @@ -0,0 +1,49 @@ +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(()) +} -- 2.43.0