Skip to main content

twapi_v2/
api.rs

1use std::time::Duration;
2
3use reqwest::{RequestBuilder, StatusCode};
4use serde::de::DeserializeOwned;
5use tokio::time::sleep;
6
7use crate::{
8    error::{Error, TwitterError},
9    headers::Headers,
10};
11
12pub mod delete_2_lists_id;
13pub mod delete_2_lists_id_members_user_id;
14pub mod delete_2_tweets_id;
15pub mod delete_2_users_id_bookmarks_tweet_id;
16pub mod delete_2_users_id_followed_lists_list_id;
17pub mod delete_2_users_id_likes_tweet_id;
18pub mod delete_2_users_id_pinned_lists;
19pub mod delete_2_users_id_retweets_source_tweet_id;
20pub mod delete_2_users_source_user_id_blocking_target_user_id;
21pub mod delete_2_users_source_user_id_following_target_user_id;
22pub mod delete_2_users_source_user_id_muting_target_user_id;
23pub mod get_2_compliance_jobs;
24pub mod get_2_compliance_jobs_id;
25pub mod get_2_dm_conversations_dm_conversation_id_dm_events;
26pub mod get_2_dm_conversations_with_participant_id_dm_events;
27pub mod get_2_dm_events;
28pub mod get_2_lists_id;
29pub mod get_2_lists_id_followers;
30pub mod get_2_lists_id_members;
31pub mod get_2_lists_id_tweets;
32pub mod get_2_media_upload;
33pub mod get_2_spaces;
34pub mod get_2_spaces_by_creator_ids;
35pub mod get_2_spaces_id;
36pub mod get_2_spaces_id_buyers;
37pub mod get_2_spaces_id_tweets;
38pub mod get_2_spaces_search;
39pub mod get_2_trends_by_woeid_woeid;
40pub mod get_2_tweets;
41pub mod get_2_tweets_compliance_stream;
42pub mod get_2_tweets_count_all;
43pub mod get_2_tweets_count_recent;
44pub mod get_2_tweets_id;
45pub mod get_2_tweets_id_liking_users;
46pub mod get_2_tweets_id_quote_tweets;
47pub mod get_2_tweets_id_retweeted_by;
48pub mod get_2_tweets_id_retweets;
49pub mod get_2_tweets_sample10_stream;
50pub mod get_2_tweets_sample_stream;
51pub mod get_2_tweets_search_all;
52pub mod get_2_tweets_search_recent;
53pub mod get_2_tweets_search_stream;
54pub mod get_2_tweets_search_stream_rules;
55pub mod get_2_usage_tweets;
56pub mod get_2_users;
57pub mod get_2_users_by;
58pub mod get_2_users_by_username_username;
59pub mod get_2_users_compliance_stream;
60pub mod get_2_users_id;
61pub mod get_2_users_id_blocking;
62pub mod get_2_users_id_bookmarks;
63pub mod get_2_users_id_followed_lists;
64pub mod get_2_users_id_followers;
65pub mod get_2_users_id_following;
66pub mod get_2_users_id_liked_tweets;
67pub mod get_2_users_id_list_memberships;
68pub mod get_2_users_id_mentions;
69pub mod get_2_users_id_muting;
70pub mod get_2_users_id_owned_lists;
71pub mod get_2_users_id_pinned_lists;
72pub mod get_2_users_id_timelines_reverse_chronological;
73pub mod get_2_users_id_tweets;
74pub mod get_2_users_me;
75pub mod get_2_users_reposts_of_me;
76pub mod get_2_users_search;
77pub mod post_2_compliance_jobs;
78pub mod post_2_dm_conversations;
79pub mod post_2_dm_conversations_dm_conversation_id_message;
80pub mod post_2_dm_conversations_with_participant_id_message;
81pub mod post_2_lists;
82pub mod post_2_lists_id_members;
83pub mod post_2_media_metadata_create;
84pub mod post_2_media_subtitles_create;
85pub mod post_2_media_subtitles_delete;
86pub mod post_2_media_upload_id_append;
87pub mod post_2_media_upload_id_finalize;
88pub mod post_2_media_upload_initialize;
89pub mod post_2_oauth2_token_refresh_token;
90pub mod post_2_tweets;
91pub mod post_2_tweets_search_stream_rules;
92pub mod post_2_users_id_blocking;
93pub mod post_2_users_id_bookmarks;
94pub mod post_2_users_id_followed_lists;
95pub mod post_2_users_id_following;
96pub mod post_2_users_id_likes;
97pub mod post_2_users_id_muting;
98pub mod post_2_users_id_pinned_lists;
99pub mod post_2_users_id_retweets;
100pub mod put_2_lists_id;
101pub mod put_2_tweets_id_hidden;
102
103const ENV_KEY: &str = "TWAPI_V2_TWITTER_API_PREFIX_API";
104const PREFIX_URL_TWITTER: &str = "https://api.x.com";
105
106pub fn clear_prefix_url() {
107    // TODO: Audit that the environment access only happens in single-threaded code.
108    unsafe { std::env::set_var(ENV_KEY, PREFIX_URL_TWITTER) };
109}
110
111pub fn setup_prefix_url(url: &str) {
112    // TODO: Audit that the environment access only happens in single-threaded code.
113    unsafe { std::env::set_var(ENV_KEY, url) };
114}
115
116#[derive(Debug, Clone, Default)]
117pub struct TwapiOptions {
118    pub prefix_url: Option<String>,
119    pub timeout: Option<Duration>,
120    pub try_count: Option<u32>,
121    pub retry_interval_duration: Option<Duration>,
122    pub retryable_status_codes: Option<Vec<u16>>,
123}
124
125pub(crate) fn make_url(twapi_options: &Option<TwapiOptions>, post_url: &str) -> String {
126    make_url_with_prefix(
127        &std::env::var(ENV_KEY).unwrap_or(PREFIX_URL_TWITTER.to_owned()),
128        twapi_options,
129        post_url,
130    )
131}
132
133pub(crate) fn make_url_with_prefix(
134    default_perfix_url: &str,
135    twapi_options: &Option<TwapiOptions>,
136    post_url: &str,
137) -> String {
138    let prefix_url = if let Some(twapi_options) = twapi_options {
139        if let Some(prefix_url) = twapi_options.prefix_url.as_ref() {
140            prefix_url
141        } else {
142            default_perfix_url
143        }
144    } else {
145        default_perfix_url
146    };
147    format!("{}{}", prefix_url, post_url)
148}
149
150pub trait Authentication {
151    fn execute(
152        &self,
153        builder: RequestBuilder,
154        method: &str,
155        uri: &str,
156        options: &[(&str, &str)],
157    ) -> RequestBuilder;
158}
159
160pub struct BearerAuthentication {
161    bearer_code: String,
162}
163
164impl BearerAuthentication {
165    pub fn new<T: Into<String>>(bearer_code: T) -> Self {
166        Self {
167            bearer_code: bearer_code.into(),
168        }
169    }
170}
171
172impl Authentication for BearerAuthentication {
173    fn execute(
174        &self,
175        builder: RequestBuilder,
176        _method: &str,
177        _uri: &str,
178        _options: &[(&str, &str)],
179    ) -> RequestBuilder {
180        builder.bearer_auth(&self.bearer_code)
181    }
182}
183
184pub async fn execute_twitter<T>(
185    f: impl Fn() -> RequestBuilder,
186    twapi_options: &Option<TwapiOptions>,
187) -> Result<(T, Headers), Error>
188where
189    T: DeserializeOwned,
190{
191    let mut count = 0;
192    #[allow(unused_assignments)]
193    let mut last_error: Option<Error> = None;
194    let default_retryable_status_codes: Vec<u16> = vec![StatusCode::INTERNAL_SERVER_ERROR.as_u16()];
195    let retryable_status_codes = twapi_options
196        .as_ref()
197        .and_then(|options| options.retryable_status_codes.as_ref())
198        .unwrap_or(default_retryable_status_codes.as_ref());
199    let retryable_interval_duration = twapi_options
200        .as_ref()
201        .and_then(|options| options.retry_interval_duration)
202        .unwrap_or(Duration::from_millis(100));
203
204    loop {
205        let mut builder = f();
206        if let Some(timeout) = twapi_options.as_ref().and_then(|options| options.timeout) {
207            builder = builder.timeout(timeout);
208        }
209
210        let response = builder.send().await?;
211        let status_code = response.status();
212        let header = response.headers();
213        let headers = Headers::new(header);
214
215        if status_code.is_success() {
216            return Ok((response.json::<T>().await?, headers));
217        } else {
218            let text = response.text().await?;
219            last_error = Some(match serde_json::from_str(&text) {
220                Ok(value) => Error::Twitter(
221                    TwitterError::new(&value, status_code),
222                    value,
223                    Box::new(headers),
224                ),
225                Err(err) => Error::Other(format!("{:?}:{}", err, text), Some(status_code)),
226            });
227        }
228
229        if count + 1
230            >= twapi_options
231                .as_ref()
232                .and_then(|options| options.try_count)
233                .unwrap_or(0)
234        {
235            break;
236        }
237
238        if !retryable_status_codes.contains(&status_code.as_u16()) {
239            break;
240        }
241
242        sleep(retryable_interval_duration * 2_u32.pow(count)).await;
243        count += 1;
244    }
245
246    Err(last_error.unwrap_or(Error::Other(
247        "Retry Over last_error is None".to_string(),
248        None,
249    )))
250}