instagram_web_api/endpoints/
topsearch.rs

1use 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//
19#[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    //
26    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            //
41            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
67//
68impl<'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//
132//
133//
134#[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//
144#[derive(Deserialize, Serialize, Debug, Clone)]
145pub struct TopsearchResponseBodyUser {
146    pub position: usize,
147    pub user: TopsearchUser,
148}
149
150//
151#[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    // TODO
162}
163
164//
165#[derive(Deserialize, Serialize, Debug, Clone)]
166pub struct TopsearchResponseBodyHashtag {
167    pub position: usize,
168    pub hashtag: TopsearchHashtag,
169}
170
171//
172//
173//
174#[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}