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
··· 1048 1048 Self::unauthenticated() 1049 1049 } 1050 1050 } 1051 + 1052 + /// MemoryCredentialSession: credential session with in memory store and identity resolver 1053 + pub type MemoryCredentialSession = CredentialSession< 1054 + MemorySessionStore<SessionKey, AtpSession>, 1055 + jacquard_identity::PublicResolver, 1056 + >; 1057 + 1058 + impl MemoryCredentialSession { 1059 + /// Create an unauthenticated MemoryCredentialSession. 1060 + /// 1061 + /// Uses an in memory store and a public resolver. 1062 + /// Equivalent to a BasicClient that isn't wrapped in Agent 1063 + fn unauthenticated() -> Self { 1064 + use std::sync::Arc; 1065 + let http = reqwest::Client::new(); 1066 + let resolver = jacquard_identity::PublicResolver::new(http, Default::default()); 1067 + let store = MemorySessionStore::default(); 1068 + CredentialSession::new(Arc::new(store), Arc::new(resolver)) 1069 + } 1070 + 1071 + /// Create a MemoryCredentialSession and authenticate with the provided details 1072 + /// 1073 + /// - `identifier`: handle (preferred), DID, or `https://` PDS base URL. 1074 + /// - `session_id`: optional session label; defaults to "session". 1075 + /// - Persists and activates the session, and updates the base endpoint to the user's PDS. 1076 + /// 1077 + /// # Example 1078 + /// ```no_run 1079 + /// # use jacquard::client::BasicClient; 1080 + /// # use jacquard::types::string::AtUri; 1081 + /// # use jacquard_api::app_bsky::feed::post::Post; 1082 + /// use crate::jacquard::client::{Agent, AgentSessionExt}; 1083 + /// # #[tokio::main] 1084 + /// # async fn main() -> Result<(), Box<dyn std::error::Error>> { 1085 + /// let (session, _) = MemoryCredentialSession::authenticated(identifier, password, None); 1086 + /// let agent = Agent::from(session); 1087 + /// let output = agent.create_record::<Post>(post, None).await?; 1088 + /// # Ok(()) 1089 + /// # } 1090 + /// ``` 1091 + async fn authenticated( 1092 + identifier: CowStr<'_>, 1093 + password: CowStr<'_>, 1094 + session_id: Option<CowStr<'_>>, 1095 + ) -> Result<(Self, AtpSession), ClientError> { 1096 + let session = MemoryCredentialSession::unauthenticated(); 1097 + let auth = session 1098 + .login(identifier, password, session_id, None, None) 1099 + .await?; 1100 + Ok((session, auth)) 1101 + } 1102 + } 1103 + 1104 + impl Default for MemoryCredentialSession { 1105 + fn default() -> Self { 1106 + MemoryCredentialSession::unauthenticated() 1107 + } 1108 + }
+27 -6
crates/jacquard/Cargo.toml
··· 17 17 # Minimal API bindings 18 18 api = ["jacquard-api/minimal"] 19 19 # Bluesky API bindings 20 - api_bluesky = ["api", "jacquard-api/bluesky" ] 20 + api_bluesky = ["api", "jacquard-api/bluesky"] 21 21 # Bluesky API bindings, plus a curated selection of community lexicons 22 - api_full = ["api", "jacquard-api/bluesky", "jacquard-api/other", "jacquard-api/lexicon_community"] 22 + api_full = [ 23 + "api", 24 + "jacquard-api/bluesky", 25 + "jacquard-api/other", 26 + "jacquard-api/lexicon_community", 27 + ] 23 28 # All captured generated lexicon API bindings 24 29 api_all = ["api_full", "jacquard-api/ufos"] 25 30 26 31 # Propagate loopback to oauth (server + browser helper) 27 32 loopback = ["jacquard-oauth/loopback", "jacquard-oauth/browser-open"] 28 33 # Enable tracing instrumentation 29 - tracing = ["dep:tracing", "jacquard-common/tracing", "jacquard-oauth/tracing", "jacquard-identity/tracing"] 34 + tracing = [ 35 + "dep:tracing", 36 + "jacquard-common/tracing", 37 + "jacquard-oauth/tracing", 38 + "jacquard-identity/tracing", 39 + ] 30 40 dns = ["jacquard-identity/dns"] 31 41 streaming = ["jacquard-common/streaming"] 32 42 websocket = ["jacquard-common/websocket"] ··· 83 93 path = "../../examples/streaming_download.rs" 84 94 required-features = ["api_bluesky", "streaming"] 85 95 96 + [[example]] 97 + name = "app_password_create_post" 98 + path = "../../examples/app_password_create_post.rs" 99 + required-features = ["api_bluesky"] 100 + 86 101 87 102 [dependencies] 88 103 jacquard-api = { version = "0.5", path = "../jacquard-api" } 89 - jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] } 104 + jacquard-common = { version = "0.5", path = "../jacquard-common", features = [ 105 + "reqwest-client", 106 + ] } 90 107 jacquard-oauth = { version = "0.5", path = "../jacquard-oauth" } 91 108 jacquard-derive = { version = "0.5", path = "../jacquard-derive", optional = true } 92 109 jacquard-identity = { version = "0.5", path = "../jacquard-identity" } ··· 112 129 tracing = { workspace = true, optional = true } 113 130 114 131 [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 115 - reqwest = { workspace = true, features = ["http2", "system-proxy", "rustls-tls"] } 132 + reqwest = { workspace = true, features = [ 133 + "http2", 134 + "system-proxy", 135 + "rustls-tls", 136 + ] } 116 137 tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] } 117 138 118 139 [target.'cfg(target_family = "wasm")'.dependencies] ··· 123 144 miette = { workspace = true, features = ["fancy"] } 124 145 125 146 [package.metadata.docs.rs] 126 - features = [ "api_all", "derive", "dns", "loopback" ] 147 + features = ["api_all", "derive", "dns", "loopback"]
+49
examples/app_password_create_post.rs
··· 1 + use clap::Parser; 2 + use jacquard::CowStr; 3 + use jacquard::api::app_bsky::feed::post::Post; 4 + use jacquard::client::{Agent, AgentSessionExt, MemoryCredentialSession}; 5 + use jacquard::types::string::Datetime; 6 + 7 + #[derive(Parser, Debug)] 8 + #[command(author, version, about = "Create a simple post")] 9 + struct Args { 10 + /// Handle (e.g., alice.bsky.social) or DID 11 + input: CowStr<'static>, 12 + 13 + /// App Password 14 + password: CowStr<'static>, 15 + 16 + /// Post text 17 + #[arg(short, long)] 18 + text: String, 19 + } 20 + 21 + #[tokio::main] 22 + async fn main() -> miette::Result<()> { 23 + let args = Args::parse(); 24 + 25 + let (session, auth) = 26 + MemoryCredentialSession::authenticated(args.input, args.password, None).await?; 27 + println!("Signed in as {}", auth.handle); 28 + 29 + let agent: Agent<_> = Agent::from(session); 30 + 31 + // Create a simple text post using the Agent convenience method 32 + let post = Post { 33 + text: CowStr::from(args.text), 34 + created_at: Datetime::now(), 35 + embed: None, 36 + entities: None, 37 + facets: None, 38 + labels: None, 39 + langs: None, 40 + reply: None, 41 + tags: None, 42 + extra_data: Default::default(), 43 + }; 44 + 45 + let output = agent.create_record(post, None).await?; 46 + println!("✓ Created post: {}", output.uri); 47 + 48 + Ok(()) 49 + }