1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
use lazy_static::*;
use reqwest::blocking::Client;
use reqwest::blocking::Request;
use reqwest::cookie::Jar;
use reqwest::IntoUrl;
use reqwest::Method;

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;

use super::quiz::{Level, Quiz};

const LC_GRAPHQL_ENDPOINT: &str = "https://leetcode.com/graphql";
const LC_GRAPHQL_BODY: [&str; 2] = [
    r#"{"operationName": "questionData", "variables": {"titleSlug": ""#,
    r#""},"query": "query questionData($titleSlug: String!) {question(titleSlug: $titleSlug) {questionId questionFrontendId boundTopicId title titleSlug content translatedTitle translatedContent isPaidOnly difficulty likes dislikes isLiked similarQuestions exampleTestcases contributors {username profileUrl avatarUrl __typename} topicTags {name slug translatedName __typename} companyTagStats codeSnippets {lang langSlug code __typename} stats hints solution {id canSeeDetail paidOnly hasVideoSolution paidOnlyVideo __typename} status sampleTestCase metaData judgerAvailable judgeType mysqlSchemas enableRunCode enableTestMode enableDebugger envInfo libraryUrl adminUrl __typename}}"}"#,
];

/// referer header for all quiz & random-one-question
const LC_ALL_QUIZ_REFERER: &str = "https://leetcode.com/problemset/all/";

/// endpoint of all quiz
const LC_ALL_QUIZ_API: &str = "https://leetcode.com/api/problems/all/";

/// endpoint of random pick
const LC_RANDOM_QUIZ_API: &str = "https://leetcode.com/problems/random-one-question/all";

lazy_static! {
    /// csrf token
    static ref CSRFTOKEN: Mutex<Option<String>> = Mutex::new(None);
}

/// return response
/// url will return bunch of js script, need name in url to make json request
pub(super) fn get_quiz_by_url(url: &str) -> Result<Quiz, String> {
    let token = get_csrftoken("./vault/csrftoken")?;
    let cli = make_client(&token, LC_GRAPHQL_ENDPOINT).map_err(|e| e.to_string())?;
    let req = request_builder(
        &cli,
        Method::POST,
        LC_GRAPHQL_ENDPOINT,
        url,
        &token,
        LC_GRAPHQL_BODY[0].to_string() + url.rsplit('/').nth(1).unwrap() + LC_GRAPHQL_BODY[1],
    )
    .map_err(|e| e.to_string())?;

    let resp = cli.execute(req).map_err(|e| e.to_string())?;
    Quiz::from_resp(resp, url.into())
}

pub(super) fn get_all_quiz() -> Result<serde_json::Value, String> {
    let token = get_csrftoken("./vault/csrftoken")?;
    let cli = make_client(&token, LC_ALL_QUIZ_API).map_err(|e| e.to_string())?;
    let req = request_builder(
        &cli,
        Method::GET,
        LC_ALL_QUIZ_API,
        LC_ALL_QUIZ_REFERER,
        &token,
        String::new(),
    )
    .map_err(|e| e.to_string())?;

    match cli.execute(req).map_err(|e| e.to_string())?.text() {
        Ok(c) => serde_json::from_str(&c).map_err(|e| e.to_string()),
        Err(e) => Err(e.to_string()),
    }
}

pub(super) fn get_quiz_by_id(id: u64) -> Result<Quiz, String> {
    let v = get_all_quiz()?;
    let quiz_list = v
        .get("stat_status_pairs")
        .ok_or("Cannot found stat_status_pairs".to_string())?
        .as_array()
        .ok_or("Cannot as_array".to_string())?;

    let quiz_name = quiz_list
        .iter()
        .find_map(|v| {
            v.get("stat").map_or(None, |stat| {
                // fronted id
                stat.get("frontend_question_id").map_or(None, |v| {
                    if v.as_u64().unwrap() == id {
                        stat.get("question__title_slug")
                    } else {
                        None
                    }
                })
            })
        })
        .ok_or("Cannot found this id".to_string())?
        .as_str()
        .ok_or("Cannot parse to string".to_string())?;
    //dbg!("quiz name: {}", quiz_name);
    Quiz::get_by_name(quiz_name)
}

pub(super) fn get_random_quiz(l: Option<Level>) -> Result<Quiz, String> {
    let token = get_csrftoken("./vault/csrftoken")?;
    let cli = make_client(&token, LC_RANDOM_QUIZ_API).map_err(|e| e.to_string())?;

    loop {
        let req = request_builder(
            &cli,
            Method::GET,
            LC_RANDOM_QUIZ_API,
            LC_ALL_QUIZ_REFERER,
            &token,
            String::new(),
        )
        .map_err(|e| e.to_string())?;

        // 302 will call new location immediatly
        match cli.execute(req).map_err(|e| e.to_string()) {
            Ok(c) => {
                let jump_to_url = c.url().as_str(); // this url
                match l.as_ref() {
                    Some(level) => {
                        let q = get_quiz_by_url(jump_to_url)?;
                        if q.quiz_level() == level {
                            return Ok(q);
                        } else {
                            continue;
                        }
                    }
                    None => return get_quiz_by_url(jump_to_url), // get this quiz,
                }
            }
            Err(e) => return Err(e.to_string()),
        }
    }
}

/// read csrftoken from file
fn get_csrftoken(path: impl AsRef<Path>) -> Result<String, String> {
    // cache it
    match CSRFTOKEN.try_lock() {
        Ok(ref mut t) => {
            if t.is_some() {
                return Ok(t.as_ref().unwrap().to_string());
            } else {
                let mut f = File::open(path).map_err(|e| e.to_string() + ". Read token error")?;
                let mut buffer = Vec::new();
                f.read_to_end(&mut buffer).map_err(|e| e.to_string())?;

                let token = String::from_utf8(buffer).map_err(|e| e.to_string())?;
                **t = Some(token.clone());

                Ok(token)
            }
        }
        // this part is meanless when app run. but for test.
        // need to handle err
        Err(_) => {
            let mut f = File::open(path).map_err(|e| e.to_string())?;
            let mut buffer = Vec::new();
            f.read_to_end(&mut buffer).map_err(|e| e.to_string())?;

            let token = String::from_utf8(buffer).map_err(|e| e.to_string())?;

            Ok(token)
        }
    }
}

/// set token with input token
pub fn set_token(token_str: &Option<String>) {
    if token_str.is_some() {
        match CSRFTOKEN.try_lock() {
            Ok(ref mut t) => **t = token_str.clone(),
            Err(e) => panic!("{}", e.to_string()),
        }
    }
}

fn make_client(cookie: &str, url: impl IntoUrl) -> reqwest::Result<Client> {
    let jar = Jar::default();
    jar.add_cookie_str(&(String::from("csrftoken=") + cookie), &url.into_url()?);
    Client::builder()
        .cookie_store(true)
        .cookie_provider(Arc::new(jar))
        .build()
}

fn request_builder(
    client: &Client,
    method: Method,
    endpoint: impl IntoUrl + Clone,
    referer: impl IntoUrl + Clone,
    token: &str,
    body: String,
) -> reqwest::Result<Request> {
    client
        .request(method, endpoint)
        .header("X-CSRFToken", token)
        .header("referer", referer.as_str())
        .header("Content-Type", "application/json")
        .body(body)
        .build()
}

#[cfg(test)]
mod tests {
    use reqwest::blocking::Client;

    use super::*;

    #[test]
    fn test_request_builder() {
        let cli = Client::builder().build().unwrap();
        let token = get_csrftoken("./vault/csrftoken").unwrap();
        let req = request_builder(
            &cli,
            Method::POST,
            "https://aaa/bb/namehere/",
            "https://aaa/bb/namehere/",
            &token,
            "haha".into(),
        )
        .unwrap();

        assert_eq!(
            String::from_utf8(req.body().unwrap().as_bytes().unwrap().to_vec()).unwrap(),
            "haha".to_string()
        );
    }

    #[test]
    fn test_make_json_body() {
        dbg!(LC_GRAPHQL_BODY[0].to_string() + "aaa" + LC_GRAPHQL_BODY[1]);
    }

    #[test]
    fn test_set_token() {
        set_token(&Some("aaa".to_string()));
        match CSRFTOKEN.try_lock() {
            Ok(ref mut t) => assert_eq!(**t, Some("aaa".to_string())),
            Err(e) => panic!("{}", e.to_string()),
        }
    }
}