forked from
nonbinary.computer/jacquard
A better Rust ATProto crate
1use clap::Parser;
2use jacquard::CowStr;
3use jacquard::api::app_bsky::embed::images::{Image, Images};
4use jacquard::api::app_bsky::feed::post::{Post, PostEmbed};
5use jacquard::client::{Agent, AgentSessionExt, FileAuthStore};
6use jacquard::oauth::client::OAuthClient;
7use jacquard::oauth::loopback::LoopbackConfig;
8use jacquard::types::blob::MimeType;
9use jacquard::types::string::Datetime;
10use miette::IntoDiagnostic;
11use smol_str::SmolStr;
12use std::path::PathBuf;
13
14#[derive(Parser, Debug)]
15#[command(author, version, about = "Create a post with an image")]
16struct Args {
17 /// Handle (e.g., alice.bsky.social), DID, or PDS URL
18 input: CowStr<'static>,
19
20 /// Post text
21 #[arg(short, long)]
22 text: String,
23
24 /// Path to image file
25 #[arg(short, long)]
26 image: PathBuf,
27
28 /// Alt text for image
29 #[arg(long)]
30 alt: Option<String>,
31
32 /// Path to auth store file (will be created if missing)
33 #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
34 store: String,
35}
36
37#[tokio::main]
38async fn main() -> miette::Result<()> {
39 let args = Args::parse();
40
41 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store));
42 let session = oauth
43 .login_with_local_server(args.input, Default::default(), LoopbackConfig::default())
44 .await?;
45
46 let agent: Agent<_> = Agent::from(session);
47
48 // Read image file
49 let image_data = std::fs::read(&args.image).into_diagnostic()?;
50
51 // Infer mime type from extension
52 let mime_str = match args.image.extension().and_then(|s| s.to_str()) {
53 Some("jpg") | Some("jpeg") => "image/jpeg",
54 Some("png") => "image/png",
55 Some("gif") => "image/gif",
56 Some("webp") => "image/webp",
57 _ => "image/jpeg", // default
58 };
59 let mime_type = MimeType::new(mime_str);
60
61 println!("Uploading image...");
62 let blob = agent.upload_blob(image_data, mime_type).await?;
63
64 // Create post with image embed
65 let post = Post {
66 text: SmolStr::from(args.text),
67 created_at: Datetime::now(),
68 embed: Some(PostEmbed::Images(Box::new(Images {
69 images: vec![Image {
70 alt: SmolStr::from(args.alt.unwrap_or_default()),
71 image: blob.into(),
72 aspect_ratio: None,
73 extra_data: Default::default(),
74 }],
75 extra_data: Default::default(),
76 }))),
77 entities: None,
78 facets: None,
79 labels: None,
80 langs: None,
81 reply: None,
82 tags: None,
83 extra_data: Default::default(),
84 };
85
86 let output = agent.create_record(post, None).await?;
87 println!("Created post with image: {}", output.uri);
88
89 Ok(())
90}