agent_twitter_client/timeline/
search.rs1use crate::profile::parse_profile;
2use crate::timeline::v1::{QueryProfilesResponse, QueryTweetsResponse};
3use crate::timeline::v2::{parse_legacy_tweet, SearchEntryRaw};
4use lazy_static::lazy_static;
5use serde::Deserialize;
6
7lazy_static! {
8 static ref EMPTY_INSTRUCTIONS: Vec<SearchInstruction> = Vec::new();
9 static ref EMPTY_ENTRIES: Vec<SearchEntryRaw> = Vec::new();
10}
11
12#[derive(Debug, Deserialize)]
13pub struct SearchTimeline {
14 pub data: Option<SearchData>,
15}
16
17#[derive(Debug, Deserialize)]
18pub struct SearchData {
19 pub search_by_raw_query: Option<SearchByRawQuery>,
20}
21
22#[derive(Debug, Deserialize)]
23pub struct SearchByRawQuery {
24 pub search_timeline: Option<SearchTimelineData>,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct SearchTimelineData {
29 pub timeline: Option<TimelineData>,
30}
31
32#[derive(Debug, Deserialize)]
33pub struct TimelineData {
34 pub instructions: Option<Vec<SearchInstruction>>,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct SearchInstruction {
39 pub entries: Option<Vec<SearchEntryRaw>>,
40 pub entry: Option<SearchEntryRaw>,
41 #[serde(rename = "type")]
42 pub instruction_type: Option<String>,
43}
44
45pub fn parse_search_timeline_tweets(timeline: &SearchTimeline) -> QueryTweetsResponse {
46 let mut bottom_cursor = None;
47 let mut top_cursor = None;
48 let mut tweets = Vec::new();
49
50 let instructions = timeline
51 .data
52 .as_ref()
53 .and_then(|data| data.search_by_raw_query.as_ref())
54 .and_then(|search| search.search_timeline.as_ref())
55 .and_then(|timeline| timeline.timeline.as_ref())
56 .and_then(|timeline| timeline.instructions.as_ref())
57 .unwrap_or(&EMPTY_INSTRUCTIONS);
58
59 for instruction in instructions {
60 if let Some(instruction_type) = &instruction.instruction_type {
61 if instruction_type == "TimelineAddEntries"
62 || instruction_type == "TimelineReplaceEntry"
63 {
64 if let Some(entry) = &instruction.entry {
65 if let Some(content) = &entry.content {
66 match content.cursor_type.as_deref() {
67 Some("Bottom") => {
68 bottom_cursor = content.value.clone();
69 continue;
70 }
71 Some("Top") => {
72 top_cursor = content.value.clone();
73 continue;
74 }
75 _ => {}
76 }
77 }
78 }
79
80 let entries = instruction.entries.as_ref().unwrap_or(&EMPTY_ENTRIES);
82 for entry in entries {
83 if let Some(content) = &entry.content {
84 if let Some(item_content) = &content.item_content {
85 if item_content.tweet_display_type.as_deref() == Some("Tweet") {
86 if let Some(tweet_results) = &item_content.tweet_results {
87 if let Some(result) = &tweet_results.result {
88 let user_legacy = result
89 .core
90 .as_ref()
91 .and_then(|core| core.user_results.as_ref())
92 .and_then(|user_results| user_results.result.as_ref())
93 .and_then(|result| result.legacy.as_ref());
94
95 if let Ok(tweet_result) = parse_legacy_tweet(
96 user_legacy,
97 result.legacy.as_deref(),
98 )
99 {
100 if tweet_result.views.is_none() {
101 if let Some(views) = &result.views {
102 if let Some(count) = &views.count {
103 if let Ok(view_count) = count.parse::<i32>()
104 {
105 let mut tweet = tweet_result;
106 tweet.views = Some(view_count);
107 tweets.push(tweet);
108 }
109 }
110 }
111 } else {
112 tweets.push(tweet_result);
113 }
114 }
115 }
116 }
117 }
118 } else if let Some(cursor_type) = &content.cursor_type {
119 match cursor_type.as_str() {
120 "Bottom" => bottom_cursor = content.value.clone(),
121 "Top" => top_cursor = content.value.clone(),
122 _ => {}
123 }
124 }
125 }
126 }
127 }
128 }
129 }
130
131 QueryTweetsResponse {
132 tweets,
133 next: bottom_cursor,
134 previous: top_cursor,
135 }
136}
137
138pub fn parse_search_timeline_users(timeline: &SearchTimeline) -> QueryProfilesResponse {
139 let mut bottom_cursor = None;
140 let mut top_cursor = None;
141 let mut profiles = Vec::new();
142
143 let instructions = timeline
144 .data
145 .as_ref()
146 .and_then(|data| data.search_by_raw_query.as_ref())
147 .and_then(|search| search.search_timeline.as_ref())
148 .and_then(|timeline| timeline.timeline.as_ref())
149 .and_then(|timeline| timeline.instructions.as_ref())
150 .unwrap_or(&EMPTY_INSTRUCTIONS);
151
152 for instruction in instructions {
153 if let Some(instruction_type) = &instruction.instruction_type {
154 if instruction_type == "TimelineAddEntries"
155 || instruction_type == "TimelineReplaceEntry"
156 {
157 if let Some(entry) = &instruction.entry {
158 if let Some(content) = &entry.content {
159 match content.cursor_type.as_deref() {
160 Some("Bottom") => {
161 bottom_cursor = content.value.clone();
162 continue;
163 }
164 Some("Top") => {
165 top_cursor = content.value.clone();
166 continue;
167 }
168 _ => {}
169 }
170 }
171 }
172
173 let entries = instruction.entries.as_ref().unwrap_or(&EMPTY_ENTRIES);
175 for entry in entries {
176 if let Some(content) = &entry.content {
177 if let Some(item_content) = &content.item_content {
178 if item_content.user_display_type.as_deref() == Some("User") {
179 if let Some(user_results) = &item_content.user_results {
180 if let Some(result) = &user_results.result {
181 if let Some(legacy) = &result.legacy {
182 let mut profile =
183 parse_profile(legacy, result.is_blue_verified);
184
185 if profile.id.is_empty() {
186 profile.id =
187 result.rest_id.clone().unwrap_or_default();
188 }
189
190 profiles.push(profile);
191 }
192 }
193 }
194 }
195 } else if let Some(cursor_type) = &content.cursor_type {
196 match cursor_type.as_str() {
197 "Bottom" => bottom_cursor = content.value.clone(),
198 "Top" => top_cursor = content.value.clone(),
199 _ => {}
200 }
201 }
202 }
203 }
204 }
205 }
206 }
207
208 QueryProfilesResponse {
209 profiles,
210 next: bottom_cursor,
211 previous: top_cursor,
212 }
213}