pub mod commands;
pub mod exit_codes;
use clap::{Parser, Subcommand};
pub use crate::output::OutputFormat;
#[derive(Parser, Debug)]
#[command(
name = "xr",
about = "Auth enabled curl-like interface for the X API",
long_about = r#"A command-line tool for making authenticated requests to the X API.
Shortcut commands (agent-friendly):
xr post "Hello world!" Post to X
xr reply 1234567890 "Nice!" Reply to a post
xr read 1234567890 Read a post
xr search "golang" -n 20 Search posts
xr whoami Show your profile
xr like 1234567890 Like a post
xr repost 1234567890 Repost
xr follow @user Follow a user
xr dm @user "Hey!" Send a DM
xr timeline Home timeline
xr mentions Your mentions
Raw API access (curl-style):
basic requests xr /2/users/me
xr -X POST /2/tweets -d '{"text":"Hello world!"}'
xr -H "Content-Type: application/json" /2/tweets
authentication xr --auth oauth2 /2/users/me
xr --auth oauth1 /2/users/me
xr --auth app /2/users/me
media and streaming xr media upload path/to/video.mp4
xr /2/tweets/search/stream --auth app
xr -s /2/users/me
Multi-app management:
xr auth apps add my-app --client-id ... --client-secret ...
xr auth apps list
xr auth default # interactive picker
xr auth default my-app # set by name
xr --app my-app /2/users/me # per-request override
Shell completions:
xr completions bash > ~/.bash_completion.d/xr
xr completions zsh > ~/.zfunc/_xr
xr completions fish > ~/.config/fish/completions/xr.fish
Run 'xr --help' to see all available commands."#,
version
)]
pub struct Cli {
#[arg(short = 'X', long = "method", global = false)]
pub method: Option<String>,
#[arg(short = 'H', long = "header")]
pub headers: Vec<String>,
#[arg(short = 'd', long = "data")]
pub data: Option<String>,
#[arg(long = "auth")]
pub auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
pub username: Option<String>,
#[arg(short = 'v', long = "verbose")]
pub verbose: bool,
#[arg(short = 't', long = "trace")]
pub trace: bool,
#[arg(short = 's', long = "stream")]
pub stream: bool,
#[arg(short = 'F', long = "file")]
pub file: Option<String>,
#[arg(long = "app", global = true)]
pub app: Option<String>,
#[arg(
long,
global = true,
default_value = "text",
value_enum,
env = "XURL_OUTPUT"
)]
pub output: OutputFormat,
#[arg(long, short = 'q', global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub no_interactive: bool,
#[arg(long, global = true, default_value = "30")]
pub timeout: u64,
#[command(subcommand)]
pub command: Option<Commands>,
pub url: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Post {
text: String,
#[arg(long = "media-id")]
media_ids: Vec<String>,
#[command(flatten)]
common: CommonFlags,
},
Reply {
post_id: String,
text: String,
#[arg(long = "media-id")]
media_ids: Vec<String>,
#[command(flatten)]
common: CommonFlags,
},
Quote {
post_id: String,
text: String,
#[command(flatten)]
common: CommonFlags,
},
Delete {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Read {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Search {
query: String,
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Whoami {
#[command(flatten)]
common: CommonFlags,
},
User {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Timeline {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Mentions {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Like {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Unlike {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Repost {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Unrepost {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Bookmark {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Unbookmark {
post_id: String,
#[command(flatten)]
common: CommonFlags,
},
Bookmarks {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Likes {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Follow {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Unfollow {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Following {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[arg(long = "of")]
of: Option<String>,
#[command(flatten)]
common: CommonFlags,
},
Followers {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[arg(long = "of")]
of: Option<String>,
#[command(flatten)]
common: CommonFlags,
},
Block {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Unblock {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Mute {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Unmute {
#[arg(value_name = "USERNAME")]
target_username: String,
#[command(flatten)]
common: CommonFlags,
},
Usage {
#[command(flatten)]
common: CommonFlags,
},
Dm {
#[arg(value_name = "USERNAME")]
target_username: String,
text: String,
#[command(flatten)]
common: CommonFlags,
},
Dms {
#[arg(short = 'n', long = "max-results", default_value = "10")]
max_results: i32,
#[command(flatten)]
common: CommonFlags,
},
Auth {
#[command(subcommand)]
command: AuthCommands,
},
Media {
#[command(subcommand)]
command: MediaCommands,
},
Schema {
command: Option<String>,
#[arg(long)]
list: bool,
#[arg(long)]
all: bool,
},
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
Version,
}
#[derive(clap::Args, Debug, Clone)]
pub struct CommonFlags {
#[arg(long = "auth")]
pub auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
pub username: Option<String>,
#[arg(short = 'v', long = "verbose")]
pub verbose: bool,
#[arg(short = 't', long = "trace")]
pub trace: bool,
}
impl CommonFlags {
pub fn to_call_options(&self) -> crate::api::CallOptions {
crate::api::CallOptions {
auth_type: self.auth_type.clone().unwrap_or_default(),
username: self.username.clone().unwrap_or_default(),
no_auth: false,
verbose: self.verbose,
trace: self.trace,
}
}
}
#[derive(Subcommand, Debug)]
pub enum AuthCommands {
Oauth2 {
#[arg(long)]
no_browser: bool,
#[arg(long, requires = "no_browser", value_parser = clap::value_parser!(u8).range(1..=2))]
step: Option<u8>,
#[arg(long = "auth-url", requires = "step")]
auth_url: Option<String>,
},
Oauth1 {
#[arg(long = "consumer-key")]
consumer_key: String,
#[arg(long = "consumer-secret")]
consumer_secret: String,
#[arg(long = "access-token")]
access_token: String,
#[arg(long = "token-secret")]
token_secret: String,
},
App {
#[arg(long = "bearer-token")]
bearer_token: String,
},
Status,
Clear {
#[arg(long)]
all: bool,
#[arg(long)]
oauth1: bool,
#[arg(long = "oauth2-username")]
oauth2_username: Option<String>,
#[arg(long)]
bearer: bool,
},
Apps {
#[command(subcommand)]
command: AppCommands,
},
Default {
app_name: Option<String>,
username: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum AppCommands {
Add {
name: String,
#[arg(long = "client-id")]
client_id: String,
#[arg(long = "client-secret")]
client_secret: String,
},
Update {
name: String,
#[arg(long = "client-id")]
client_id: Option<String>,
#[arg(long = "client-secret")]
client_secret: Option<String>,
},
Remove {
name: String,
},
List,
}
#[derive(Subcommand, Debug)]
pub enum MediaCommands {
Upload {
file: String,
#[arg(long = "media-type", default_value = "video/mp4")]
media_type: String,
#[arg(long = "category", default_value = "amplify_video")]
category: String,
#[arg(long = "wait", default_value = "true")]
wait: bool,
#[arg(long = "auth")]
auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(short = 'v', long = "verbose")]
verbose: bool,
#[arg(short = 't', long = "trace")]
trace: bool,
#[arg(short = 'H', long = "header")]
headers: Vec<String>,
},
Status {
media_id: String,
#[arg(long = "auth")]
auth_type: Option<String>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(short = 'v', long = "verbose")]
verbose: bool,
#[arg(short = 'w', long = "wait")]
wait: bool,
#[arg(short = 't', long = "trace")]
trace: bool,
#[arg(short = 'H', long = "header")]
headers: Vec<String>,
},
}