1#![allow(unused)]
2
3use reqwest::multipart::Form;
4use reqwest::{header::AUTHORIZATION, Client};
5use reqwest::{Method, Url};
6use serde::de::DeserializeOwned;
7
8use std::{
9 collections::HashMap,
10 env, fs,
11 str::FromStr,
12 time::{SystemTime, UNIX_EPOCH},
13};
14
15use serde::Deserialize;
16use serde_json::{json, Value};
17
18#[derive(Debug, Deserialize)]
19struct TwitterApiResponseError {
20 code: Option<u32>,
21 message: String,
22}
23
24#[derive(Debug, Deserialize)]
25pub struct TwitterPostData {
26 id: String,
27 text: Option<String>,
28}
29impl TwitterPostData {
30 pub fn id(&self) -> &str {
31 &self.id
32 }
33
34 pub fn has_text(&self) -> bool {
35 self.text.is_some()
36 }
37 pub fn description(&self) -> &str {
38 match &self.text {
39 Some(description) => description,
40 None => "none",
41 }
42 }
43}
44
45#[derive(Debug, Deserialize)]
46struct TwitterPost {
47 data: Option<TwitterPostData>,
48 errors: Option<Vec<TwitterApiResponseError>>,
49}
50
51pub mod auth;
52use auth::*;
53
54pub struct TweetMediaBuilder(pub HashMap<&'static str, Value>);
55impl TweetMediaBuilder {
56 pub fn add(&mut self, media: Option<TwitterMediaResponse>) -> &mut Self {
57 if let Some(data) = media {
58 let medias = self
59 .0
60 .entry("media_ids")
61 .or_insert_with(|| Value::from(Vec::<Value>::new()))
62 .as_array_mut()
63 .expect("no media_ids???");
64
65 medias.push(Value::from(data.media_id_string.as_str()));
66 }
67
68 self
69 }
70
71 pub fn id(&mut self, id: u64) -> &mut Self {
72 let medias = self
73 .0
74 .entry("media_ids")
75 .or_insert_with(|| Value::from(Vec::<Value>::new()))
76 .as_array_mut()
77 .expect("no media_ids???");
78
79 medias.push(Value::String(id.to_string()));
80 self
81 }
82}
83impl Default for TweetMediaBuilder {
84 fn default() -> TweetMediaBuilder {
85 let mut map = HashMap::new();
86 map.insert("media_ids", Value::from(Vec::<Value>::new()));
87
88 TweetMediaBuilder(map)
89 }
90}
91
92#[derive(Default)]
93pub struct TweetBuilder(pub HashMap<&'static str, Value>);
94impl TweetBuilder {
95 pub fn text(&mut self, text: &str) -> &mut Self {
96 self.0.insert("text", Value::from(text));
97 self
98 }
99
100 pub fn media<F>(&mut self, f: F) -> &mut Self
109 where
110 F: FnOnce(&mut TweetMediaBuilder) -> &mut TweetMediaBuilder,
111 {
112 let mut media = TweetMediaBuilder::default();
113 f(&mut media);
114
115 self.0.insert("media", json!(media.0));
116 self
117 }
118}
119
120pub mod error;
121use error::Error;
122
123#[derive(Debug, Deserialize)]
124struct TwitterApiResponse {
125 detail: Option<String>,
126 errors: Option<Vec<TwitterApiResponseError>>,
127 data: Option<Value>,
128}
129
130#[derive(Clone)]
131pub struct TwitterClient {
132 http: Client,
133 auth: TwitterAuth,
134}
135impl TwitterClient {
136 pub fn new(auth: TwitterAuth) -> Result<Self, Box<dyn std::error::Error>> {
137 let http = Client::builder()
138 .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15")
139 .build()?;
140
141 Ok(Self { http, auth })
142 }
143
144 async fn _request_t<T: DeserializeOwned>(
145 &mut self,
146 method: &str,
147 url: &str,
148 query: Option<&[(&str, &str)]>,
149 ) -> Result<T, Error> {
150 Ok(self
151 .http
152 .request(
153 Method::from_str(method).unwrap_or(Method::GET),
154 Url::parse_with_params(url, query.unwrap_or_default()).unwrap(),
155 )
156 .header(AUTHORIZATION, &self.auth.header(method, url, query))
157 .send()
158 .await?
159 .json::<T>()
160 .await?)
161 }
162
163 async fn _request<T: DeserializeOwned>(
164 &mut self,
165 method: &str,
166 url: &str,
167 query: Option<&[(&str, &str)]>,
168 ) -> Result<T, Error> {
169 let res = self
170 .http
171 .request(
172 Method::from_str(method).unwrap_or(Method::GET),
173 Url::parse_with_params(url, query.unwrap_or_default()).unwrap(),
174 )
175 .header(AUTHORIZATION, &self.auth.header(method, url, query))
176 .send()
177 .await?
178 .json::<TwitterApiResponse>()
179 .await?;
180
181 match res.data {
182 Some(data) => Ok(serde_json::from_value(data).unwrap()),
183 None => {
184 if let Some(detail) = res.detail {
185 match detail.as_ref() {
186 "Too Many Requests" => Err(Error::TooManyRequests),
187 _ => Err(Error::Unknown),
188 }
189 } else if let Some(errors) = res.errors {
190 println!("got errors: {:?}", errors);
191 Err(Error::Unknown)
192 } else {
193 Err(Error::Unknown)
194 }
195 }
196 }
197 }
198
199 async fn _json_request<T: DeserializeOwned>(
200 &mut self,
201 method: &str,
202 url: &str,
203 json: Value,
204 query: Option<&[(&str, &str)]>,
205 ) -> Result<T, Error> {
206 let res = self
207 .http
208 .request(
209 Method::from_str(method).unwrap_or(Method::GET),
210 Url::parse_with_params(url, query.unwrap_or_default()).unwrap(),
211 )
212 .header(AUTHORIZATION, &self.auth.header(method, url, query))
213 .json(&json)
214 .send()
215 .await?
216 .json::<TwitterApiResponse>()
217 .await?;
218
219 match res.data {
220 Some(data) => Ok(serde_json::from_value(data).unwrap()),
221 None => {
222 if let Some(detail) = res.detail {
223 match detail.as_ref() {
224 "Too Many Requests" => Err(Error::TooManyRequests),
225 _ => Err(Error::Unknown),
226 }
227 } else if let Some(errors) = res.errors {
228 println!("got errors: {:?}", errors);
229 Err(Error::Unknown)
230 } else {
231 Err(Error::Unknown)
232 }
233 }
234 }
235 }
236
237 async fn _multipart_request<T: DeserializeOwned>(
238 &mut self,
239 method: &str,
240 url: &str,
241 multipart: Form,
242 query: Option<&[(&str, &str)]>,
243 ) -> Result<T, Error> {
244 let res = self
245 .http
246 .request(
247 Method::from_str(method).unwrap_or(Method::GET),
248 Url::parse_with_params(url, query.unwrap_or_default()).unwrap(),
249 )
250 .header(AUTHORIZATION, &self.auth.header(method, url, query))
251 .multipart(multipart)
252 .send()
253 .await?
254 .json::<Value>()
255 .await?;
256
257 if let Some(errors) = res.get("errors") {
258 println!("got errors: {:?}", errors);
259 return Err(Error::Unknown);
260 }
261
262 match serde_json::from_value::<T>(res) {
263 Ok(data) => Ok(data),
264 Err(_) => Err(Error::BadMedia),
265 }
266 }
267
268 pub async fn me(&mut self, fields: Option<&[&str]>) -> Result<TwitterUserData, Error> {
269 let fields_str = fields.map_or(String::new(), |f| f.join(","));
270 let query = [("user.fields", fields_str.as_str())];
271
272 self._request("GET", "https://api.twitter.com/2/users/me", Some(&query))
273 .await
274 }
275
276 pub async fn upload_media(
277 &mut self,
278 path: &str,
279 filename: Option<String>,
280 ) -> Result<TwitterMediaResponse, Error> {
281 let file_bytes;
282 let mime;
283 if path.starts_with("http") {
284 let media = reqwest::get(path).await?;
285 let headers = media.headers().clone();
286 let content_type_header = headers
287 .get(reqwest::header::CONTENT_TYPE)
288 .unwrap()
289 .to_str()
290 .unwrap()
291 .to_owned();
292 file_bytes = media.bytes().await?.to_vec();
293 mime = content_type_header;
294 } else {
295 match fs::read(path) {
296 Ok(bytes) => {
297 file_bytes = bytes;
298 mime = infer::get(&file_bytes).unwrap().mime_type().to_string();
299 }
300 _ => return Err(Error::BadMedia),
301 }
302 }
303
304 let mut chunked = false;
305 let len = file_bytes.len();
306 if len <= 1024 * 1024 {
307 let file_part = reqwest::multipart::Part::bytes(file_bytes)
309 .file_name(filename.unwrap_or("media".into()));
310 let form = reqwest::multipart::Form::new().part("media", file_part);
311
312 self._multipart_request(
313 "POST",
314 "https://upload.twitter.com/1.1/media/upload.json",
315 form,
316 None,
317 )
318 .await
319 } else {
320 chunked = true;
322 let init = self
323 ._multipart_request::<TwitterMediaResponse>(
324 "POST",
325 "https://upload.twitter.com/1.1/media/upload.json",
326 reqwest::multipart::Form::new()
327 .text("command", "INIT")
328 .text("total_bytes", len.to_string())
329 .text("media_type", mime.clone()),
330 None,
331 )
332 .await;
333
334 let media_id = match init {
335 Ok(data) => data.media_id_string,
336 _ => return Err(Error::BadMedia),
337 };
338
339 for (i, chunk) in file_bytes.chunks(1024 * 1024).enumerate() {
340 let append = self
341 .http
342 .post("https://upload.twitter.com/1.1/media/upload.json")
343 .header(
344 AUTHORIZATION,
345 &self.auth.header(
346 "POST",
347 "https://upload.twitter.com/1.1/media/upload.json",
348 None,
349 ),
350 )
351 .multipart(
352 reqwest::multipart::Form::new()
353 .text("command", "APPEND")
354 .text("media_id", media_id.to_string())
355 .text("segment_index", i.to_string())
356 .part(
357 "media",
358 reqwest::multipart::Part::bytes(chunk.to_vec())
359 .file_name(format!("media_chunk_{}", i)),
360 ),
361 )
362 .send()
363 .await?
364 .status();
365
366 if append != 204 {
367 return Err(Error::BadMedia);
368 }
369 }
370
371 let mut finalize_form = reqwest::multipart::Form::new()
372 .text("command", "FINALIZE")
373 .text("media_id", media_id.to_string());
374
375 if chunked {
376 finalize_form = finalize_form.text("allow_async", "true");
377 }
378
379 let finalize = self
380 ._multipart_request::<TwitterMediaResponse>(
381 "POST",
382 "https://upload.twitter.com/1.1/media/upload.json",
383 finalize_form,
384 None,
385 )
386 .await?;
387
388 if finalize.processing_info.is_some() {
389 loop {
390 let status = self
391 ._request_t::<TwitterMediaResponse>(
392 "GET",
393 "https://upload.twitter.com/1.1/media/upload.json",
394 Some(&[("command", "STATUS"), ("media_id", &media_id)]),
395 )
396 .await;
397
398 match status {
399 Ok(mut data) => match data.status() {
400 MediaStatus::InProgress => {
401 tokio::time::sleep(tokio::time::Duration::from_secs(
403 data.seconds_left(),
404 ))
405 .await;
406 continue;
407 }
408 MediaStatus::Succeeded => return Ok(data),
409 _ => return Err(Error::BadMedia),
410 },
411 _ => return Err(Error::BadMedia),
412 }
413 }
414 } else {
415 Ok(finalize)
416 }
417 }
418 }
419
420 pub async fn tweet<F>(&mut self, f: F) -> Result<TwitterPostData, Error>
421 where
422 F: FnOnce(&mut TweetBuilder) -> &mut TweetBuilder,
423 {
424 let mut tweet = TweetBuilder::default();
425 f(&mut tweet);
426
427 self._json_request(
428 "POST",
429 "https://api.twitter.com/2/tweets",
430 json!(tweet.0),
431 None,
432 )
433 .await
434 }
435}
436
437#[derive(Debug, Deserialize)]
438pub struct TwitterMediaResponseProcessingInfo {
439 state: String,
440 progress_percent: Option<u32>,
441 check_after_secs: Option<u64>,
442}
443
444pub enum MediaStatus {
445 InProgress,
446 Succeeded,
447 Failed,
448 Bad,
449}
450
451#[derive(Debug, Deserialize)]
452pub struct TwitterMediaResponse {
453 media_id: u64,
454 media_id_string: String,
455 expires_after_secs: Option<u32>,
456 processing_info: Option<TwitterMediaResponseProcessingInfo>,
457}
458impl TwitterMediaResponse {
459 pub fn status(&mut self) -> MediaStatus {
460 match &self.processing_info {
461 Some(processing_info) => match processing_info.state.as_ref() {
462 "in_progress" => MediaStatus::InProgress,
463 "succeeded" => MediaStatus::Succeeded,
464 "failed" => MediaStatus::Failed,
465 _ => MediaStatus::Bad,
466 },
467 _ => MediaStatus::Bad,
468 }
469 }
470
471 pub fn seconds_left(&mut self) -> u64 {
472 self.processing_info
473 .as_ref()
474 .unwrap()
475 .check_after_secs
476 .unwrap_or(1)
477 }
478
479 pub fn id(&mut self) -> &str {
480 &self.media_id_string
481 }
482}
483
484#[derive(Debug, Deserialize)]
485pub struct TwitterUserData {
486 id: String,
487 name: String,
488 username: String,
489 description: Option<String>,
490 created_at: Option<String>,
491}
492impl TwitterUserData {
493 pub fn id(&self) -> &str {
494 &self.id
495 }
496
497 pub fn name(&self) -> &str {
498 &self.name
499 }
500
501 pub fn username(&self) -> &str {
502 &self.username
503 }
504
505 pub fn has_description(&self) -> bool {
506 self.description.is_some()
507 }
508 pub fn description(&self) -> &str {
509 match &self.description {
510 Some(description) => description,
511 None => "",
512 }
513 }
514
515 pub fn has_created_at(&self) -> bool {
516 self.created_at.is_some()
517 }
518 pub fn created_at(&self) -> &str {
519 match &self.created_at {
520 Some(date) => date,
521 None => "invalid",
522 }
523 }
524}
525
526#[derive(Debug, Deserialize)]
527struct TwitterUserResponse {
528 detail: Option<String>,
529 data: Option<TwitterUserData>,
530}