rust_twitter_bot_lib/
lib.rs

1//! A crate for creating Twitter bots
2//!
3//! # Example
4//!
5//! ```rust
6//! use rust_twitter_bot_lib::*;
7//!
8//! fn main() {
9//!   let example_bot = TwitterBot::new()
10//!     .consumer_key(YOUR_CONSUMER_KEY)
11//!     .consumer_secret_key(YOUR_CONSUMER_SECRET_KEY)
12//!     .access_token(YOUR_ACCESS_TOKEN)
13//!     .secret_access_token(YOUR_SECRET_ACCESS_TOKEN);
14//!
15//!   let res = example_bot.tweet("🐦 + 🦀 = 💙 #myfirstTweet").unwrap();
16//!
17//!   println!("{:?}", res);
18//! }
19//! ```
20extern crate oauthcli;
21extern crate reqwest;
22extern crate serde;
23extern crate url;
24
25mod tweet_structure;
26
27use std::{
28    collections::HashMap,
29    error::Error,
30    fmt::{self, Display},
31    path::Path,
32};
33pub use tweet_structure::{Tweet, User};
34
35use reqwest::multipart;
36use serde::{Deserialize, Serialize};
37
38/// The main struct provided by this crate. See crate documentation for more
39/// information.
40#[derive(Default, Serialize, Deserialize)]
41pub struct TwitterBot {
42    consumer_key: Option<String>,
43    consumer_secret_key: Option<String>,
44    access_token: Option<String>,
45    secret_access_token: Option<String>,
46}
47
48impl TwitterBot {
49    /// Creates a new Empty `TwitterBot`
50    pub fn new() -> Self {
51        TwitterBot::default()
52    }
53
54    /// Add your `consumer_key`<br/>  
55    /// Get it in your [Twitter App Dashboard](https://developer.twitter.com/en/apps/)
56    pub fn consumer_key(self, consumer_key: &str) -> Self {
57        Self {
58            consumer_key: Some(consumer_key.to_owned()),
59            ..self
60        }
61    }
62
63    /// Add your `consumer_secret_key`<br/>  
64    /// Get it in your [Twitter App Dashboard](https://developer.twitter.com/en/apps/)
65    pub fn consumer_secret_key(self, consumer_secret_key: &str) -> Self {
66        Self {
67            consumer_secret_key: Some(consumer_secret_key.to_owned()),
68            ..self
69        }
70    }
71
72    /// Add your `access_token`<br/>  
73    /// Get it in your [Twitter App Dashboard](https://developer.twitter.com/en/apps/)
74    pub fn access_token(self, access_token: &str) -> Self {
75        Self {
76            access_token: Some(access_token.to_owned()),
77            ..self
78        }
79    }
80
81    /// Add your `secret_access_token`<br/>  
82    /// Get it in your [Twitter App Dashboard](https://developer.twitter.com/en/apps/)
83    pub fn secret_access_token(self, secret_access_token: &str) -> Self {
84        Self {
85            secret_access_token: Some(secret_access_token.to_owned()),
86            ..self
87        }
88    }
89
90    fn is_connected(&self) -> Option<Box<Error>> {
91        if self.consumer_key.is_none() {
92            return Some(TwitterBotError::new("consumer_key missing").into());
93        } else if self.consumer_secret_key.is_none() {
94            return Some(TwitterBotError::new("consumer_secret_key missing").into());
95        } else if self.access_token.is_none() {
96            return Some(TwitterBotError::new("access_token missing").into());
97        } else if self.secret_access_token.is_none() {
98            return Some(TwitterBotError::new("secret_access_token missing").into());
99        }
100        return None;
101    }
102
103    fn send_request<T: for<'de> serde::Deserialize<'de>>(
104        &self,
105        url: url::Url,
106        method: &str,
107    ) -> Result<T, Box<Error>> {
108        if let Some(err) = self.is_connected() {
109            return Err(err);
110        }
111
112        let header = oauthcli::OAuthAuthorizationHeaderBuilder::new(
113            method,
114            &url,
115            self.consumer_key.as_ref().unwrap(),
116            self.consumer_secret_key.as_ref().unwrap(),
117            oauthcli::SignatureMethod::HmacSha1,
118        )
119        .token(
120            self.access_token.as_ref().unwrap(),
121            self.secret_access_token.as_ref().unwrap(),
122        )
123        .finish_for_twitter();
124
125        let client = reqwest::Client::new();
126        let mut response = if method == "POST" {
127            client
128                .post(&url.to_string())
129                .header("Authorization", header.to_string())
130                .send()?
131        } else if method == "GET" {
132            client
133                .get(&url.to_string())
134                .header("Authorization", header.to_string())
135                .send()?
136        } else {
137            panic!("Invalid method");
138        };
139
140        if response.status() == 200 {
141            return Ok(response.json()?);
142        } else {
143            let err: tweet_structure::TwitterError = response.json()?;
144            return Err(TwitterBotError::new(&err.message()).into());
145        }
146    }
147
148    /// Create a media from a file<br/>
149    /// Will fail if `consumer_key`, `consumer_key`, `access_token` and `secret_access_token` are not set
150    pub fn upload_file(&self, path: &Path) -> Result<u64, Box<Error>> {
151        if let Some(err) = self.is_connected() {
152            return Err(err);
153        }
154
155        let header = oauthcli::OAuthAuthorizationHeaderBuilder::new(
156            "POST",
157            &url::Url::parse("https://upload.twitter.com/1.1/media/upload.json")?,
158            self.consumer_key.as_ref().unwrap(),
159            self.consumer_secret_key.as_ref().unwrap(),
160            oauthcli::SignatureMethod::HmacSha1,
161        )
162        .token(
163            self.access_token.as_ref().unwrap(),
164            self.secret_access_token.as_ref().unwrap(),
165        )
166        .finish_for_twitter();
167
168        let form = multipart::Form::new().part("media", multipart::Part::file(path)?);
169
170        let client = reqwest::Client::new();
171
172        let mut response = client
173            .post("https://upload.twitter.com/1.1/media/upload.json")
174            .header("Authorization", header.to_string())
175            .multipart(form)
176            .send()?;
177
178        if response.status() == 200 {
179            let res: tweet_structure::Media = response.json()?;
180            return Ok(res.media_id);
181        } else {
182            let err: tweet_structure::TwitterError = response.json()?;
183            return Err(TwitterBotError::new(&err.message()).into());
184        }
185    }
186
187    /// Tweet `content`<br/>  
188    /// Will fail if `consumer_key`, `consumer_key`, `access_token` and `secret_access_token` are not set
189    pub fn tweet(
190        &self,
191        content: &str,
192        params: Option<HashMap<&str, &str>>,
193    ) -> Result<tweet_structure::Tweet, Box<Error>> {
194        let mut request = url::Url::parse("https://api.twitter.com/1.1/statuses/update.json")?;
195
196        {
197            let mut query_pairs = request.query_pairs_mut();
198            query_pairs.append_pair("status", content);
199            if let Some(pairs) = params {
200                for (key, value) in pairs.iter() {
201                    query_pairs.append_pair(key, value);
202                }
203            }
204        }
205
206        let request = url::Url::parse(&request.to_string().replace("+", "%20"))?;
207
208        Ok(self.send_request(request, "POST")?)
209    }
210
211    /// Get tweet with id = `tweet_id`<br/>  
212    /// Will fail if `consumer_key`, `consumer_key`, `access_token` and `secret_access_token` are not set
213    pub fn get_tweet(&self, tweet_id: &str) -> Result<tweet_structure::Tweet, Box<Error>> {
214        let mut request = url::Url::parse("https://api.twitter.com/1.1/statuses/show.json")?;
215        request.query_pairs_mut().append_pair("id", tweet_id);
216
217        Ok(self.send_request(request, "GET")?)
218    }
219
220    /// Get tweet that satisfy `query`<br/>  
221    /// Will fail if `consumer_key`, `consumer_key`, `access_token` and `secret_access_token` are not set
222    pub fn get_tweets_query(
223        &self,
224        query: &str,
225        params: Option<HashMap<&str, &str>>,
226    ) -> Result<Vec<tweet_structure::Tweet>, Box<Error>> {
227        let mut request = url::Url::parse("https://api.twitter.com/1.1/search/tweets.json")?;
228
229        {
230            let mut query_pairs = request.query_pairs_mut();
231            query_pairs.append_pair("q", query);
232            query_pairs.append_pair("count", "100");
233            if let Some(pairs) = params {
234                for (key, value) in pairs.iter() {
235                    query_pairs.append_pair(key, value);
236                }
237            }
238        }
239
240        let request = url::Url::parse(&request.to_string().replace("+", "%20"))?;
241
242        let response: tweet_structure::SearchResponse = self.send_request(request, "GET")?;
243
244        Ok(response.statuses)
245    }
246}
247
248#[derive(Debug)]
249struct TwitterBotError(String);
250
251impl Display for TwitterBotError {
252    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
253        write!(f, "{}", self.0)
254    }
255}
256
257impl Error for TwitterBotError {
258    fn description(&self) -> &str {
259        &self.0
260    }
261
262    fn cause(&self) -> Option<&Error> {
263        None
264    }
265}
266
267impl TwitterBotError {
268    fn new(description: &str) -> Self {
269        Self(description.to_owned())
270    }
271}