leetcode_cli/plugins/
leetcode.rs

1use self::req::{Json, Mode, Req};
2use crate::{
3    config::{self, Config},
4    Result,
5};
6use reqwest::{
7    header::{HeaderMap, HeaderName, HeaderValue},
8    Client, ClientBuilder, Response,
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            ["query getTopicTag($slug: String!) {",
90                "  topicTag(slug: $slug) {",
91                "    questions {",
92                "      questionId",
93                "    }",
94                "  }",
95                "}"]
96            .join("\n"),
97        );
98
99        Req {
100            default_headers: self.default_headers,
101            refer: Some(self.conf.sys.urls.tag(slug)),
102            info: false,
103            json: Some(json),
104            mode: Mode::Post,
105            name: "get_question_ids_by_tag",
106            url: (*url).to_string(),
107        }
108        .send(&self.client)
109        .await
110    }
111
112    pub async fn get_user_info(self) -> Result<Response> {
113        trace!("Requesting user info...");
114        let url = &self.conf.sys.urls.graphql;
115        let mut json: Json = HashMap::new();
116        json.insert("operationName", "a".to_string());
117        json.insert(
118            "query",
119            "query a {
120                 user {
121                     username
122                     isCurrentUserPremium
123                 }
124             }"
125            .to_owned(),
126        );
127
128        Req {
129            default_headers: self.default_headers,
130            refer: None,
131            info: false,
132            json: Some(json),
133            mode: Mode::Post,
134            name: "get_user_info",
135            url: (*url).to_string(),
136        }
137        .send(&self.client)
138        .await
139    }
140
141    /// Get daily problem
142    pub async fn get_question_daily(self) -> Result<Response> {
143        trace!("Requesting daily problem...");
144        let url = &self.conf.sys.urls.graphql;
145        let mut json: Json = HashMap::new();
146
147        match self.conf.cookies.site {
148            config::LeetcodeSite::LeetcodeCom => {
149                json.insert("operationName", "daily".to_string());
150                json.insert(
151                    "query",
152                    ["query daily {",
153                        "  activeDailyCodingChallengeQuestion {",
154                        "    question {",
155                        "      questionFrontendId",
156                        "    }",
157                        "  }",
158                        "}"]
159                    .join("\n"),
160                );
161            }
162            config::LeetcodeSite::LeetcodeCn => {
163                json.insert("operationName", "questionOfToday".to_string());
164                json.insert(
165                    "query",
166                    ["query questionOfToday {",
167                        "  todayRecord {",
168                        "    question {",
169                        "      questionFrontendId",
170                        "    }",
171                        "  }",
172                        "}"]
173                    .join("\n"),
174                );
175            }
176        }
177
178        Req {
179            default_headers: self.default_headers,
180            refer: None,
181            info: false,
182            json: Some(json),
183            mode: Mode::Post,
184            name: "get_question_daily",
185            url: (*url).to_string(),
186        }
187        .send(&self.client)
188        .await
189    }
190
191    /// Get specific problem detail
192    pub async fn get_question_detail(self, slug: &str) -> Result<Response> {
193        trace!("Requesting {} detail...", &slug);
194        let refer = self.conf.sys.urls.problem(slug);
195        let mut json: Json = HashMap::new();
196        json.insert(
197            "query",
198            ["query getQuestionDetail($titleSlug: String!) {",
199                "  question(titleSlug: $titleSlug) {",
200                "    content",
201                "    stats",
202                "    codeDefinition",
203                "    sampleTestCase",
204                "    exampleTestcases",
205                "    enableRunCode",
206                "    metaData",
207                "    translatedContent",
208                "  }",
209                "}"]
210            .join("\n"),
211        );
212
213        json.insert(
214            "variables",
215            r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug),
216        );
217
218        json.insert("operationName", "getQuestionDetail".to_string());
219
220        Req {
221            default_headers: self.default_headers,
222            refer: Some(refer),
223            info: false,
224            json: Some(json),
225            mode: Mode::Post,
226            name: "get_problem_detail",
227            url: self.conf.sys.urls.graphql,
228        }
229        .send(&self.client)
230        .await
231    }
232
233    /// Send code to judge
234    pub async fn run_code(self, j: Json, url: String, refer: String) -> Result<Response> {
235        info!("Sending code to judge...");
236        Req {
237            default_headers: self.default_headers,
238            refer: Some(refer),
239            info: false,
240            json: Some(j),
241            mode: Mode::Post,
242            name: "run_code",
243            url,
244        }
245        .send(&self.client)
246        .await
247    }
248
249    /// Get the result of submission / testing
250    pub async fn verify_result(self, id: String) -> Result<Response> {
251        trace!("Verifying result...");
252        let url = self.conf.sys.urls.verify(&id);
253
254        Req {
255            default_headers: self.default_headers,
256            refer: None,
257            info: false,
258            json: None,
259            mode: Mode::Get,
260            name: "verify_result",
261            url,
262        }
263        .send(&self.client)
264        .await
265    }
266}
267
268/// Sub-module for leetcode, simplify requests
269mod req {
270    use super::LeetCode;
271    use crate::err::Error;
272    use reqwest::{header::HeaderMap, Client, Response};
273    use std::collections::HashMap;
274
275    /// Standardize json format
276    pub type Json = HashMap<&'static str, String>;
277
278    /// Standardize request mode
279    pub enum Mode {
280        Get,
281        Post,
282    }
283
284    /// LeetCode request prototype
285    pub struct Req {
286        pub default_headers: HeaderMap,
287        pub refer: Option<String>,
288        pub json: Option<Json>,
289        pub info: bool,
290        pub mode: Mode,
291        pub name: &'static str,
292        pub url: String,
293    }
294
295    impl Req {
296        pub async fn send(self, client: &Client) -> Result<Response, Error> {
297            trace!("Running leetcode::{}...", &self.name);
298            if self.info {
299                info!("{}", &self.name);
300            }
301            let url = self.url.to_owned();
302            let headers = LeetCode::headers(
303                self.default_headers,
304                vec![("Referer", &self.refer.unwrap_or(url))],
305            )?;
306
307            let req = match self.mode {
308                Mode::Get => client.get(&self.url),
309                Mode::Post => client.post(&self.url).json(&self.json),
310            };
311
312            Ok(req.headers(headers).send().await?)
313        }
314    }
315}