leetcoderustapi/
lib.rs

1use error::Errors;
2use problem_actions::Problem;
3use problem_build::{Filters, ProblemBuilder};
4use profile::{MyProfile, UserProfile};
5use reqwest::header::{HeaderMap, HeaderValue};
6use resources::{
7    cookie::CookieData, descr::ProblemData, fav_list::FavoriteList,
8    problemfulldata::ProblemFullData,
9};
10use serde_json::json;
11
12pub mod error;
13pub mod problem_actions;
14pub mod problem_build;
15pub mod profile;
16pub mod resources;
17
18#[derive(Debug)]
19pub struct UserApi {
20    client: reqwest::Client,
21}
22
23impl UserApi {
24    pub async fn new(cookie: &str) -> Result<Self, Errors> {
25        let mut headers = HeaderMap::new();
26
27        headers.insert("Host", HeaderValue::from_static("leetcode.com"));
28        headers.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"));
29        headers.insert("Origin", HeaderValue::from_static("https://leetcode.com"));
30        headers.insert("Referer", HeaderValue::from_static("https://leetcode.com/"));
31        headers.insert("content-type", HeaderValue::from_static("application/json"));
32        headers.insert("Connection", HeaderValue::from_static("keep-alive"));
33        headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
34        headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
35        headers.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));
36
37        let valid_data = Self::valid_check(headers.clone(), &cookie).await?;
38
39        let cookie = if valid_data.0 {
40            cookie
41        } else {
42            return Err(error::Errors::ApiError(
43                "Cookie is invalid or User not signed".into(),
44            ));
45        };
46
47        let token = valid_data.1;
48
49        headers.insert("Cookie", HeaderValue::from_str(&cookie).unwrap());
50        headers.insert("x-csrftoken", HeaderValue::from_str(&token).unwrap());
51
52        let client = reqwest::Client::builder()
53            .default_headers(headers)
54            .build()?;
55
56        Ok(Self { client })
57    }
58
59    async fn valid_check(mut headers: HeaderMap, cookie: &str) -> Result<(bool, String), Errors> {
60        let token = if let Some(token) = cookie
61            .strip_prefix("csrftoken=")
62            .and_then(|val| Some(&val[..64]))
63        {
64            token
65        } else {
66            return Err(Errors::ApiError("Cannot take token from cookie".into()));
67        };
68
69        headers.insert("Cookie", HeaderValue::from_str(&cookie).unwrap());
70        headers.insert("x-csrftoken", HeaderValue::from_str(&token).unwrap());
71        headers.insert("content-type", HeaderValue::from_static("application/json"));
72
73        let json_data = json!({
74            "operationName": "globalData",
75            "variables": {},
76            "query": "query globalData {\n  userStatus {\n    isSignedIn\n    isAdmin\n    isStaff\n    isSuperuser\n    isMockUser\n    isTranslator\n    isPremium\n    isVerified\n    checkedInToday\n    username\n    realName\n    avatar\n    optedIn\n    requestRegion\n    region\n    activeSessionId\n    permissions\n    notificationStatus {\n      lastModified\n      numUnread\n      __typename\n    }\n    completedFeatureGuides\n    __typename\n  }\n  recaptchaKey\n}"
77        });
78
79        let query = serde_json::to_string(&json_data)?;
80
81        let client = reqwest::Client::new();
82
83        let cookie_info = client
84            .post("https://leetcode.com/graphql/")
85            .body(query)
86            .headers(headers.clone())
87            .send()
88            .await?
89            .text()
90            .await?;
91
92        let resp_info = serde_json::from_str::<CookieData>(&cookie_info)?;
93
94        if resp_info.data.userStatus.isSignedIn {
95            return Ok((true, String::from(token)));
96        }
97
98        Ok((false, String::from(token)))
99    }
100
101    pub async fn set_problem(&self, problem_name: &str) -> Result<Problem, Errors> {
102        let info = Self::fetch_problem_full_data(
103            &self,
104            Self::get_question_name(&self, String::from(problem_name)).await?,
105        )
106        .await?;
107
108        Ok(Problem {
109            client: self.client.clone(),
110            task_search_name: info.0,
111            full_data: info.1,
112        })
113    }
114
115    pub async fn set_problem_by_id(&self, problem_id: u32) -> Result<Problem, Errors> {
116        let info = Self::fetch_problem_full_data(
117            &self,
118            Self::get_question_name(&self, problem_id.to_string()).await?,
119        )
120        .await?;
121
122        Ok(Problem {
123            client: self.client.clone(),
124            task_search_name: info.0,
125            full_data: info.1,
126        })
127    }
128
129    async fn fetch_problem_full_data(
130        &self,
131        problem: String,
132    ) -> Result<(String, ProblemFullData), Errors> {
133        let json_obj = json!({
134            "operationName": "questionData",
135            "variables": {
136                "titleSlug": problem
137            },
138            "query": "query questionData($titleSlug: String!) {\n  question(titleSlug: $titleSlug) {\n    questionId\n    questionFrontendId\n    boundTopicId\n    title\n    titleSlug\n    content\n    translatedTitle\n    translatedContent\n    isPaidOnly\n    canSeeQuestion\n    difficulty\n    likes\n    dislikes\n    isLiked\n    similarQuestions\n    exampleTestcases\n    categoryTitle\n    contributors {\n      username\n      profileUrl\n      avatarUrl\n      __typename\n    }\n    topicTags {\n      name\n      slug\n      translatedName\n      __typename\n    }\n    companyTagStats\n    codeSnippets {\n      lang\n      langSlug\n      code\n      __typename\n    }\n    stats\n    hints\n    solution {\n      id\n      canSeeDetail\n      paidOnly\n      hasVideoSolution\n      paidOnlyVideo\n      __typename\n    }\n    status\n    sampleTestCase\n    metaData\n    judgerAvailable\n    judgeType\n    mysqlSchemas\n    enableRunCode\n    enableTestMode\n    enableDebugger\n    envInfo\n    libraryUrl\n    adminUrl\n    challengeQuestion {\n      id\n      date\n      incompleteChallengeCount\n      streakCount\n      type\n      __typename\n    }\n    __typename\n  }\n}"
139        });
140
141        let query = serde_json::to_string(&json_obj)?;
142
143        let full_data = self
144            .client
145            .post("https://leetcode.com/graphql/")
146            .body(query)
147            .send()
148            .await?
149            .json::<ProblemFullData>()
150            .await?;
151
152        Ok((full_data.data.question.titleSlug.clone(), full_data))
153    }
154
155    pub async fn show_problm_list(
156        &self,
157        key_word: &str,
158        limit: u32,
159    ) -> Result<ProblemData, Errors> {
160        let query = json!({
161            "query": "query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) { problemsetQuestionList: questionList( categorySlug: $categorySlug limit: $limit skip: $skip filters: $filters ) { total: totalNum questions: data { acRate difficulty freqBar frontendQuestionId: questionFrontendId isFavor paidOnly: isPaidOnly status title titleSlug topicTags { name id slug } hasSolution hasVideoSolution } } }",
162            "variables": {
163                "categorySlug": "",
164                "skip": 0,
165                "limit": limit,
166                "filters": {
167                    "searchKeywords": String::from(key_word)
168                }
169            },
170            "operationName": "problemsetQuestionList"
171        });
172
173        let query = serde_json::to_string(&query)?;
174
175        let problem_info = self
176            .client
177            .post("https://leetcode.com/graphql/")
178            .body(query)
179            .send()
180            .await?
181            .text()
182            .await?;
183
184        Ok(serde_json::from_str::<ProblemData>(&problem_info)?)
185    }
186
187    pub fn problem_builder(&self) -> ProblemBuilder {
188        ProblemBuilder {
189            client: self.client.clone(),
190            key_word: String::new(),
191            limit: 5,
192            category: String::new(),
193            filters: Filters::default(),
194        }
195    }
196
197    async fn get_question_name(&self, name: String) -> Result<String, Errors> {
198        let query = json!({
199            "query": "query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) { problemsetQuestionList: questionList( categorySlug: $categorySlug limit: $limit skip: $skip filters: $filters ) { questions: data { titleSlug } } }",
200            "variables": {
201                "categorySlug": "",
202                "skip": 0,
203                "limit": 1,
204                "filters": {
205                    "searchKeywords": name
206                }
207            },
208            "operationName": "problemsetQuestionList"
209        });
210
211        let query = serde_json::to_string(&query)?;
212
213        let problem_info = self
214            .client
215            .post("https://leetcode.com/graphql/")
216            .body(query)
217            .send()
218            .await?
219            .text()
220            .await?;
221
222        let parsed_data: ProblemData = serde_json::from_str(&problem_info)?;
223
224        Ok(parsed_data.data.problemsetQuestionList.questions[0]
225            .titleSlug
226            .clone())
227    }
228
229    pub async fn my_profile(&self) -> Result<MyProfile, Errors> {
230        Ok(MyProfile {
231            client: self.client.clone(),
232            fav_lists: Self::fetch_fav_list_data(&self).await?,
233        })
234    }
235
236    pub fn find_profile(&self, username: &str) -> UserProfile {
237        UserProfile {
238            client: self.client.clone(),
239            username: String::from(username),
240        }
241    }
242
243    async fn fetch_fav_list_data(&self) -> Result<FavoriteList, Errors> {
244        let query = json!({
245            "operationName": "favoritesList",
246            "variables": {},
247            "query": "query favoritesList {
248                favoritesLists {
249                    allFavorites {
250                        idHash
251                        name
252                        description
253                        viewCount
254                        creator
255                        isWatched
256                        isPublicFavorite
257                        questions {
258                            questionId
259                            status
260                            title
261                            titleSlug
262                            __typename
263                        }
264                        __typename
265                    }
266                    watchedFavorites {
267                        idHash
268                        name
269                        description
270                        viewCount
271                        creator
272                        isWatched
273                        isPublicFavorite
274                        questions {
275                            questionId
276                            status
277                            title
278                            titleSlug
279                            __typename
280                        }
281                        __typename
282                    }
283                    __typename
284                }
285                userStatus {
286                    username
287                    __typename
288                }
289            }"
290        });
291
292        let query = serde_json::to_string(&query)?;
293
294        let list_data = self
295            .client
296            .post("https://leetcode.com/graphql/")
297            .body(query)
298            .send()
299            .await?
300            .text()
301            .await?;
302
303        Ok(serde_json::from_str::<FavoriteList>(&list_data)?)
304    }
305}
306
307#[derive(Debug)]
308pub enum ProgrammingLanguage {
309    CPP,
310    Java,
311    Python,
312    Python3,
313    C,
314    CSharp,
315    JavaScript,
316    TypeScript,
317    Ruby,
318    Swift,
319    Go,
320    Bash,
321    Scala,
322    Kotlin,
323    Rust,
324    PHP,
325    Racket,
326    Erlang,
327    Elixir,
328    Dart,
329    Pandas,
330    React,
331}