use clap::{Parser, Subcommand};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Parser)]
#[command(name = "autoreply")]
#[command(about = "Bluesky profile and post search utility", long_about = None)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub quiet: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Profile(ProfileArgs),
Search(SearchArgs),
Login(LoginCommand),
Feed(FeedArgs),
Thread(ThreadArgs),
Post(PostArgs),
React(ReactArgs),
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct ProfileArgs {
#[arg(short = 'a', long)]
#[schemars(description = "Handle (alice.bsky.social) or DID (did:plc:...)")]
pub account: String,
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct SearchArgs {
#[arg(short = 'f', long)]
#[schemars(description = "Handle or DID for account whose posts you want to search")]
pub from: String,
#[arg(short = 'q', long)]
#[schemars(description = "Search terms (case-insensitive)")]
pub query: String,
#[arg(short = 'l', long)]
#[schemars(description = "Maximum number of results (default 50)")]
pub limit: Option<usize>,
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct PostArgs {
#[arg(short = 'a', long)]
#[schemars(description = "Handle or DID to post as (postAs)")]
#[serde(rename = "postAs")]
pub post_as: String,
#[arg(short = 't', long)]
#[schemars(description = "The text content of the post")]
pub text: String,
#[arg(short = 'r', long)]
#[schemars(description = "Optional at:// URI or https://bsky.app/... URL to reply to")]
#[serde(rename = "replyTo")]
pub reply_to: Option<String>,
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct FeedArgs {
#[arg(short = 'f', long)]
#[schemars(description = "Optional feed URI or name. If unspecified, returns the default popular feed")]
pub feed: Option<String>,
#[arg(short = 'u', long)]
#[schemars(description = "Optional BlueSky handle for authenticated access")]
pub login: Option<String>,
#[arg(short = 'p', long)]
#[schemars(description = "Optional BlueSky password")]
pub password: Option<String>,
#[arg(short = 'c', long)]
#[schemars(description = "Optional cursor for pagination")]
pub cursor: Option<String>,
#[arg(short = 'l', long)]
#[schemars(description = "Limit the number of posts (default 20)")]
pub limit: Option<usize>,
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct ThreadArgs {
#[arg(short = 'p', long)]
#[schemars(description = "The BlueSky URL or at:// URI of the post to fetch the thread for")]
#[serde(rename = "postURI")]
pub post_uri: String,
#[arg(short = 'u', long)]
#[schemars(description = "Optional BlueSky handle for authenticated access")]
pub login: Option<String>,
#[arg(short = 'w', long)]
#[schemars(description = "Optional BlueSky password")]
pub password: Option<String>,
}
#[derive(Parser, JsonSchema, Deserialize, Serialize, Clone, Debug)]
pub struct ReactArgs {
#[arg(short = 'a', long)]
#[schemars(description = "Handle or DID to react as (reactAs)")]
#[serde(rename = "reactAs")]
pub react_as: String,
#[arg(long)]
#[schemars(description = "Array of post URIs/URLs to like")]
#[serde(default)]
pub like: Vec<String>,
#[arg(long)]
#[schemars(description = "Array of post URIs/URLs to unlike")]
#[serde(default)]
pub unlike: Vec<String>,
#[arg(long)]
#[schemars(description = "Array of post URIs/URLs to repost")]
#[serde(default)]
pub repost: Vec<String>,
#[arg(long)]
#[schemars(description = "Array of post URIs/URLs to delete")]
#[serde(default)]
pub delete: Vec<String>,
}
#[derive(Parser, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LoginCommand {
#[command(subcommand)]
pub command: Option<LoginSubcommands>,
#[arg(short = 'u', long, global = true)]
pub handle: Option<String>,
#[arg(short = 'p', long, num_args = 0..=1, default_missing_value = "", global = true)]
pub password: Option<String>,
#[arg(short = 's', long, global = true)]
pub service: Option<String>,
}
#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum LoginSubcommands {
List,
Default {
handle: String,
},
Delete {
#[arg(short = 'u', long)]
handle: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_args() {
let args = ProfileArgs {
account: "alice.bsky.social".to_string(),
};
assert_eq!(args.account, "alice.bsky.social");
}
#[test]
fn test_search_args() {
let args = SearchArgs {
from: "bob.bsky.social".to_string(),
query: "rust programming".to_string(),
limit: Some(10),
};
assert_eq!(args.from, "bob.bsky.social");
assert_eq!(args.query, "rust programming");
assert_eq!(args.limit, Some(10));
}
#[test]
fn test_post_args() {
let args = PostArgs {
post_as: "alice.bsky.social".to_string(),
text: "Hello, world!".to_string(),
reply_to: None,
};
assert_eq!(args.post_as, "alice.bsky.social");
assert_eq!(args.text, "Hello, world!");
assert!(args.reply_to.is_none());
}
#[test]
fn test_react_args() {
let args = ReactArgs {
react_as: "bob.bsky.social".to_string(),
like: vec!["at://did:plc:abc/app.bsky.feed.post/123".to_string()],
unlike: vec![],
repost: vec![],
delete: vec![],
};
assert_eq!(args.react_as, "bob.bsky.social");
assert_eq!(args.like.len(), 1);
assert_eq!(args.unlike.len(), 0);
}
#[test]
fn test_feed_args() {
let args = FeedArgs {
feed: Some("at://did:plc:xyz/app.bsky.feed.generator/hot".to_string()),
login: Some("alice.bsky.social".to_string()),
password: None,
cursor: None,
limit: Some(50),
};
assert_eq!(args.feed, Some("at://did:plc:xyz/app.bsky.feed.generator/hot".to_string()));
assert_eq!(args.limit, Some(50));
}
#[test]
fn test_thread_args() {
let args = ThreadArgs {
post_uri: "at://did:plc:abc/app.bsky.feed.post/123".to_string(),
login: None,
password: None,
};
assert_eq!(args.post_uri, "at://did:plc:abc/app.bsky.feed.post/123");
}
}