1use crate::BEARER_TOKEN;
2use log::debug;
3use serde_json::json;
4use std::cmp;
5use eyre::{ContextCompat, Result};
6use super::{
7 types::{parse_legacy_tweet, Data, Tweet},
8 TwAPI,
9};
10
11const SEARCH_URL: &str = "https://twitter.com/i/api/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline";
12
13impl TwAPI {
14 pub async fn search(
15 &self,
16 query: &str,
17 limit: u8,
18 cursor: &str,
19 ) -> Result<Data> {
20 let limit = cmp::min(50u8, limit);
21
22 let mut variables = json!(
23 {
24 "rawQuery": query.to_string(),
25 "count": limit,
26 "querySource": "typed_query",
27 "product": "Top"
28 }
29 );
30 let features = json!(
31 {
32 "rweb_lists_timeline_redesign_enabled": true,
33 "responsive_web_graphql_exclude_directive_enabled": true,
34 "verified_phone_label_enabled": false,
35 "creator_subscriptions_tweet_preview_api_enabled": true,
36 "responsive_web_graphql_timeline_navigation_enabled": true,
37 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
38 "tweetypie_unmention_optimization_enabled": true,
39 "responsive_web_edit_tweet_api_enabled": true,
40 "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
41 "view_counts_everywhere_api_enabled": true,
42 "longform_notetweets_consumption_enabled": true,
43 "responsive_web_twitter_article_tweet_consumption_enabled": false,
44 "tweet_awards_web_tipping_enabled": false,
45 "freedom_of_speech_not_reach_fetch_enabled": true,
46 "standardized_nudges_misinfo": true,
47 "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
48 "longform_notetweets_rich_text_read_enabled": true,
49 "longform_notetweets_inline_media_enabled": true,
50 "responsive_web_media_download_video_enabled": false,
51 "responsive_web_enhance_cards_enabled": false,
52 }
53 );
54 let field_toggles = json!(
55 {
56 "withArticleRichContentState": false
57 }
58 );
59 if cursor.ne("") {
60 variables["cursor"] = cursor.to_string().into();
61 }
62 variables["product"] = "Latest".into();
63 let q = [
64 ("variables", variables.to_string()),
65 ("features", features.to_string()),
66 ("fieldToggles", field_toggles.to_string()),
67 ];
68 let req = self
69 .client
70 .get(SEARCH_URL)
71 .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
72 .header("X-CSRF-Token", self.csrf_token.to_owned())
73 .query(&q)
74 .build()?;
75 let text = self
76 .client
77 .execute(req).await?
78 .text().await?;
79 let res: Data = serde_json::from_str(&text)?;
80 return Ok(res);
81 }
82
83 pub async fn search_tweets(
84 &self,
85 query: &str,
86 limit: u8,
87 cursor: &str,
88 ) -> Result<(Vec<Tweet>, String)> {
89 let search_result = self.search(query, limit, cursor).await;
90 let mut cursor = String::from("");
91 match search_result {
92 Ok(res) => {
93 let mut tweets: Vec<Tweet> = vec![];
94 let instructions = res
95 .data
96 .search_by_raw_query
97 .search_timeline
98 .timeline
99 .instructions
100 .context("can't parse tweets from timeline, instructions is none")?;
101 for item in instructions {
102 if item.instruction_type.ne("TimelineAddEntries")
103 && item.instruction_type.ne("TimelineReplaceEntry")
104 {
105 continue;
106 }
107 if item.entry.is_some() {
108 let entry = item.entry.context("entry is none")?;
109 let cursor_type = entry.content.cursor_type.unwrap_or("".to_string());
110 if cursor_type.eq("Bottom") {
111 if entry.content.value.is_some() {
112 cursor = entry.content.value.context("content value is none")?;
113 }
114 }
115 }
116 for entry in item.entries {
117 if entry.content.item_content.is_none() {
118 continue;
119 }
120 let item = entry.content.item_content.context("item content is none")?;
121 if item.tweet_display_type.eq("Tweet") {
122 let core = item.tweet_results.result.core;
123 if core.is_none() {
124 continue;
125 }
126 let u = core.context("core is none")?.user_results.result.legacy.context("legacy is none")?;
127 let t = item.tweet_results.result.legacy;
128 if let Ok(tweet) = parse_legacy_tweet(&u, &t) {
129 tweets.push(tweet)
130 }
131 }
132 }
133 }
134 debug!("Found tweets: {tweets:?}");
135 Ok((tweets, cursor))
136 }
137 Err(e) => Err(e),
138 }
139 }
140}