instagram_web_api/endpoints/
topsearch.rs1use http_api_client_endpoint::{
2 http::{
3 header::{ACCEPT, COOKIE, USER_AGENT},
4 Error as HttpError, Method, StatusCode,
5 },
6 Body, Endpoint, Request, Response, MIME_APPLICATION_JSON,
7};
8use rand::{thread_rng, Rng as _};
9use serde::{Deserialize, Serialize};
10use serde_json::Error as SerdeJsonError;
11use url::{ParseError as UrlParseError, Url};
12
13use crate::{
14 endpoints::{URL_BASE, USER_AGENT_HEADER_VALUE},
15 objects::{TopsearchHashtag, TopsearchLocation, TopsearchUser},
16};
17
18#[derive(Debug, Clone)]
20pub struct Topsearch<'a> {
21 pub cookie_header_value: &'a str,
22 pub query: &'a str,
23 pub context: TopsearchContext,
24 pub rank_token: Option<&'a str>,
25 pub user_agent_header_value: Option<&'a str>,
27}
28impl<'a> Topsearch<'a> {
29 pub fn new(
30 cookie_header_value: &'a str,
31 query: &'a str,
32 context: impl Into<Option<TopsearchContext>>,
33 rank_token: impl Into<Option<&'a str>>,
34 ) -> Self {
35 Self {
36 cookie_header_value,
37 query,
38 context: context.into().unwrap_or_default(),
39 rank_token: rank_token.into(),
40 user_agent_header_value: None,
42 }
43 }
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
47pub enum TopsearchContext {
48 Blended,
49 Hashtag,
50 User,
51}
52impl core::fmt::Display for TopsearchContext {
53 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
54 match self {
55 Self::Blended => write!(f, "blended"),
56 Self::Hashtag => write!(f, "hashtag"),
57 Self::User => write!(f, "user"),
58 }
59 }
60}
61impl Default for TopsearchContext {
62 fn default() -> Self {
63 Self::Blended
64 }
65}
66
67impl<'a> Endpoint for Topsearch<'a> {
69 type RenderRequestError = TopsearchError;
70
71 type ParseResponseOutput = TopsearchResponseBody;
72 type ParseResponseError = TopsearchError;
73
74 fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
75 let url = format!("{}/web/search/topsearch/", URL_BASE);
76 let mut url = Url::parse(url.as_str()).map_err(TopsearchError::MakeRequestUrlFailed)?;
77
78 url.query_pairs_mut()
79 .append_pair("context", &self.context.to_string());
80
81 url.query_pairs_mut().append_pair("query", self.query);
82
83 if let Some(rank_token) = &self.rank_token {
84 url.query_pairs_mut().append_pair("rank_token", rank_token);
85 } else {
86 url.query_pairs_mut()
87 .append_pair("rank_token", gen_rank_token().as_str());
88 }
89
90 url.query_pairs_mut()
91 .append_pair("include_reel", true.to_string().as_str());
92
93 let request = Request::builder()
94 .method(Method::GET)
95 .uri(url.as_str())
96 .header(COOKIE, self.cookie_header_value)
97 .header(
98 USER_AGENT,
99 self.user_agent_header_value
100 .unwrap_or(USER_AGENT_HEADER_VALUE),
101 )
102 .header(ACCEPT, MIME_APPLICATION_JSON)
103 .body(vec![])
104 .map_err(TopsearchError::MakeRequestFailed)?;
105
106 Ok(request)
107 }
108
109 fn parse_response(
110 &self,
111 response: Response<Body>,
112 ) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
113 match response.status() {
114 StatusCode::OK => serde_json::from_slice(response.body())
115 .map_err(TopsearchError::DeResponseBodyFailed),
116 status_code => Err(TopsearchError::StatusCodeMismatch(status_code)),
117 }
118 }
119}
120
121pub fn gen_rank_token() -> String {
122 let mut rng = thread_rng();
123 format!(
124 "0.{}",
125 (0..15)
126 .map(|_| rng.gen_range(0..10).to_string())
127 .collect::<String>()
128 )
129}
130
131#[derive(Deserialize, Serialize, Debug, Clone)]
135pub struct TopsearchResponseBody {
136 pub users: Vec<TopsearchResponseBodyUser>,
137 pub places: Vec<TopsearchResponseBodyPlace>,
138 pub hashtags: Vec<TopsearchResponseBodyHashtag>,
139 pub has_more: bool,
140 pub rank_token: String,
141}
142
143#[derive(Deserialize, Serialize, Debug, Clone)]
145pub struct TopsearchResponseBodyUser {
146 pub position: usize,
147 pub user: TopsearchUser,
148}
149
150#[derive(Deserialize, Serialize, Debug, Clone)]
152pub struct TopsearchResponseBodyPlace {
153 pub position: usize,
154 pub place: TopsearchResponseBodyPlaceItem,
155}
156
157#[derive(Deserialize, Serialize, Debug, Clone)]
158pub struct TopsearchResponseBodyPlaceItem {
159 pub location: TopsearchLocation,
160 pub title: String,
161 }
163
164#[derive(Deserialize, Serialize, Debug, Clone)]
166pub struct TopsearchResponseBodyHashtag {
167 pub position: usize,
168 pub hashtag: TopsearchHashtag,
169}
170
171#[derive(thiserror::Error, Debug)]
175pub enum TopsearchError {
176 #[error("MakeRequestUrlFailed {0}")]
177 MakeRequestUrlFailed(UrlParseError),
178 #[error("MakeRequestFailed {0}")]
179 MakeRequestFailed(HttpError),
180 #[error("StatusCodeMismatch {0}")]
181 StatusCodeMismatch(StatusCode),
182 #[error("DeResponseBodyFailed {0}")]
183 DeResponseBodyFailed(SerdeJsonError),
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn de_response_body() {
192 match serde_json::from_str::<TopsearchResponseBody>(include_str!(
193 "../../tests/response_body_files/topsearch_music_blended.json"
194 )) {
195 Ok(body) => {
196 assert_eq!(body.users.len(), 48);
197 }
198 Err(err) => panic!("{}", err),
199 }
200
201 match serde_json::from_str::<TopsearchResponseBody>(include_str!(
202 "../../tests/response_body_files/topsearch_with_empty.json"
203 )) {
204 Ok(body) => {
205 assert_eq!(body.users.len(), 0);
206 }
207 Err(err) => panic!("{}", err),
208 }
209 }
210}