critter/
lib.rs

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 add_media(&mut self, media: &str) -> &mut Self {
101        self.0.insert(
102            "media",
103            json!({"media_ids": [media]}),
104        );
105        self
106    }*/
107
108    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            // simple upload
308            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 media upload
321            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                                //println!("in progress");
402                                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}