use crate::error::{XError, XResult};
use crate::types::{SearchTweetsParams, Tweet, User, XResponse};
use reqwest::Client;
#[derive(Debug, Clone)]
pub struct XClient {
client: Client,
bearer_token: String,
base_url: String,
}
impl XClient {
pub fn new(bearer_token: String) -> Self {
Self {
client: Client::new(),
bearer_token,
base_url: "https://api.twitter.com/2".to_string(),
}
}
pub fn from_env() -> XResult<Self> {
let bearer_token = std::env::var("X_BEARER_TOKEN")
.map_err(|_| XError::Config("X_BEARER_TOKEN not found".to_string()))?;
Ok(Self::new(bearer_token))
}
pub async fn get_user_by_username(&self, username: &str) -> XResult<Option<User>> {
let url = format!("{}/users/by/username/{}", self.base_url, username);
let response = self.client
.get(&url)
.bearer_auth(&self.bearer_token)
.query(&[("user.fields", "id,name,username,description,public_metrics,profile_image_url,verified,created_at")])
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(XError::Api {
status: status.as_u16(),
message: error_text,
});
}
let api_response: XResponse<User> = response.json().await?;
if let Some(errors) = api_response.errors {
if !errors.is_empty() {
return Err(XError::Api {
status: 400,
message: format!("API errors: {:?}", errors),
});
}
}
Ok(api_response.data)
}
pub async fn get_user_by_id(&self, user_id: &str) -> XResult<Option<User>> {
let url = format!("{}/users/{}", self.base_url, user_id);
let response = self.client
.get(&url)
.bearer_auth(&self.bearer_token)
.query(&[("user.fields", "id,name,username,description,public_metrics,profile_image_url,verified,created_at")])
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(XError::Api {
status: status.as_u16(),
message: error_text,
});
}
let api_response: XResponse<User> = response.json().await?;
if let Some(errors) = api_response.errors {
if !errors.is_empty() {
return Err(XError::Api {
status: 400,
message: format!("API errors: {:?}", errors),
});
}
}
Ok(api_response.data)
}
pub async fn search_tweets(&self, params: SearchTweetsParams) -> XResult<Vec<Tweet>> {
let url = format!("{}/tweets/search/recent", self.base_url);
let mut query_params = vec![("query", params.query)];
if let Some(max_results) = params.max_results {
query_params.push(("max_results", max_results.to_string()));
}
if let Some(tweet_fields) = params.tweet_fields {
query_params.push(("tweet.fields", tweet_fields.join(",")));
}
if let Some(user_fields) = params.user_fields {
query_params.push(("user.fields", user_fields.join(",")));
}
if let Some(expansions) = params.expansions {
query_params.push(("expansions", expansions.join(",")));
}
let response = self.client
.get(&url)
.bearer_auth(&self.bearer_token)
.query(&query_params)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(XError::Api {
status: status.as_u16(),
message: error_text,
});
}
let api_response: XResponse<Vec<Tweet>> = response.json().await?;
if let Some(errors) = api_response.errors {
if !errors.is_empty() {
return Err(XError::Api {
status: 400,
message: format!("API errors: {:?}", errors),
});
}
}
Ok(api_response.data.unwrap_or_default())
}
pub async fn get_tweet(&self, tweet_id: &str) -> XResult<Option<Tweet>> {
let url = format!("{}/tweets/{}", self.base_url, tweet_id);
let response = self.client
.get(&url)
.bearer_auth(&self.bearer_token)
.query(&[
("tweet.fields", "id,text,author_id,created_at,public_metrics,context_annotations,referenced_tweets"),
("expansions", "author_id"),
])
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(XError::Api {
status: status.as_u16(),
message: error_text,
});
}
let api_response: XResponse<Tweet> = response.json().await?;
if let Some(errors) = api_response.errors {
if !errors.is_empty() {
return Err(XError::Api {
status: 400,
message: format!("API errors: {:?}", errors),
});
}
}
Ok(api_response.data)
}
pub async fn get_user_tweets(&self, user_id: &str, max_results: Option<u32>) -> XResult<Vec<Tweet>> {
let url = format!("{}/users/{}/tweets", self.base_url, user_id);
let mut query_params = vec![
("tweet.fields", "id,text,author_id,created_at,public_metrics".to_string()),
];
if let Some(max) = max_results {
query_params.push(("max_results", max.to_string()));
}
let response = self.client
.get(&url)
.bearer_auth(&self.bearer_token)
.query(&query_params)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(XError::Api {
status: status.as_u16(),
message: error_text,
});
}
let api_response: XResponse<Vec<Tweet>> = response.json().await?;
if let Some(errors) = api_response.errors {
if !errors.is_empty() {
return Err(XError::Api {
status: 400,
message: format!("API errors: {:?}", errors),
});
}
}
Ok(api_response.data.unwrap_or_default())
}
}