rust_twitter_bot_lib/
lib.rs1extern 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#[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 pub fn new() -> Self {
51 TwitterBot::default()
52 }
53
54 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 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 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 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 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 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 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 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}