1use {
2 crate::{
3 api,
4 endpoints::Endpoints,
5 timeline_v2::{
6 parse_threaded_conversation, parse_timeline_tweets_v2, QueryTweetsResponse, ThreadedConversation,
7 },
8 Result, Xplore, XploreError,
9 },
10 chrono::{DateTime, Utc},
11 reqwest::Method,
12 serde::{Deserialize, Serialize},
13 serde_json::{json, Value},
14};
15
16pub const DEFAULT_EXPANSIONS: &[&str] = &[
17 "attachments.poll_ids",
18 "attachments.media_keys",
19 "author_id",
20 "referenced_tweets.id",
21 "in_reply_to_user_id",
22 "edit_history_tweet_ids",
23 "geo.place_id",
24 "entities.mentions.username",
25 "referenced_tweets.id.author_id",
26];
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct Tweet {
30 pub ext_views: Option<i32>,
31 pub created_at: Option<String>,
32 pub bookmark_count: Option<i32>,
33 pub conversation_id: Option<String>,
34 pub hashtags: Vec<String>,
35 pub html: Option<String>,
36 pub id: Option<String>,
37 pub in_reply_to_status: Option<Box<Tweet>>,
38 pub in_reply_to_status_id: Option<String>,
39 pub is_quoted: Option<bool>,
40 pub is_pin: Option<bool>,
41 pub is_reply: Option<bool>,
42 pub is_retweet: Option<bool>,
43 pub is_self_thread: Option<bool>,
44 pub likes: Option<i32>,
45 pub name: Option<String>,
46 pub mentions: Vec<Mention>,
47 pub permanent_url: Option<String>,
48 pub photos: Vec<Photo>,
49 pub place: Option<PlaceRaw>,
50 pub quoted_status: Option<Box<Tweet>>,
51 pub quoted_status_id: Option<String>,
52 pub replies: Option<i32>,
53 pub retweets: Option<i32>,
54 pub retweeted_status: Option<Box<Tweet>>,
55 pub retweeted_status_id: Option<String>,
56 pub text: Option<String>,
57 pub thread: Vec<Tweet>,
58 pub time_parsed: Option<DateTime<Utc>>,
59 pub timestamp: Option<i64>,
60 pub urls: Vec<String>,
61 pub user_id: Option<String>,
62 pub username: Option<String>,
63 pub videos: Vec<Video>,
64 pub views: Option<i32>,
65 pub sensitive_content: Option<bool>,
66 pub poll: Option<PollV2>,
67 pub quote_count: Option<i32>,
68 pub reply_count: Option<i32>,
69 pub retweet_count: Option<i32>,
70 pub screen_name: Option<String>,
71 pub thread_id: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Mention {
76 pub id: String,
77 pub username: Option<String>,
78 pub name: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Photo {
83 pub id: String,
84 pub url: String,
85 pub alt_text: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Video {
90 pub id: String,
91 pub preview: String,
92 pub url: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PlaceRaw {
97 pub id: Option<String>,
98 pub place_type: Option<String>,
99 pub name: Option<String>,
100 pub full_name: Option<String>,
101 pub country_code: Option<String>,
102 pub country: Option<String>,
103 pub bounding_box: Option<BoundingBox>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct BoundingBox {
108 #[serde(rename = "type")]
109 pub type_: Option<String>,
110 pub coordinates: Option<Vec<Vec<Vec<f64>>>>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct PollV2 {
115 pub id: Option<String>,
116 pub end_datetime: Option<String>,
117 pub voting_status: Option<String>,
118 pub options: Vec<PollOption>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct PollOption {
123 pub position: Option<i32>,
124 pub label: String,
125 pub votes: Option<i32>,
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129struct TweetResponse {
130 data: TweetData,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134struct TweetData {
135 create_tweet: TweetCreateResult,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139struct TweetCreateResult {
140 tweet_results: TweetResultWrapper,
141}
142
143#[derive(Debug, Serialize, Deserialize)]
144struct TweetResultWrapper {
145 result: TweetResult,
146}
147
148#[derive(Debug, Serialize, Deserialize)]
149struct TweetResult {
150 core: TweetCore,
151 edit_control: TweetEditControl,
152 is_translatable: bool,
153 legacy: TweetLegacy,
154 rest_id: String,
155 source: String,
156 unmention_data: serde_json::Value,
157 unmention_info: serde_json::Value,
158 views: TweetViews,
159}
160
161#[derive(Debug, Serialize, Deserialize)]
162struct TweetCore {
163 user_results: UserResultWrapper,
164}
165
166#[derive(Debug, Serialize, Deserialize)]
167struct UserResultWrapper {
168 result: UserResult,
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172struct UserResult {
173 __typename: String,
174 affiliates_highlighted_label: serde_json::Value,
175 has_graduated_access: bool,
176 id: String,
177 is_blue_verified: bool,
178 legacy: UserLegacy,
179 profile_image_shape: String,
180 rest_id: String,
181}
182
183#[derive(Debug, Serialize, Deserialize)]
184struct UserLegacy {
185 can_dm: bool,
186 can_media_tag: bool,
187 created_at: String,
188 default_profile: bool,
189 default_profile_image: bool,
190 description: String,
191 entities: UserEntities,
192 fast_followers_count: u64,
193 favourites_count: u64,
194 followers_count: u64,
195 friends_count: u64,
196 has_custom_timelines: bool,
197 is_translator: bool,
198 listed_count: u64,
199 location: String,
200 media_count: u64,
201 name: String,
202 needs_phone_verification: bool,
203 normal_followers_count: u64,
204 pinned_tweet_ids_str: Vec<String>,
205 possibly_sensitive: bool,
206 profile_image_url_https: String,
207 profile_interstitial_type: String,
208 screen_name: String,
209 statuses_count: u64,
210 translator_type: String,
211 verified: bool,
212 want_retweets: bool,
213 withheld_in_countries: Vec<String>,
214}
215
216#[derive(Debug, Serialize, Deserialize)]
217struct UserEntities {
218 description: UserDescriptionEntities,
219}
220
221#[derive(Debug, Serialize, Deserialize)]
222struct UserDescriptionEntities {
223 urls: Vec<serde_json::Value>,
224}
225
226#[derive(Debug, Serialize, Deserialize)]
227struct TweetEditControl {
228 edit_tweet_ids: Vec<String>,
229 editable_until_msecs: String,
230 edits_remaining: String,
231 is_edit_eligible: bool,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
235struct TweetLegacy {
236 bookmark_count: u64,
237 bookmarked: bool,
238 conversation_id_str: String,
239 created_at: String,
240 display_text_range: Vec<u64>,
241 entities: TweetEntities,
242 favorite_count: u64,
243 favorited: bool,
244 full_text: String,
245 id_str: String,
246 is_quote_status: bool,
247 lang: String,
248 quote_count: u64,
249 reply_count: u64,
250 retweet_count: u64,
251 retweeted: bool,
252 user_id_str: String,
253}
254
255#[derive(Debug, Serialize, Deserialize)]
256struct TweetEntities {
257 hashtags: Vec<serde_json::Value>,
258 symbols: Vec<serde_json::Value>,
259 urls: Vec<serde_json::Value>,
260 user_mentions: Vec<serde_json::Value>,
261}
262
263#[derive(Debug, Serialize, Deserialize)]
264struct TweetViews {
265 state: String,
266}
267
268#[derive(Debug, Serialize, Deserialize)]
270pub struct TweetRetweetResponse {
271 pub data: TweetRetweetData,
272}
273
274#[derive(Debug, Serialize, Deserialize)]
275pub struct TweetRetweetData {
276 pub create_retweet: TweetRetweetCreateResult,
277}
278
279#[derive(Debug, Serialize, Deserialize)]
280pub struct TweetRetweetCreateResult {
281 pub retweet_results: TweetRetweetResultWrapper,
282}
283
284#[derive(Debug, Serialize, Deserialize)]
285pub struct TweetRetweetResultWrapper {
286 pub result: TweetRetweetResult,
287}
288
289#[derive(Debug, Serialize, Deserialize)]
290pub struct TweetRetweetResult {
291 pub legacy: TweetRetweetLegacy,
292 pub rest_id: String,
293}
294
295#[derive(Debug, Serialize, Deserialize)]
296pub struct TweetRetweetLegacy {
297 pub full_text: String,
298}
299
300pub async fn post_tweet(
301 xplore: &mut Xplore,
302 text: &str,
303 reply_to: Option<&str>,
304 media_data: Option<Vec<(Vec<u8>, String)>>,
305) -> Result<Value> {
306 create_tweet_request(xplore, text, reply_to, media_data).await
307}
308
309pub async fn read_tweet(xplore: &mut Xplore, tweet_id: &str) -> Result<Tweet> {
310 get_tweet(xplore, tweet_id).await
311}
312
313pub async fn retweet(xplore: &mut Xplore, tweet_id: &str) -> Result<TweetRetweetResponse> {
314 let value = retweet_(xplore, tweet_id).await?;
315 let res = serde_json::from_value(value)?;
316
317 Ok(res)
318}
319
320pub async fn like_tweet(xplore: &mut Xplore, tweet_id: &str) -> Result<Value> {
321 let value = like_tweet_(xplore, tweet_id).await?;
322 Ok(value)
323}
324
325pub async fn get_user_tweets(xplore: &mut Xplore, user_id: &str, limit: usize) -> Result<Vec<Tweet>> {
326 let url = format!("https://api.twitter.com/2/users/{}/tweets", user_id);
327 let body = json!({
328 "max_results": limit,
329 "tweet.fields": "created_at,author_id,conversation_id,public_metrics"
330 });
331
332 let (v, _) = api::send_request::<Vec<Tweet>>(&mut xplore.auth, &url, Method::GET, Some(body)).await?;
333 Ok(v)
334}
335
336pub async fn send_quote_tweet(
337 xplore: &mut Xplore,
338 text: &str,
339 quoted_tweet_id: &str,
340 media_data: Option<Vec<(Vec<u8>, String)>>,
341) -> Result<Value> {
342 create_quote_tweet(xplore, text, quoted_tweet_id, media_data).await
343}
344
345pub async fn fetch_tweets_and_replies(
346 xplore: &mut Xplore,
347 username: &str,
348 max_tweets: i32,
349 cursor: Option<&str>,
350) -> Result<QueryTweetsResponse> {
351 fetch_tweets_and_replies_(xplore, username, max_tweets, cursor).await
352}
353
354pub async fn fetch_tweets_and_replies_by_user_id(
355 xplore: &mut Xplore,
356 user_id: &str,
357 max_tweets: i32,
358 cursor: Option<&str>,
359) -> Result<QueryTweetsResponse> {
360 fetch_tweets_and_replies_by_user_id_(xplore, user_id, max_tweets, cursor).await
361}
362
363pub async fn fetch_list_tweets(
364 xplore: &mut Xplore,
365 list_id: &str,
366 max_tweets: i32,
367 cursor: Option<&str>,
368) -> Result<Value> {
369 fetch_list_tweets_(xplore, list_id, max_tweets, cursor).await
370}
371
372pub async fn create_long_tweet(
373 xplore: &mut Xplore,
374 text: &str,
375 reply_to: Option<&str>,
376 media_ids: Option<Vec<String>>,
377) -> Result<Value> {
378 create_long_tweet_(xplore, text, reply_to, media_ids).await
379}
380
381pub async fn fetch_tweets(xplore: &mut Xplore, user_id: &str, max_tweets: i32, cursor: Option<&str>) -> Result<Value> {
385 let mut variables = json!({
386 "userId": user_id,
387 "count": max_tweets.min(200),
388 "includePromotedContent": false
389 });
390
391 if let Some(cursor_val) = cursor {
392 variables["cursor"] = json!(cursor_val);
393 }
394
395 let url = "https://twitter.com/i/api/graphql/YNXM2DGuE2Sff6a2JD3Ztw/UserTweets";
396 let method = Method::GET;
397 let body = Some(json!({
398 "variables": variables,
399 "features": get_default_features()
400 }));
401 let (value, _) = api::send_request(&mut xplore.auth, url, method, body).await?;
402
403 Ok(value)
404}
405
406pub async fn fetch_tweets_and_replies_(
407 xplore: &mut Xplore,
408 username: &str,
409 max_tweets: i32,
410 cursor: Option<&str>,
411) -> Result<QueryTweetsResponse> {
412 let user_id = xplore.get_user_id(username).await?;
413
414 let endpoint = Endpoints::user_tweets_and_replies(&user_id, max_tweets.min(40), cursor);
415 let url = &endpoint.to_request_url();
416 let (value, _) = api::send_request(&mut xplore.auth, url, Method::GET, None).await?;
417
418 let parsed_response = parse_timeline_tweets_v2(&value);
419 Ok(parsed_response)
420}
421
422pub async fn fetch_tweets_and_replies_by_user_id_(
423 xplore: &mut Xplore,
424 user_id: &str,
425 max_tweets: i32,
426 cursor: Option<&str>,
427) -> Result<QueryTweetsResponse> {
428 let endpoint = Endpoints::user_tweets_and_replies(user_id, max_tweets.min(40), cursor);
429 let url = &endpoint.to_request_url();
430 let method = Method::GET;
431
432 let (value, _headers) = api::send_request(&mut xplore.auth, url, method, None).await?;
433
434 let parsed_response = parse_timeline_tweets_v2(&value);
435 Ok(parsed_response)
436}
437
438pub async fn fetch_list_tweets_(
439 xplore: &mut Xplore,
440 list_id: &str,
441 max_tweets: i32,
442 cursor: Option<&str>,
443) -> Result<Value> {
444 let mut variables = json!({
445 "listId": list_id,
446 "count": max_tweets.min(200)
447 });
448
449 if let Some(cursor_val) = cursor {
450 variables["cursor"] = json!(cursor_val);
451 }
452
453 let url = "https://twitter.com/i/api/graphql/LFKj1wqHNTsEJ4Oq7TzaNA/ListLatestTweetsTimeline";
454 let body = Some(json!({
455 "variables": variables,
456 "features": get_default_features()
457 }));
458
459 let (value, _) = api::send_request(&mut xplore.auth, url, Method::GET, body).await?;
460
461 Ok(value)
462}
463
464pub async fn create_quote_tweet(
465 xplore: &mut Xplore,
466 text: &str,
467 quoted_tweet_id: &str,
468 media_data: Option<Vec<(Vec<u8>, String)>>,
469) -> Result<Value> {
470 let mut variables = json!({
471 "tweet_text": text,
472 "dark_request": false,
473 "attachment_url": format!("https://twitter.com/twitter/status/{}", quoted_tweet_id),
474 "media": {
475 "media_entities": [],
476 "possibly_sensitive": false
477 },
478 "semantic_annotation_ids": []
479 });
480
481 if let Some(media_files) = media_data {
482 let mut media_entities = Vec::new();
483
484 for (file_data, media_type) in media_files {
485 let media_id = upload_media(xplore, file_data, &media_type).await?;
486 media_entities.push(json!({
487 "media_id": media_id,
488 "tagged_users": []
489 }));
490 }
491
492 variables["media"]["media_entities"] = json!(media_entities);
493 }
494
495 let url = "https://twitter.com/i/api/graphql/a1p9RWpkYKBjWv_I3WzS-A/CreateTweet";
496 let body = Some(json!({
497 "variables": variables,
498 "features": create_quote_tweet_features()
499 }));
500 let (v, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
501
502 Ok(v)
503}
504
505pub async fn like_tweet_(xplore: &mut Xplore, tweet_id: &str) -> Result<Value> {
506 let url = "https://twitter.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet";
507 let body = Some(json!({
508 "variables": {
509 "tweet_id": tweet_id
510 }
511 }));
512
513 let (value, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
514 Ok(value)
515}
516
517pub async fn retweet_(xplore: &mut Xplore, tweet_id: &str) -> Result<Value> {
518 let url = "https://twitter.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet";
519 let body = Some(json!({
520 "variables": {
521 "tweet_id": tweet_id,
522 "dark_request": false
523 }
524 }));
525 let (value, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
526 Ok(value)
527}
528
529pub async fn create_long_tweet_(
530 xplore: &mut Xplore,
531 text: &str,
532 reply_to: Option<&str>,
533 media_ids: Option<Vec<String>>,
534) -> Result<Value> {
535 let mut variables = json!({
536 "tweet_text": text,
537 "dark_request": false,
538 "media": {
539 "media_entities": [],
540 "possibly_sensitive": false
541 },
542 "semantic_annotation_ids": []
543 });
544
545 if let Some(reply_id) = reply_to {
546 variables["reply"] = json!({
547 "in_reply_to_tweet_id": reply_id
548 });
549 }
550
551 if let Some(media) = media_ids {
552 variables["media"]["media_entities"] = json!(media
553 .iter()
554 .map(|id| json!({
555 "media_id": id,
556 "tagged_users": []
557 }))
558 .collect::<Vec<_>>());
559 }
560
561 let url = "https://twitter.com/i/api/graphql/YNXM2DGuE2Sff6a2JD3Ztw/CreateNoteTweet";
562 let body = Some(json!({
563 "variables": variables,
564 "features": get_long_tweet_features()
565 }));
566 let (value, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
567
568 Ok(value)
569}
570
571pub async fn fetch_liked_tweets(
572 xplore: &mut Xplore,
573 user_id: &str,
574 max_tweets: i32,
575 cursor: Option<&str>,
576) -> Result<Value> {
577 let mut variables = json!({
578 "userId": user_id,
579 "count": max_tweets.min(200),
580 "includePromotedContent": false
581 });
582
583 if let Some(cursor_val) = cursor {
584 variables["cursor"] = json!(cursor_val);
585 }
586
587 let url = "https://twitter.com/i/api/graphql/YlkSUg4Czo2Zx7yRqpwDow/Likes";
588 let body = Some(json!({
589 "variables": variables,
590 "features": get_default_features()
591 }));
592 let (value, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
593 Ok(value)
594}
595
596pub async fn upload_media(xplore: &mut Xplore, file_data: Vec<u8>, media_type: &str) -> Result<String> {
597 let upload_url = "https://upload.twitter.com/1.1/media/upload.json";
598
599 let is_video = media_type.starts_with("video/");
601
602 if is_video {
603 upload_video_in_chunks(xplore, file_data, media_type).await
605 } else {
606 let form = reqwest::multipart::Form::new().part("media", reqwest::multipart::Part::bytes(file_data));
608
609 let (response, _) = api::request_multipart::<Value>(&mut xplore.auth, upload_url, form).await?;
610
611 response["media_id_string"]
612 .as_str()
613 .map(String::from)
614 .ok_or_else(|| XploreError::Api("Failed to get media_id".into()))
615 }
616}
617
618async fn upload_video_in_chunks(xplore: &mut Xplore, file_data: Vec<u8>, media_type: &str) -> Result<String> {
619 let upload_url = "https://upload.twitter.com/1.1/media/upload.json";
620
621 let body = Some(json!({
622 "command": "INIT",
623 "total_bytes": file_data.len(),
624 "media_type": media_type
625 }));
626 let (init_response, _) = api::send_request::<Value>(&mut xplore.auth, upload_url, Method::POST, body).await?;
627
628 let media_id = init_response["media_id_string"]
629 .as_str()
630 .ok_or_else(|| XploreError::Api("Failed to get media_id".into()))?
631 .to_string();
632
633 let chunk_size = 5 * 1024 * 1024; for (segment_index, chunk) in file_data.chunks(chunk_size).enumerate() {
637 let form = reqwest::multipart::Form::new()
638 .text("command", "APPEND")
639 .text("media_id", media_id.clone())
640 .text("segment_index", segment_index.to_string())
641 .part("media", reqwest::multipart::Part::bytes(chunk.to_vec()));
642
643 let _ = api::request_multipart::<Value>(&mut xplore.auth, upload_url, form).await?;
644 }
645
646 let (finalize_response, _) = api::send_request::<Value>(
648 &mut xplore.auth,
649 &format!("{}?command=FINALIZE&media_id={}", upload_url, media_id),
650 Method::POST,
651 None,
652 )
653 .await?;
654
655 if finalize_response.get("processing_info").is_some() {
657 check_upload_status(xplore, &media_id).await?;
658 }
659
660 Ok(media_id)
661}
662
663async fn check_upload_status(xplore: &mut Xplore, media_id: &str) -> Result<()> {
664 let upload_url = "https://upload.twitter.com/1.1/media/upload.json";
665
666 for _ in 0..20 {
667 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; let url = &format!("{}?command=STATUS&media_id={}", upload_url, media_id);
671 let method = Method::GET;
672 let body = None;
673 let (status_response, _) = api::send_request::<Value>(&mut xplore.auth, url, method, body).await?;
674
675 if let Some(processing_info) = status_response.get("processing_info") {
676 match processing_info["state"].as_str() {
677 Some("succeeded") => return Ok(()),
678 Some("failed") => return Err(XploreError::Api("Video processing failed".into())),
679 _ => continue,
680 }
681 }
682 }
683
684 Err(XploreError::Api("Video processing timeout".into()))
685}
686
687pub async fn get_tweet(xplore: &mut Xplore, id: &str) -> Result<Tweet> {
688 let tweet_detail_request = Endpoints::tweet_detail(id);
689 let url = tweet_detail_request.to_request_url();
690
691 let (response, _) = api::send_request::<Value>(&mut xplore.auth, &url, Method::GET, None).await?;
692 let data = response.clone();
693 let conversation: ThreadedConversation = serde_json::from_value(data)?;
694 let tweets = parse_threaded_conversation(&conversation);
695 tweets.into_iter().next().ok_or_else(|| XploreError::Api("No tweets found".into()))
696}
697
698fn create_tweet_features() -> Value {
699 json!({
700 "interactive_text_enabled": true,
701 "longform_notetweets_inline_media_enabled": false,
702 "responsive_web_text_conversations_enabled": false,
703 "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
704 "vibe_api_enabled": false,
705 "rweb_lists_timeline_redesign_enabled": true,
706 "responsive_web_graphql_exclude_directive_enabled": true,
707 "verified_phone_label_enabled": false,
708 "creator_subscriptions_tweet_preview_api_enabled": true,
709 "responsive_web_graphql_timeline_navigation_enabled": true,
710 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
711 "tweetypie_unmention_optimization_enabled": true,
712 "responsive_web_edit_tweet_api_enabled": true,
713 "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
714 "view_counts_everywhere_api_enabled": true,
715 "longform_notetweets_consumption_enabled": true,
716 "tweet_awards_web_tipping_enabled": false,
717 "freedom_of_speech_not_reach_fetch_enabled": true,
718 "standardized_nudges_misinfo": true,
719 "longform_notetweets_rich_text_read_enabled": true,
720 "responsive_web_enhance_cards_enabled": false,
721 "subscriptions_verification_info_enabled": true,
722 "subscriptions_verification_info_reason_enabled": true,
723 "subscriptions_verification_info_verified_since_enabled": true,
724 "super_follow_badge_privacy_enabled": false,
725 "super_follow_exclusive_tweet_notifications_enabled": false,
726 "super_follow_tweet_api_enabled": false,
727 "super_follow_user_api_enabled": false,
728 "android_graphql_skip_api_media_color_palette": false,
729 "creator_subscriptions_subscription_count_enabled": false,
730 "blue_business_profile_image_shape_enabled": false,
731 "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
732 "rweb_video_timestamps_enabled": false,
733 "c9s_tweet_anatomy_moderator_badge_enabled": false,
734 "responsive_web_twitter_article_tweet_consumption_enabled": false
735 })
736}
737
738fn get_default_features() -> Value {
739 json!({
740 "interactive_text_enabled": true,
741 "longform_notetweets_inline_media_enabled": false,
742 "responsive_web_text_conversations_enabled": false,
743 "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
744 "vibe_api_enabled": false,
745 "rweb_lists_timeline_redesign_enabled": true,
746 "responsive_web_graphql_exclude_directive_enabled": true,
747 "verified_phone_label_enabled": false,
748 "creator_subscriptions_tweet_preview_api_enabled": true,
749 "responsive_web_graphql_timeline_navigation_enabled": true,
750 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
751 "tweetypie_unmention_optimization_enabled": true,
752 "responsive_web_edit_tweet_api_enabled": true,
753 "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
754 "view_counts_everywhere_api_enabled": true,
755 "longform_notetweets_consumption_enabled": true,
756 "tweet_awards_web_tipping_enabled": false,
757 "freedom_of_speech_not_reach_fetch_enabled": true,
758 "standardized_nudges_misinfo": true,
759 "longform_notetweets_rich_text_read_enabled": true,
760 "responsive_web_enhance_cards_enabled": false,
761 "subscriptions_verification_info_enabled": true,
762 "subscriptions_verification_info_reason_enabled": true,
763 "subscriptions_verification_info_verified_since_enabled": true,
764 "super_follow_badge_privacy_enabled": false,
765 "super_follow_exclusive_tweet_notifications_enabled": false,
766 "super_follow_tweet_api_enabled": false,
767 "super_follow_user_api_enabled": false,
768 "android_graphql_skip_api_media_color_palette": false,
769 "creator_subscriptions_subscription_count_enabled": false,
770 "blue_business_profile_image_shape_enabled": false,
771 "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
772 "rweb_video_timestamps_enabled": true,
773 "c9s_tweet_anatomy_moderator_badge_enabled": true,
774 "responsive_web_twitter_article_tweet_consumption_enabled": false,
775 "creator_subscriptions_quote_tweet_preview_enabled": false,
776 "profile_label_improvements_pcf_label_in_post_enabled": false,
777 "rweb_tipjar_consumption_enabled": true,
778 "articles_preview_enabled": true
779 })
780}
781
782fn get_long_tweet_features() -> Value {
784 json!({
785 "premium_content_api_read_enabled": false,
786 "communities_web_enable_tweet_community_results_fetch": true,
787 "c9s_tweet_anatomy_moderator_badge_enabled": true,
788 "responsive_web_grok_analyze_button_fetch_trends_enabled": true,
789 "responsive_web_edit_tweet_api_enabled": true,
790 "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
791 "view_counts_everywhere_api_enabled": true,
792 "longform_notetweets_consumption_enabled": true,
793 "responsive_web_twitter_article_tweet_consumption_enabled": true,
794 "tweet_awards_web_tipping_enabled": false,
795 "longform_notetweets_rich_text_read_enabled": true,
796 "longform_notetweets_inline_media_enabled": true,
797 "responsive_web_graphql_exclude_directive_enabled": true,
798 "verified_phone_label_enabled": false,
799 "freedom_of_speech_not_reach_fetch_enabled": true,
800 "standardized_nudges_misinfo": true,
801 "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
802 "responsive_web_graphql_timeline_navigation_enabled": true,
803 "responsive_web_enhance_cards_enabled": false
804 })
805}
806
807pub async fn create_tweet_request(
808 xplore: &mut Xplore,
809 text: &str,
810 reply_to: Option<&str>,
811 media_data: Option<Vec<(Vec<u8>, String)>>,
812) -> Result<Value> {
813 let mut variables = json!({
815 "tweet_text": text,
816 "dark_request": false,
817 "media": {
818 "media_entities": [],
819 "possibly_sensitive": false
820 },
821 "semantic_annotation_ids": []
822 });
823
824 if let Some(reply_id) = reply_to {
826 variables["reply"] = json!({
827 "in_reply_to_tweet_id": reply_id
828 });
829 }
830
831 if let Some(media_files) = media_data {
833 let mut media_entities = Vec::new();
834
835 for (file_data, media_type) in media_files {
837 let media_id = upload_media(xplore, file_data, &media_type).await?;
838 media_entities.push(json!({
839 "media_id": media_id,
840 "tagged_users": []
841 }));
842 }
843
844 variables["media"]["media_entities"] = json!(media_entities);
845 }
846 let features = create_tweet_features();
847
848 let url = "https://twitter.com/i/api/graphql/a1p9RWpkYKBjWv_I3WzS-A/CreateTweet";
849 let body = Some(json!({
850 "variables": variables,
851 "features": features,
852 "fieldToggles": {}
853 }));
854 let (value, _) = api::send_request(&mut xplore.auth, url, Method::POST, body).await?;
855 Ok(value)
856}
857
858fn create_quote_tweet_features() -> Value {
859 json!({
860 "interactive_text_enabled": true,
861 "longform_notetweets_inline_media_enabled": false,
862 "responsive_web_text_conversations_enabled": false,
863 "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
864 "vibe_api_enabled": false,
865 "rweb_lists_timeline_redesign_enabled": true,
866 "responsive_web_graphql_exclude_directive_enabled": true,
867 "verified_phone_label_enabled": false,
868 "creator_subscriptions_tweet_preview_api_enabled": true,
869 "responsive_web_graphql_timeline_navigation_enabled": true,
870 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
871 "tweetypie_unmention_optimization_enabled": true,
872 "responsive_web_edit_tweet_api_enabled": true,
873 "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
874 "view_counts_everywhere_api_enabled": true,
875 "longform_notetweets_consumption_enabled": true,
876 "tweet_awards_web_tipping_enabled": false,
877 "freedom_of_speech_not_reach_fetch_enabled": true,
878 "standardized_nudges_misinfo": true,
879 "longform_notetweets_rich_text_read_enabled": true,
880 "responsive_web_enhance_cards_enabled": false,
881 "subscriptions_verification_info_enabled": true,
882 "subscriptions_verification_info_reason_enabled": true,
883 "subscriptions_verification_info_verified_since_enabled": true,
884 "super_follow_badge_privacy_enabled": false,
885 "super_follow_exclusive_tweet_notifications_enabled": false,
886 "super_follow_tweet_api_enabled": false,
887 "super_follow_user_api_enabled": false,
888 "android_graphql_skip_api_media_color_palette": false,
889 "creator_subscriptions_subscription_count_enabled": false,
890 "blue_business_profile_image_shape_enabled": false,
891 "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
892 "rweb_video_timestamps_enabled": true,
893 "c9s_tweet_anatomy_moderator_badge_enabled": true,
894 "responsive_web_twitter_article_tweet_consumption_enabled": false
895 })
896}
897
898pub async fn fetch_user_tweets(
899 xplore: &mut Xplore,
900 user_id: &str,
901 max_tweets: i32,
902 cursor: Option<&str>,
903) -> Result<QueryTweetsResponse> {
904 let endpoint = Endpoints::user_tweets(user_id, max_tweets.min(200), cursor);
905 let url = &endpoint.to_request_url();
906
907 let (value, _) = api::send_request(&mut xplore.auth, url, Method::GET, None).await?;
908
909 let parsed_response = parse_timeline_tweets_v2(&value);
910 Ok(parsed_response)
911}