A better Rust ATProto crate
1use clap::Parser; 2use jacquard::api::app_bsky::actor::{AdultContentPref, PreferencesItem}; 3use jacquard::client::vec_update::VecUpdate; 4use jacquard::client::{Agent, FileAuthStore}; 5use jacquard::oauth::atproto::AtprotoClientMetadata; 6use jacquard::oauth::client::OAuthClient; 7use jacquard::oauth::loopback::LoopbackConfig; 8use jacquard::{CowStr, IntoStatic}; 9 10#[derive(Parser, Debug)] 11#[command(author, version, about = "Update Bluesky preferences")] 12struct Args { 13 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 14 input: CowStr<'static>, 15 16 /// Enable adult content 17 #[arg(long)] 18 enable_adult_content: bool, 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/// Helper struct for the VecUpdate pattern on preferences 26pub struct PreferencesUpdate; 27 28impl VecUpdate for PreferencesUpdate { 29 type GetRequest<'de> = jacquard::api::app_bsky::actor::get_preferences::GetPreferences; 30 type PutRequest<'de> = jacquard::api::app_bsky::actor::put_preferences::PutPreferences<'de>; 31 type Item = PreferencesItem<'static>; 32 33 fn build_get<'s>() -> Self::GetRequest<'s> { 34 jacquard::api::app_bsky::actor::get_preferences::GetPreferences::new().build() 35 } 36 37 fn build_put<'s>(items: Vec<Self::Item>) -> Self::PutRequest<'s> { 38 jacquard::api::app_bsky::actor::put_preferences::PutPreferences { 39 preferences: items, 40 extra_data: Default::default(), 41 } 42 } 43 44 fn extract_vec( 45 output: jacquard::api::app_bsky::actor::get_preferences::GetPreferencesOutput<'_>, 46 ) -> Vec<Self::Item> { 47 output 48 .preferences 49 .into_iter() 50 .map(|p| p.into_static()) 51 .collect() 52 } 53 54 fn matches(a: &Self::Item, b: &Self::Item) -> bool { 55 // Match by enum variant discriminant 56 std::mem::discriminant(a) == std::mem::discriminant(b) 57 } 58} 59 60#[tokio::main] 61async fn main() -> miette::Result<()> { 62 let args = Args::parse(); 63 64 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store)); 65 let session = oauth 66 .login_with_local_server(args.input, Default::default(), LoopbackConfig::default()) 67 .await?; 68 69 let agent: Agent<_> = Agent::from(session); 70 71 // Create the adult content preference 72 let adult_pref = AdultContentPref { 73 enabled: args.enable_adult_content, 74 extra_data: Default::default(), 75 }; 76 77 // Update preferences using update_vec_item 78 // This will replace existing AdultContentPref or add it if not present 79 agent 80 .update_vec_item::<PreferencesUpdate>(PreferencesItem::AdultContentPref(Box::new( 81 adult_pref, 82 ))) 83 .await?; 84 85 println!( 86 "✓ Updated adult content preference: {}", 87 if args.enable_adult_content { 88 "enabled" 89 } else { 90 "disabled" 91 } 92 ); 93 94 Ok(()) 95}