use clap::Subcommand;
use tail_fin_common::TailFinError;
use tail_fin_twitter::{extract_tweet_id, TimelineType, TwitterApi, TwitterClient};
use crate::session::{
browser_session, no_mode_error, print_json, print_list, require_browser, twitter_http_client,
Ctx,
};
macro_rules! twitter_http_cmd {
($ctx:expr, $cmd:expr, $client:ident => $body:expr) => {{
let $client = if let Some(ref cf) = $ctx.cookies {
twitter_http_client(cf)?
} else if let Some(ref host) = $ctx.connect {
tail_fin_twitter::TwitterHttpClient::from_browser(host).await?
} else {
return Err(no_mode_error("twitter", $cmd));
};
$body
}};
}
#[derive(Subcommand)]
pub enum TwitterAction {
Timeline {
#[arg(long, default_value = "following")]
r#type: String,
#[arg(long, default_value_t = 20)]
count: usize,
#[arg(long)]
cursor: Option<String>,
},
Search {
query: String,
#[arg(long, default_value_t = 20)]
count: usize,
#[arg(long)]
cursor: Option<String>,
},
Profile {
username: String,
},
Bookmarks {
#[arg(long, default_value_t = 20)]
count: usize,
#[arg(long)]
cursor: Option<String>,
},
Likes {
username: String,
#[arg(long, default_value_t = 20)]
count: usize,
#[arg(long)]
cursor: Option<String>,
},
Thread {
tweet_id: String,
#[arg(long, default_value_t = 50)]
count: usize,
},
Post {
text: String,
},
Like {
url: String,
},
Follow {
username: String,
},
Unfollow {
username: String,
},
Delete {
url: String,
},
Block {
username: String,
},
Unblock {
username: String,
},
Bookmark {
url: String,
},
Unbookmark {
url: String,
},
Reply {
url: String,
text: String,
},
Trending {
#[arg(long, default_value_t = 20)]
count: usize,
},
Followers {
username: String,
#[arg(long, default_value_t = 50)]
count: usize,
},
Following {
username: String,
#[arg(long, default_value_t = 50)]
count: usize,
},
Notifications {
#[arg(long, default_value_t = 20)]
count: usize,
},
Download {
username: String,
#[arg(long, default_value_t = 10)]
count: usize,
},
HideReply {
url: String,
},
ExportCookies {
#[arg(long, value_name = "PATH")]
output: Option<String>,
},
Article {
tweet_id: String,
},
Lists {
#[arg(long, default_value_t = 20)]
count: usize,
},
ReplyDm {
text: String,
#[arg(long, default_value_t = 10)]
count: usize,
},
Accept {
#[arg(long)]
filter: Option<String>,
#[arg(long, default_value_t = 20)]
count: usize,
},
}
pub async fn run(action: TwitterAction, ctx: &Ctx) -> Result<(), TailFinError> {
match action {
TwitterAction::ExportCookies { output } => {
let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
let out_path = output
.map(std::path::PathBuf::from)
.unwrap_or_else(|| crate::session::default_cookies_path("twitter"));
let count = tail_fin_twitter::auth::export_cookies(chrome_host, &out_path).await?;
eprintln!("Saved {} cookies to {}", count, out_path.display());
}
TwitterAction::Timeline {
r#type,
count,
cursor,
} => {
let kind = match r#type.as_str() {
"for-you" => TimelineType::ForYou,
"following" => TimelineType::Following,
other => {
return Err(TailFinError::Api(format!(
"Unknown timeline type '{other}'. Use 'for-you' or 'following'."
)));
}
};
if let Some(ref cookies_flag) = ctx.cookies {
let client = twitter_http_client(cookies_flag)?;
let resp = client.timeline(kind, count, cursor.as_deref()).await?;
print_json(&resp)?;
} else if let Some(ref host) = ctx.connect {
let session = browser_session(host, ctx.headed).await?;
let client = TwitterClient::new(session);
let resp = client.timeline(kind, count, cursor.as_deref()).await?;
print_json(&resp)?;
} else {
return Err(no_mode_error("twitter", "timeline"));
}
}
TwitterAction::Search {
query,
count,
cursor,
} => {
if let Some(ref cookies_flag) = ctx.cookies {
let client = twitter_http_client(cookies_flag)?;
let resp = client.search(&query, count, cursor.as_deref()).await?;
print_json(&resp)?;
} else if let Some(ref host) = ctx.connect {
let session = browser_session(host, ctx.headed).await?;
let client = TwitterClient::new(session);
let resp = client.search(&query, count, cursor.as_deref()).await?;
print_json(&resp)?;
} else {
return Err(no_mode_error("twitter", "search"));
}
}
TwitterAction::Profile { username } => {
twitter_http_cmd!(ctx, "profile", client => {
let profile = client.profile(&username).await?;
print_json(&profile)?;
});
}
TwitterAction::Bookmarks { count, cursor } => {
twitter_http_cmd!(ctx, "bookmarks", client => {
let resp = client.bookmarks(count, cursor.as_deref()).await?;
print_json(&resp)?;
});
}
TwitterAction::Likes {
username,
count,
cursor,
} => {
twitter_http_cmd!(ctx, "likes", client => {
let resp = client.likes(&username, count, cursor.as_deref()).await?;
print_json(&resp)?;
});
}
TwitterAction::Thread { tweet_id, count } => {
let id = extract_tweet_id(&tweet_id);
twitter_http_cmd!(ctx, "thread", client => {
let tweets = client.thread(&id, count).await?;
print_list("tweets", &tweets, tweets.len())?;
});
}
TwitterAction::Post { text } => {
let host = require_browser(&ctx.connect, "twitter", "post")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).post(&text).await?;
print_json(&result)?;
}
TwitterAction::Like { url } => {
let host = require_browser(&ctx.connect, "twitter", "like")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).like(&url).await?;
print_json(&result)?;
}
TwitterAction::Follow { username } => {
let host = require_browser(&ctx.connect, "twitter", "follow")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).follow(&username).await?;
print_json(&result)?;
}
TwitterAction::Unfollow { username } => {
let host = require_browser(&ctx.connect, "twitter", "unfollow")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).unfollow(&username).await?;
print_json(&result)?;
}
TwitterAction::Delete { url } => {
let host = require_browser(&ctx.connect, "twitter", "delete")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).delete(&url).await?;
print_json(&result)?;
}
TwitterAction::Block { username } => {
let host = require_browser(&ctx.connect, "twitter", "block")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).block(&username).await?;
print_json(&result)?;
}
TwitterAction::Unblock { username } => {
let host = require_browser(&ctx.connect, "twitter", "unblock")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).unblock(&username).await?;
print_json(&result)?;
}
TwitterAction::Bookmark { url } => {
let host = require_browser(&ctx.connect, "twitter", "bookmark")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).bookmark(&url).await?;
print_json(&result)?;
}
TwitterAction::Unbookmark { url } => {
let host = require_browser(&ctx.connect, "twitter", "unbookmark")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).unbookmark(&url).await?;
print_json(&result)?;
}
TwitterAction::Reply { url, text } => {
let host = require_browser(&ctx.connect, "twitter", "reply")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).reply(&url, &text).await?;
print_json(&result)?;
}
TwitterAction::Trending { count } => {
let host = require_browser(&ctx.connect, "twitter", "trending")?;
let session = browser_session(&host, ctx.headed).await?;
let trends = TwitterClient::new(session).trending(count).await?;
print_list("trends", &trends, trends.len())?;
}
TwitterAction::Followers { username, count } => {
twitter_http_cmd!(ctx, "followers", client => {
let users = client.followers(&username, count).await?;
print_list("users", &users, users.len())?;
});
}
TwitterAction::Following { username, count } => {
twitter_http_cmd!(ctx, "following", client => {
let users = client.following(&username, count).await?;
print_list("users", &users, users.len())?;
});
}
TwitterAction::Notifications { count } => {
twitter_http_cmd!(ctx, "notifications", client => {
let notifs = client.notifications(count).await?;
print_list("notifications", ¬ifs, notifs.len())?;
});
}
TwitterAction::Download { username, count } => {
let host = require_browser(&ctx.connect, "twitter", "download")?;
let session = browser_session(&host, ctx.headed).await?;
let media = TwitterClient::new(session)
.download(&username, count)
.await?;
print_list("media", &media, media.len())?;
}
TwitterAction::HideReply { url } => {
let host = require_browser(&ctx.connect, "twitter", "hide-reply")?;
let session = browser_session(&host, ctx.headed).await?;
let result = TwitterClient::new(session).hide_reply(&url).await?;
print_json(&result)?;
}
TwitterAction::Article { tweet_id } => {
let id = extract_tweet_id(&tweet_id);
twitter_http_cmd!(ctx, "article", client => {
let article = client.article(&id).await?;
print_json(&article)?;
});
}
TwitterAction::Lists { count } => {
let host = require_browser(&ctx.connect, "twitter", "lists")?;
let session = browser_session(&host, ctx.headed).await?;
let lists = TwitterClient::new(session).lists(count).await?;
print_list("lists", &lists, lists.len())?;
}
TwitterAction::ReplyDm { text, count } => {
let host = require_browser(&ctx.connect, "twitter", "reply-dm")?;
let session = browser_session(&host, ctx.headed).await?;
let results = TwitterClient::new(session).reply_dm(&text, count).await?;
print_list("dm_results", &results, results.len())?;
}
TwitterAction::Accept { filter, count } => {
let host = require_browser(&ctx.connect, "twitter", "accept")?;
let session = browser_session(&host, ctx.headed).await?;
let results = TwitterClient::new(session)
.accept(filter.as_deref(), count)
.await?;
print_list("accept_results", &results, results.len())?;
}
}
Ok(())
}
pub struct Adapter;
impl crate::adapter::CliAdapter for Adapter {
fn name(&self) -> &'static str {
"twitter"
}
fn about(&self) -> &'static str {
"Twitter/X operations"
}
fn command(&self) -> clap::Command {
<TwitterAction as clap::Subcommand>::augment_subcommands(
clap::Command::new("twitter").about("Twitter/X operations"),
)
}
fn dispatch<'a>(
&'a self,
matches: &'a clap::ArgMatches,
ctx: &'a crate::session::Ctx,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
>,
> {
Box::pin(async move {
let action = <TwitterAction as clap::FromArgMatches>::from_arg_matches(matches)
.map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
run(action, ctx).await
})
}
}