leetcode_cli/plugins/
leetcode.rs

1use self::req::{Json, Mode, Req};
2use crate::{
3    Result,
4    config::{self, Config},
5};
6use reqwest::{
7    Client, ClientBuilder, Response,
8    header::{HeaderMap, HeaderName, HeaderValue},
9};
10use std::{collections::HashMap, str::FromStr, time::Duration};
11
12/// LeetCode API set
13#[derive(Clone)]
14pub struct LeetCode {
15    pub conf: Config,
16    client: Client,
17    default_headers: HeaderMap,
18}
19
20impl LeetCode {
21    /// Parse reqwest headers
22    fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result<HeaderMap> {
23        for (k, v) in ts.into_iter() {
24            let name = HeaderName::from_str(k)?;
25            let value = HeaderValue::from_str(v)?;
26            headers.insert(name, value);
27        }
28
29        Ok(headers)
30    }
31
32    /// New LeetCode client
33    pub fn new() -> Result<LeetCode> {
34        let conf = config::Config::locate()?;
35        let (cookie, csrf) = if conf.cookies.csrf.is_empty() || conf.cookies.session.is_empty() {
36            let cookies = super::chrome::cookies()?;
37            (cookies.to_string(), cookies.csrf)
38        } else {
39            (conf.cookies.clone().to_string(), conf.cookies.clone().csrf)
40        };
41        let default_headers = LeetCode::headers(
42            HeaderMap::new(),
43            vec![
44                ("Cookie", &cookie),
45                ("x-csrftoken", &csrf),
46                ("x-requested-with", "XMLHttpRequest"),
47                ("Origin", &conf.sys.urls.base),
48            ],
49        )?;
50
51        let client = ClientBuilder::new()
52            .gzip(true)
53            .connect_timeout(Duration::from_secs(30))
54            .build()?;
55
56        Ok(LeetCode {
57            conf,
58            client,
59            default_headers,
60        })
61    }
62
63    /// Get category problems
64    pub async fn get_category_problems(self, category: &str) -> Result<Response> {
65        trace!("Requesting {} problems...", &category);
66        let url = &self.conf.sys.urls.problems(category);
67
68        Req {
69            default_headers: self.default_headers,
70            refer: None,
71            info: false,
72            json: None,
73            mode: Mode::Get,
74            name: "get_category_problems",
75            url: url.to_string(),
76        }
77        .send(&self.client)
78        .await
79    }
80
81    pub async fn get_question_ids_by_tag(self, slug: &str) -> Result<Response> {
82        trace!("Requesting {} ref problems...", &slug);
83        let url = &self.conf.sys.urls.graphql;
84        let mut json: Json = HashMap::new();
85        json.insert("operationName", "getTopicTag".to_string());
86        json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug));
87        json.insert(
88            "query",
89            [
90                "query getTopicTag($slug: String!) {",
91                "  topicTag(slug: $slug) {",
92                "    questions {",
93                "      questionId",
94                "    }",
95                "  }",
96                "}",
97            ]
98            .join("\n"),
99        );
100
101        Req {
102            default_headers: self.default_headers,
103            refer: Some(self.conf.sys.urls.tag(slug)),
104            info: false,
105            json: Some(json),
106            mode: Mode::Post,
107            name: "get_question_ids_by_tag",
108            url: (*url).to_string(),
109        }
110        .send(&self.client)
111        .await
112    }
113
114    pub async fn get_user_info(self) -> Result<Response> {
115        trace!("Requesting user info...");
116        let url = &self.conf.sys.urls.graphql;
117        let mut json: Json = HashMap::new();
118        json.insert("operationName", "a".to_string());
119        json.insert(
120            "query",
121            "query a {
122                 user {
123                     username
124                     isCurrentUserPremium
125                 }
126             }"
127            .to_owned(),
128        );
129
130        Req {
131            default_headers: self.default_headers,
132            refer: None,
133            info: false,
134            json: Some(json),
135            mode: Mode::Post,
136            name: "get_user_info",
137            url: (*url).to_string(),
138        }
139        .send(&self.client)
140        .await
141    }
142
143    /// Get daily problem
144    pub async fn get_question_daily(self) -> Result<Response> {
145        trace!("Requesting daily problem...");
146        let url = &self.conf.sys.urls.graphql;
147        let mut json: Json = HashMap::new();
148
149        match self.conf.cookies.site {
150            config::LeetcodeSite::LeetcodeCom => {
151                json.insert("operationName", "daily".to_string());
152                json.insert(
153                    "query",
154                    [
155                        "query daily {",
156                        "  activeDailyCodingChallengeQuestion {",
157                        "    question {",
158                        "      questionFrontendId",
159                        "    }",
160                        "  }",
161                        "}",
162                    ]
163                    .join("\n"),
164                );
165            }
166            config::LeetcodeSite::LeetcodeCn => {
167                json.insert("operationName", "questionOfToday".to_string());
168                json.insert(
169                    "query",
170                    [
171                        "query questionOfToday {",
172                        "  todayRecord {",
173                        "    question {",
174                        "      questionFrontendId",
175                        "    }",
176                        "  }",
177                        "}",
178                    ]
179                    .join("\n"),
180                );
181            }
182        }
183
184        Req {
185            default_headers: self.default_headers,
186            refer: None,
187            info: false,
188            json: Some(json),
189            mode: Mode::Post,
190            name: "get_question_daily",
191            url: (*url).to_string(),
192        }
193        .send(&self.client)
194        .await
195    }
196
197    /// Get specific problem detail
198    pub async fn get_question_detail(self, slug: &str) -> Result<Response> {
199        trace!("Requesting {} detail...", &slug);
200        let refer = self.conf.sys.urls.problem(slug);
201        let mut json: Json = HashMap::new();
202        json.insert(
203            "query",
204            [
205                "query getQuestionDetail($titleSlug: String!) {",
206                "  question(titleSlug: $titleSlug) {",
207                "    content",
208                "    stats",
209                "    codeDefinition",
210                "    sampleTestCase",
211                "    exampleTestcases",
212                "    enableRunCode",
213                "    metaData",
214                "    translatedContent",
215                "  }",
216                "}",
217            ]
218            .join("\n"),
219        );
220
221        json.insert(
222            "variables",
223            r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug),
224        );
225
226        json.insert("operationName", "getQuestionDetail".to_string());
227
228        Req {
229            default_headers: self.default_headers,
230            refer: Some(refer),
231            info: false,
232            json: Some(json),
233            mode: Mode::Post,
234            name: "get_problem_detail",
235            url: self.conf.sys.urls.graphql,
236        }
237        .send(&self.client)
238        .await
239    }
240
241    /// Send code to judge
242    pub async fn run_code(self, j: Json, url: String, refer: String) -> Result<Response> {
243        info!("Sending code to judge...");
244        Req {
245            default_headers: self.default_headers,
246            refer: Some(refer),
247            info: false,
248            json: Some(j),
249            mode: Mode::Post,
250            name: "run_code",
251            url,
252        }
253        .send(&self.client)
254        .await
255    }
256
257    /// Get the result of submission / testing
258    pub async fn verify_result(self, id: String) -> Result<Response> {
259        trace!("Verifying result...");
260        let url = self.conf.sys.urls.verify(&id);
261
262        Req {
263            default_headers: self.default_headers,
264            refer: None,
265            info: false,
266            json: None,
267            mode: Mode::Get,
268            name: "verify_result",
269            url,
270        }
271        .send(&self.client)
272        .await
273    }
274}
275
276/// Sub-module for leetcode, simplify requests
277mod req {
278    use super::LeetCode;
279    use crate::err::Error;
280    use reqwest::{Client, Response, header::HeaderMap};
281    use std::collections::HashMap;
282
283    /// Standardize json format
284    pub type Json = HashMap<&'static str, String>;
285
286    /// Standardize request mode
287    pub enum Mode {
288        Get,
289        Post,
290    }
291
292    /// LeetCode request prototype
293    pub struct Req {
294        pub default_headers: HeaderMap,
295        pub refer: Option<String>,
296        pub json: Option<Json>,
297        pub info: bool,
298        pub mode: Mode,
299        pub name: &'static str,
300        pub url: String,
301    }
302
303    impl Req {
304        pub async fn send(self, client: &Client) -> Result<Response, Error> {
305            trace!("Running leetcode::{}...", &self.name);
306            if self.info {
307                info!("{}", &self.name);
308            }
309            let url = self.url.to_owned();
310            let headers = LeetCode::headers(
311                self.default_headers,
312                vec![("Referer", &self.refer.unwrap_or(url))],
313            )?;
314
315            let req = match self.mode {
316                Mode::Get => client.get(&self.url),
317                Mode::Post => client.post(&self.url).json(&self.json),
318            };
319
320            Ok(req.headers(headers).send().await?)
321        }
322    }
323}