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