1use std::collections::HashMap;
2use urlencoding;
3
4pub 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}