A better Rust ATProto crate
1use clap::Parser;
2use jacquard::api::app_bsky::actor::profile::Profile;
3use jacquard::client::{Agent, AgentSessionExt, FileAuthStore};
4use jacquard::oauth::client::OAuthClient;
5use jacquard::oauth::loopback::LoopbackConfig;
6use jacquard::types::string::AtUri;
7use smol_str::SmolStr;
8
9#[derive(Parser, Debug)]
10#[command(author, version, about = "Update profile display name and description")]
11struct Args {
12 /// Handle (e.g., alice.bsky.social), DID, or PDS URL
13 input: String,
14
15 /// New display name
16 #[arg(long)]
17 display_name: Option<String>,
18
19 /// New bio/description
20 #[arg(long)]
21 description: Option<String>,
22
23 /// Path to auth store file (will be created if missing)
24 #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
25 store: String,
26}
27
28#[tokio::main]
29async fn main() -> miette::Result<()> {
30 let args = Args::parse();
31
32 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store));
33 let session = oauth
34 .login_with_local_server(
35 SmolStr::from(args.input),
36 Default::default(),
37 LoopbackConfig::default(),
38 )
39 .await?;
40
41 let agent: Agent<_> = Agent::from(session);
42
43 // Get session info to build the at:// URI for the profile record
44 let (did, _) = agent
45 .info()
46 .await
47 .ok_or_else(|| miette::miette!("No session info available"))?;
48
49 // Profile records use "self" as the rkey
50 let uri_string = format!("at://{}/app.bsky.actor.profile/self", did);
51 let uri = AtUri::new(uri_string.as_str())?;
52
53 // Update profile in-place using the fetch-modify-put pattern.
54 agent
55 .update_record::<Profile, _>(&uri, |profile| {
56 if let Some(name) = &args.display_name {
57 profile.display_name = Some(SmolStr::from(name.as_str()));
58 }
59 if let Some(desc) = &args.description {
60 profile.description = Some(SmolStr::from(desc.as_str()));
61 }
62 })
63 .await?;
64
65 println!("✓ Profile updated successfully");
66
67 Ok(())
68}