agent_twitter_client/api/
endpoints.rs

1use std::collections::HashMap;
2use urlencoding;
3
4// Constants for default options matching TypeScript
5pub const DEFAULT_EXPANSIONS: &[&str] = &[
6    "attachments.poll_ids",
7    "attachments.media_keys",
8    "author_id",
9    "referenced_tweets.id",
10    "in_reply_to_user_id",
11    "edit_history_tweet_ids",
12    "geo.place_id",
13    "entities.mentions.username",
14    "referenced_tweets.id.author_id",
15];
16
17pub const DEFAULT_TWEET_FIELDS: &[&str] = &[
18    "attachments",
19    "author_id",
20    "context_annotations",
21    "conversation_id",
22    "created_at",
23    "entities",
24    "geo",
25    "id",
26    "in_reply_to_user_id",
27    "lang",
28    "public_metrics",
29    "edit_controls",
30    "possibly_sensitive",
31    "referenced_tweets",
32    "reply_settings",
33    "source",
34    "text",
35    "withheld",
36    "note_tweet",
37];
38
39#[derive(Debug, Clone)]
40pub struct ApiEndpoint {
41    pub url: String,
42    pub variables: Option<HashMap<String, serde_json::Value>>,
43    pub features: Option<HashMap<String, bool>>,
44    pub field_toggles: Option<HashMap<String, bool>>,
45}
46
47impl ApiEndpoint {
48    pub fn to_request_url(&self) -> String {
49        let mut params = Vec::new();
50
51        if let Some(variables) = &self.variables {
52            params.push(format!(
53                "variables={}",
54                urlencoding::encode(&serde_json::to_string(&variables).unwrap())
55            ));
56        }
57
58        if let Some(features) = &self.features {
59            params.push(format!(
60                "features={}",
61                urlencoding::encode(&serde_json::to_string(&features).unwrap())
62            ));
63        }
64
65        if let Some(toggles) = &self.field_toggles {
66            params.push(format!(
67                "fieldToggles={}",
68                urlencoding::encode(&serde_json::to_string(&toggles).unwrap())
69            ));
70        }
71
72        if params.is_empty() {
73            self.url.clone()
74        } else {
75            format!("{}?{}", self.url, params.join("&"))
76        }
77    }
78}
79
80pub struct Endpoints;
81
82impl Endpoints {
83    pub fn tweet_detail(tweet_id: &str) -> ApiEndpoint {
84        ApiEndpoint {
85            url: "https://twitter.com/i/api/graphql/xOhkmRac04YFZmOzU9PJHg/TweetDetail".to_string(),
86            variables: Some(HashMap::from([
87                ("focalTweetId".to_string(), tweet_id.into()),
88                ("with_rux_injections".to_string(), false.into()),
89                ("includePromotedContent".to_string(), true.into()),
90                ("withCommunity".to_string(), true.into()),
91                (
92                    "withQuickPromoteEligibilityTweetFields".to_string(),
93                    true.into(),
94                ),
95                ("withBirdwatchNotes".to_string(), true.into()),
96                ("withVoice".to_string(), true.into()),
97                ("withV2Timeline".to_string(), true.into()),
98            ])),
99            features: Some(HashMap::from([
100                (
101                    "responsive_web_graphql_exclude_directive_enabled".to_string(),
102                    true,
103                ),
104                ("verified_phone_label_enabled".to_string(), false),
105                (
106                    "creator_subscriptions_tweet_preview_api_enabled".to_string(),
107                    true,
108                ),
109                (
110                    "responsive_web_graphql_timeline_navigation_enabled".to_string(),
111                    true,
112                ),
113                (
114                    "responsive_web_graphql_skip_user_profile_image_extensions_enabled".to_string(),
115                    false,
116                ),
117                ("tweetypie_unmention_optimization_enabled".to_string(), true),
118                ("responsive_web_edit_tweet_api_enabled".to_string(), true),
119                (
120                    "graphql_is_translatable_rweb_tweet_is_translatable_enabled".to_string(),
121                    true,
122                ),
123                ("view_counts_everywhere_api_enabled".to_string(), true),
124                ("longform_notetweets_consumption_enabled".to_string(), true),
125                ("tweet_awards_web_tipping_enabled".to_string(), false),
126                (
127                    "freedom_of_speech_not_reach_fetch_enabled".to_string(),
128                    true,
129                ),
130                ("standardized_nudges_misinfo".to_string(), true),
131                (
132                    "responsive_web_twitter_article_tweet_consumption_enabled".to_string(),
133                    false,
134                ),
135                (
136                    "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled"
137                        .to_string(),
138                    true,
139                ),
140                (
141                    "longform_notetweets_rich_text_read_enabled".to_string(),
142                    true,
143                ),
144                ("longform_notetweets_inline_media_enabled".to_string(), true),
145                (
146                    "responsive_web_media_download_video_enabled".to_string(),
147                    false,
148                ),
149                ("responsive_web_enhance_cards_enabled".to_string(), false),
150            ])),
151            field_toggles: Some(HashMap::from([(
152                "withArticleRichContentState".to_string(),
153                false,
154            )])),
155        }
156    }
157
158    pub fn tweet_by_rest_id(tweet_id: &str) -> ApiEndpoint {
159        ApiEndpoint {
160            url: "https://twitter.com/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"
161                .to_string(),
162            variables: Some(HashMap::from([
163                ("tweetId".to_string(), tweet_id.into()),
164                ("withCommunity".to_string(), false.into()),
165                ("includePromotedContent".to_string(), false.into()),
166                ("withVoice".to_string(), false.into()),
167            ])),
168            features: Some(HashMap::from([
169                (
170                    "creator_subscriptions_tweet_preview_api_enabled".to_string(),
171                    true,
172                ),
173                ("tweetypie_unmention_optimization_enabled".to_string(), true),
174                ("responsive_web_edit_tweet_api_enabled".to_string(), true),
175                (
176                    "graphql_is_translatable_rweb_tweet_is_translatable_enabled".to_string(),
177                    true,
178                ),
179                ("view_counts_everywhere_api_enabled".to_string(), true),
180                ("longform_notetweets_consumption_enabled".to_string(), true),
181                (
182                    "responsive_web_twitter_article_tweet_consumption_enabled".to_string(),
183                    false,
184                ),
185                ("tweet_awards_web_tipping_enabled".to_string(), false),
186                (
187                    "freedom_of_speech_not_reach_fetch_enabled".to_string(),
188                    true,
189                ),
190                ("standardized_nudges_misinfo".to_string(), true),
191            ])),
192            field_toggles: None,
193        }
194    }
195
196    pub fn user_tweets(user_id: &str, count: i32, cursor: Option<&str>) -> ApiEndpoint {
197        let mut variables = HashMap::from([
198            ("userId".to_string(), user_id.into()),
199            ("count".to_string(), count.into()),
200            ("includePromotedContent".to_string(), true.into()),
201            (
202                "withQuickPromoteEligibilityTweetFields".to_string(),
203                true.into(),
204            ),
205            ("withVoice".to_string(), true.into()),
206            ("withV2Timeline".to_string(), true.into()),
207        ]);
208
209        if let Some(cursor_value) = cursor {
210            variables.insert("cursor".to_string(), cursor_value.into());
211        }
212
213        ApiEndpoint {
214            url: "https://twitter.com/i/api/graphql/V7H0Ap3_Hh2FyS75OCDO3Q/UserTweets".to_string(),
215            variables: Some(variables),
216            features: Some(HashMap::from([
217                ("rweb_tipjar_consumption_enabled".to_string(), true),
218                (
219                    "responsive_web_graphql_exclude_directive_enabled".to_string(),
220                    true,
221                ),
222                ("verified_phone_label_enabled".to_string(), false),
223                (
224                    "creator_subscriptions_tweet_preview_api_enabled".to_string(),
225                    true,
226                ),
227                (
228                    "responsive_web_graphql_timeline_navigation_enabled".to_string(),
229                    true,
230                ),
231                (
232                    "responsive_web_graphql_skip_user_profile_image_extensions_enabled".to_string(),
233                    false,
234                ),
235                (
236                    "communities_web_enable_tweet_community_results_fetch".to_string(),
237                    true,
238                ),
239                (
240                    "c9s_tweet_anatomy_moderator_badge_enabled".to_string(),
241                    true,
242                ),
243                ("articles_preview_enabled".to_string(), true),
244                ("tweetypie_unmention_optimization_enabled".to_string(), true),
245                ("responsive_web_edit_tweet_api_enabled".to_string(), true),
246                (
247                    "graphql_is_translatable_rweb_tweet_is_translatable_enabled".to_string(),
248                    true,
249                ),
250                ("view_counts_everywhere_api_enabled".to_string(), true),
251                ("longform_notetweets_consumption_enabled".to_string(), true),
252                (
253                    "responsive_web_twitter_article_tweet_consumption_enabled".to_string(),
254                    true,
255                ),
256                ("tweet_awards_web_tipping_enabled".to_string(), false),
257                (
258                    "creator_subscriptions_quote_tweet_preview_enabled".to_string(),
259                    false,
260                ),
261                (
262                    "freedom_of_speech_not_reach_fetch_enabled".to_string(),
263                    true,
264                ),
265                ("standardized_nudges_misinfo".to_string(), true),
266                (
267                    "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled"
268                        .to_string(),
269                    true,
270                ),
271                ("rweb_video_timestamps_enabled".to_string(), true),
272                (
273                    "longform_notetweets_rich_text_read_enabled".to_string(),
274                    true,
275                ),
276                ("longform_notetweets_inline_media_enabled".to_string(), true),
277                ("responsive_web_enhance_cards_enabled".to_string(), false),
278            ])),
279            field_toggles: Some(HashMap::from([("withArticlePlainText".to_string(), false)])),
280        }
281    }
282
283    pub fn user_tweets_and_replies(user_id: &str, count: i32, cursor: Option<&str>) -> ApiEndpoint {
284        let mut variables = HashMap::from([
285            ("userId".to_string(), user_id.into()),
286            ("count".to_string(), count.into()),
287            ("includePromotedContent".to_string(), true.into()),
288            ("withCommunity".to_string(), true.into()),
289            ("withVoice".to_string(), true.into()),
290            ("withV2Timeline".to_string(), true.into()),
291        ]);
292
293        if let Some(cursor_value) = cursor {
294            variables.insert("cursor".to_string(), cursor_value.into());
295        }
296
297        ApiEndpoint {
298            url: "https://twitter.com/i/api/graphql/E4wA5vo2sjVyvpliUffSCw/UserTweetsAndReplies"
299                .to_string(),
300            variables: Some(variables),
301            features: Some(HashMap::from([
302                ("rweb_tipjar_consumption_enabled".to_string(), true),
303                (
304                    "responsive_web_graphql_exclude_directive_enabled".to_string(),
305                    true,
306                ),
307                ("verified_phone_label_enabled".to_string(), false),
308                (
309                    "creator_subscriptions_tweet_preview_api_enabled".to_string(),
310                    true,
311                ),
312                (
313                    "responsive_web_graphql_timeline_navigation_enabled".to_string(),
314                    true,
315                ),
316                (
317                    "responsive_web_graphql_skip_user_profile_image_extensions_enabled".to_string(),
318                    false,
319                ),
320                (
321                    "communities_web_enable_tweet_community_results_fetch".to_string(),
322                    true,
323                ),
324                (
325                    "c9s_tweet_anatomy_moderator_badge_enabled".to_string(),
326                    true,
327                ),
328                ("articles_preview_enabled".to_string(), true),
329                ("tweetypie_unmention_optimization_enabled".to_string(), true),
330                ("responsive_web_edit_tweet_api_enabled".to_string(), true),
331                (
332                    "graphql_is_translatable_rweb_tweet_is_translatable_enabled".to_string(),
333                    true,
334                ),
335                ("view_counts_everywhere_api_enabled".to_string(), true),
336                ("longform_notetweets_consumption_enabled".to_string(), true),
337                (
338                    "responsive_web_twitter_article_tweet_consumption_enabled".to_string(),
339                    true,
340                ),
341                ("tweet_awards_web_tipping_enabled".to_string(), false),
342                (
343                    "creator_subscriptions_quote_tweet_preview_enabled".to_string(),
344                    false,
345                ),
346                (
347                    "freedom_of_speech_not_reach_fetch_enabled".to_string(),
348                    true,
349                ),
350                ("standardized_nudges_misinfo".to_string(), true),
351                (
352                    "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled"
353                        .to_string(),
354                    true,
355                ),
356                ("rweb_video_timestamps_enabled".to_string(), true),
357                (
358                    "longform_notetweets_rich_text_read_enabled".to_string(),
359                    true,
360                ),
361                ("longform_notetweets_inline_media_enabled".to_string(), true),
362                ("responsive_web_enhance_cards_enabled".to_string(), false),
363            ])),
364            field_toggles: Some(HashMap::from([("withArticlePlainText".to_string(), false)])),
365        }
366    }
367}