leetcoderustapi 1.0.8

A full-featured Rust API for Leetcode.
Documentation
use error::Errors;
use problem_actions::Problem;
use problem_build::{Filters, ProblemBuilder};
use profile::{MyProfile, UserProfile};
use reqwest::header::{HeaderMap, HeaderValue};
use resources::{
    cookie::CookieData, descr::ProblemData, fav_list::FavoriteList,
    problemfulldata::ProblemFullData,
};
use serde_json::json;

pub mod error;
pub mod problem_actions;
pub mod problem_build;
pub mod profile;
pub mod resources;

#[derive(Debug)]
pub struct UserApi {
    client: reqwest::Client,
}

impl UserApi {
    pub async fn new(cookie: &str) -> Result<Self, Errors> {
        let mut headers = HeaderMap::new();

        headers.insert("Host", HeaderValue::from_static("leetcode.com"));
        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"));
        headers.insert("Origin", HeaderValue::from_static("https://leetcode.com"));
        headers.insert("Referer", HeaderValue::from_static("https://leetcode.com/"));
        headers.insert("content-type", HeaderValue::from_static("application/json"));
        headers.insert("Connection", HeaderValue::from_static("keep-alive"));
        headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
        headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
        headers.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));

        let valid_data = Self::valid_check(headers.clone(), &cookie).await?;

        let cookie = if valid_data.0 {
            cookie
        } else {
            return Err(error::Errors::ApiError(
                "Cookie is invalid or User not signed".into(),
            ));
        };

        let token = valid_data.1;

        headers.insert("Cookie", HeaderValue::from_str(&cookie).unwrap());
        headers.insert("x-csrftoken", HeaderValue::from_str(&token).unwrap());

        let client = reqwest::Client::builder()
            .default_headers(headers)
            .build()?;

        Ok(Self { client })
    }

    async fn valid_check(mut headers: HeaderMap, cookie: &str) -> Result<(bool, String), Errors> {
        let token = if let Some(token) = cookie
            .strip_prefix("csrftoken=")
            .and_then(|val| Some(&val[..64]))
        {
            token
        } else {
            return Err(Errors::ApiError("Cannot take token from cookie".into()));
        };

        headers.insert("Cookie", HeaderValue::from_str(&cookie).unwrap());
        headers.insert("x-csrftoken", HeaderValue::from_str(&token).unwrap());
        headers.insert("content-type", HeaderValue::from_static("application/json"));

        let json_data = json!({
            "operationName": "globalData",
            "variables": {},
            "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}"
        });

        let query = serde_json::to_string(&json_data)?;

        let client = reqwest::Client::new();

        let cookie_info = client
            .post("https://leetcode.com/graphql/")
            .body(query)
            .headers(headers.clone())
            .send()
            .await?
            .text()
            .await?;

        let resp_info = serde_json::from_str::<CookieData>(&cookie_info)?;

        if resp_info.data.userStatus.isSignedIn {
            return Ok((true, String::from(token)));
        }

        Ok((false, String::from(token)))
    }

    pub async fn set_problem(&self, problem_name: &str) -> Result<Problem, Errors> {
        let info = Self::fetch_problem_full_data(
            &self,
            Self::get_question_name(&self, String::from(problem_name)).await?,
        )
        .await?;

        Ok(Problem {
            client: self.client.clone(),
            task_search_name: info.0,
            full_data: info.1,
        })
    }

    pub async fn set_problem_by_id(&self, problem_id: u32) -> Result<Problem, Errors> {
        let info = Self::fetch_problem_full_data(
            &self,
            Self::get_question_name(&self, problem_id.to_string()).await?,
        )
        .await?;

        Ok(Problem {
            client: self.client.clone(),
            task_search_name: info.0,
            full_data: info.1,
        })
    }

    async fn fetch_problem_full_data(
        &self,
        problem: String,
    ) -> Result<(String, ProblemFullData), Errors> {
        let json_obj = json!({
            "operationName": "questionData",
            "variables": {
                "titleSlug": problem
            },
            "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}"
        });

        let query = serde_json::to_string(&json_obj)?;

        let full_data = self
            .client
            .post("https://leetcode.com/graphql/")
            .body(query)
            .send()
            .await?
            .json::<ProblemFullData>()
            .await?;

        Ok((full_data.data.question.titleSlug.clone(), full_data))
    }

    pub async fn show_problm_list(
        &self,
        key_word: &str,
        limit: u32,
    ) -> Result<ProblemData, Errors> {
        let query = json!({
            "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 } } }",
            "variables": {
                "categorySlug": "",
                "skip": 0,
                "limit": limit,
                "filters": {
                    "searchKeywords": String::from(key_word)
                }
            },
            "operationName": "problemsetQuestionList"
        });

        let query = serde_json::to_string(&query)?;

        let problem_info = self
            .client
            .post("https://leetcode.com/graphql/")
            .body(query)
            .send()
            .await?
            .text()
            .await?;

        Ok(serde_json::from_str::<ProblemData>(&problem_info)?)
    }

    pub fn problem_builder(&self) -> ProblemBuilder {
        ProblemBuilder {
            client: self.client.clone(),
            key_word: String::new(),
            limit: 5,
            category: String::new(),
            filters: Filters::default(),
        }
    }

    async fn get_question_name(&self, name: String) -> Result<String, Errors> {
        let query = json!({
            "query": "query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) { problemsetQuestionList: questionList( categorySlug: $categorySlug limit: $limit skip: $skip filters: $filters ) { questions: data { titleSlug } } }",
            "variables": {
                "categorySlug": "",
                "skip": 0,
                "limit": 1,
                "filters": {
                    "searchKeywords": name
                }
            },
            "operationName": "problemsetQuestionList"
        });

        let query = serde_json::to_string(&query)?;

        let problem_info = self
            .client
            .post("https://leetcode.com/graphql/")
            .body(query)
            .send()
            .await?
            .text()
            .await?;

        let parsed_data: ProblemData = serde_json::from_str(&problem_info)?;

        Ok(parsed_data.data.problemsetQuestionList.questions[0]
            .titleSlug
            .clone())
    }

    pub async fn my_profile(&self) -> Result<MyProfile, Errors> {
        Ok(MyProfile {
            client: self.client.clone(),
            fav_lists: Self::fetch_fav_list_data(&self).await?,
        })
    }

    pub fn find_profile(&self, username: &str) -> UserProfile {
        UserProfile {
            client: self.client.clone(),
            username: String::from(username),
        }
    }

    async fn fetch_fav_list_data(&self) -> Result<FavoriteList, Errors> {
        let query = json!({
            "operationName": "favoritesList",
            "variables": {},
            "query": "query favoritesList {
                favoritesLists {
                    allFavorites {
                        idHash
                        name
                        description
                        viewCount
                        creator
                        isWatched
                        isPublicFavorite
                        questions {
                            questionId
                            status
                            title
                            titleSlug
                            __typename
                        }
                        __typename
                    }
                    watchedFavorites {
                        idHash
                        name
                        description
                        viewCount
                        creator
                        isWatched
                        isPublicFavorite
                        questions {
                            questionId
                            status
                            title
                            titleSlug
                            __typename
                        }
                        __typename
                    }
                    __typename
                }
                userStatus {
                    username
                    __typename
                }
            }"
        });

        let query = serde_json::to_string(&query)?;

        let list_data = self
            .client
            .post("https://leetcode.com/graphql/")
            .body(query)
            .send()
            .await?
            .text()
            .await?;

        Ok(serde_json::from_str::<FavoriteList>(&list_data)?)
    }
}

#[derive(Debug)]
pub enum ProgrammingLanguage {
    CPP,
    Java,
    Python,
    Python3,
    C,
    CSharp,
    JavaScript,
    TypeScript,
    Ruby,
    Swift,
    Go,
    Bash,
    Scala,
    Kotlin,
    Rust,
    PHP,
    Racket,
    Erlang,
    Elixir,
    Dart,
    Pandas,
    React,
}