agent_twitter_client/timeline/
search.rs

1use 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                // Process entries
81                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                // Process entries
174                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}