1use clap::Parser;
2use jacquard::CowStr;
3use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
4use jacquard::client::{Agent, FileAuthStore};
5use jacquard::oauth::atproto::AtprotoClientMetadata;
6use jacquard::oauth::client::OAuthClient;
7#[cfg(feature = "loopback")]
8use jacquard::oauth::loopback::LoopbackConfig;
9use jacquard::xrpc::XrpcClient;
10#[cfg(not(feature = "loopback"))]
11use jacquard_oauth::types::AuthorizeOptions;
12use miette::IntoDiagnostic;
13
14#[derive(Parser, Debug)]
15#[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")]
16struct Args {
17 /// Handle (e.g., alice.bsky.social), DID, or PDS URL
18 input: CowStr<'static>,
19
20 /// Path to auth store file (will be created if missing)
21 #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
22 store: String,
23}
24
25#[tokio::main]
26async fn main() -> miette::Result<()> {
27 let args = Args::parse();
28
29 // File-backed auth store shared by OAuthClient and session registry
30 let store = FileAuthStore::new(&args.store);
31 let client_data = jacquard_oauth::session::ClientData {
32 keyset: None,
33 // Default sets normal localhost redirect URIs and "atproto transition:generic" scopes.
34 // The localhost helper will ensure you have at least "atproto" and will fix urls
35 config: AtprotoClientMetadata::default_localhost(),
36 };
37
38 // Build an OAuth client (this is reusable, and can create multiple sessions)
39 let oauth = OAuthClient::new(store, client_data);
40
41 #[cfg(feature = "loopback")]
42 // Authenticate with a PDS, using a loopback server to handle the callback flow
43 let session = oauth
44 .login_with_local_server(
45 args.input.clone(),
46 Default::default(),
47 LoopbackConfig::default(),
48 )
49 .await?;
50
51 #[cfg(not(feature = "loopback"))]
52 let session = {
53 use std::io::{BufRead, Write, stdin, stdout};
54
55 let auth_url = oauth
56 .start_auth(args.input, AuthorizeOptions::default())
57 .await?;
58
59 println!("To authenticate with your PDS, visit:\n{}\n", auth_url);
60 print!("\nPaste the callback url here:");
61 stdout().lock().flush().into_diagnostic()?;
62 let mut url = String::new();
63 stdin().lock().read_line(&mut url).into_diagnostic()?;
64
65 let uri = url.trim().parse::<http::Uri>().into_diagnostic()?;
66 let params =
67 serde_html_form::from_str(uri.query().ok_or(miette::miette!("invalid callback url"))?)
68 .into_diagnostic()?;
69 oauth.callback(params).await?
70 };
71
72 // Wrap in Agent and fetch the timeline
73 let agent: Agent<_> = Agent::from(session);
74 let output = agent.send(GetTimeline::new().limit(5).build()).await?;
75 let timeline = output.into_output()?;
76 for (i, post) in timeline.feed.iter().enumerate() {
77 println!("\n{}. by {}", i + 1, post.post.author.handle);
78 println!(
79 " {}",
80 serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
81 );
82 }
83
84 Ok(())
85}