From c3c3df3a5df04245b368fe88725106632dff12e0 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月18日 15:35:31 +0800 Subject: [PATCH 01/38] add graphql query for user info --- src/plugins/leetcode.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index d98f4a8..4394532 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -121,6 +121,34 @@ impl LeetCode { .await } + pub async fn get_user_info(self) -> Result { + trace!("Requesting user info..."); + let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let mut json: Json = HashMap::new(); + json.insert("operationName", "a".to_string()); + json.insert( + "query", + "query a { + user { + username + isCurrentUserPremium + } + }".to_owned() + ); + + Req { + default_headers: self.default_headers, + refer: None, + info: false, + json: Some(json), + mode: Mode::Post, + name: "get_user_info", + url: (*url).to_string(), + } + .send(&self.client) + .await + } + /// Get daily problem pub async fn get_question_daily(self) -> Result { trace!("Requesting daily problem..."); From e8e83eb04caa4d483e0b8ba52453799e51caef8c Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月18日 15:36:05 +0800 Subject: [PATCH 02/38] add parser for graphql user info --- src/cache/parser.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/cache/parser.rs b/src/cache/parser.rs index f60476d..130fbee 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -88,6 +88,21 @@ pub fn daily(v: Value) -> Option { .parse().ok() } +/// user parser +pub fn user(v: Value) -> Option> { + // None => error while parsing + // Some(None) => User not found + // Some("...") => username + let user = v.as_object()?.get("data")? + .as_object()?.get("user")?; + if *user == Value::Null { return Some(None) } + let user = user.as_object()?; + Some(Some(( + user.get("username")?.as_str()?.to_owned(), + user.get("isCurrentUserPremium")?.as_bool()? + ))) +} + pub use ss::ssr; /// string or squence mod ss { From a22a4f6df5a95ca5f09806d5ae2394e48623e965 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月18日 15:41:04 +0800 Subject: [PATCH 03/38] return `CookieError` when leetcode rejects exec/test `Run` --- src/cache/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index a0f862b..98363de 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -311,6 +311,12 @@ impl Cache { .await?; trace!("Run code result {:#?}", run_res); + // Check if leetcode accepted the Run request + if match run { + Run::Test => run_res.interpret_id.is_empty(), + Run::Submit => run_res.submission_id == 0 + } { return Err(Error::CookieError) } + let mut res: VerifyResult = VerifyResult::default(); while res.state != "SUCCESS" { res = match run { From 9d1a95cce035b87bc6801c6ccfb730cd4e43725c Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月18日 15:44:12 +0800 Subject: [PATCH 04/38] add the `is_session_bad` method to somewhat-accurately determine when a LEETCODE_SESSION becomes invalid --- src/cache/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 98363de..f5ee14f 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -58,6 +58,27 @@ impl Cache { Ok(()) } + async fn get_user_info(&self) -> Result<(string,bool), Error> { + let user = parser::user( + self.clone().0 + .get_user_info().await? + .json().await? + ); + match user { + None => Err(Error::NoneError), + Some(None) => Err(Error::CookieError), + Some(Some((s,b))) => Ok((s,b)) + } + } + + async fn is_session_bad(&self) -> bool { + // i.e. self.get_user_info().contains_err(Error::CookieError) + match self.get_user_info().await { + Err(Error::CookieError) => true, + _ => false + } + } + /// Download leetcode problems to db pub async fn download_problems(self) -> Result, Error> { info!("Fetching leetcode problems..."); From 916e936d16c4067cdadc5863f98e0427768df8d7 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月18日 15:48:03 +0800 Subject: [PATCH 05/38] When json parsing fails, if the underlying request requires user authentication, use `is_session_bad()` to check if LEETCODE_SESSION is valid. --- src/cache/mod.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index f5ee14f..8af78d7 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -9,8 +9,10 @@ use self::sql::*; use crate::{cfg, err::Error, plugins::LeetCode}; use colored::Colorize; use diesel::prelude::*; +use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::HashMap; +use reqwest::Response; /// sqlite connection pub fn conn(p: String) -> SqliteConnection { @@ -79,6 +81,13 @@ impl Cache { } } + async fn resp_to_json(&self, resp: Response) -> Result { + let maybe_json: Result = resp.json().await; + if maybe_json.is_err() && self.is_session_bad().await { + Err(Error::CookieError) + } else { Ok(maybe_json?) } + } + /// Download leetcode problems to db pub async fn download_problems(self) -> Result, Error> { info!("Fetching leetcode problems..."); @@ -90,7 +99,7 @@ impl Cache { .clone() .get_category_problems(i) .await? - .json() + .json() // does not require LEETCODE_SESSION .await?; parser::problem(&mut ps, json).ok_or(Error::NoneError)?; } @@ -121,7 +130,7 @@ impl Cache { .0 .get_question_daily() .await? - .json() + .json() // does not require LEETCODE_SESSION .await? ).ok_or(Error::NoneError) } @@ -286,30 +295,20 @@ impl Cache { /// TODO: The real delay async fn recur_verify(&self, rid: String) -> Result { - use serde_json::{from_str, Error as SJError}; use std::time::Duration; trace!("Run veriy recursion..."); std::thread::sleep(Duration::from_micros(3000)); - // debug resp raw text - let debug_raw = self + let json: VerifyResult = self.resp_to_json( + self .clone() .0 .verify_result(rid.clone()) .await? - .text() - .await?; - debug!("debug resp raw text: \n{:#?}", &debug_raw); - if debug_raw.is_empty() { - return Err(Error::CookieError); - } - - // debug json deserializing - let debug_json: Result = from_str(&debug_raw); - debug!("debug json deserializing: \n{:#?}", &debug_json); + ).await?; - Ok(debug_json?) + Ok(json) } /// Exec problem filter —— Test or Submit @@ -328,7 +327,7 @@ impl Cache { .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? - .json() + .json() // does not require LEETCODE_SESSION (very oddly) .await?; trace!("Run code result {:#?}", run_res); From bd740e8ac34670a39839044c896d79452801de88 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月20日 11:51:41 +0800 Subject: [PATCH 06/38] get rid of ZWSPs in problem descriptions (see #56) --- src/helper.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helper.rs b/src/helper.rs index 5d8d746..b68fe68 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -147,7 +147,11 @@ mod html { impl HTML for String { fn ser(&self) -> Vec { // empty tags - let tks = self.to_string(); + let tks = { + let mut s = self.clone(); + // some problems (e.g. 1653) have ZWSPs. + s.retain(|x| x != '\u{200B}'); + s }; let res: Vec; // styled { From ff7baf0e24facfd63aa4ace404136b55b4ab34ce Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月20日 13:05:41 +0800 Subject: [PATCH 07/38] add Error::PremiumError --- src/err.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/err.rs b/src/err.rs index 8fd9b57..3ae8cc1 100644 --- a/src/err.rs +++ b/src/err.rs @@ -14,6 +14,7 @@ pub enum Error { FeatureError(String), ScriptError(String), CookieError, + PremiumError, DecryptError, SilentError, NoneError, @@ -37,6 +38,13 @@ impl std::fmt::Debug for Error { .yellow() .bold(), ), + Error::PremiumError => write!( + f, + "{} \ + Your leetcode account lacks a premium subscription, which the given problem requires.\n \ + If this looks like a mistake, please open a new issue at: {}", + e, + "https://github.com/clearloop/leetcode-cli/".underline()), Error::DownloadError(s) => write!(f, "{} Download {} failed, please try again", e, s), Error::NetworkError(s) => write!(f, "{} {}, please try again", e, s), Error::ParseError(s) => write!(f, "{} {}", e, s), From 496c3329d8f856d296c91c84319df3822bd885fa Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月20日 13:07:07 +0800 Subject: [PATCH 08/38] throw PremiumError when locked questions are queried for details --- src/cache/mod.rs | 11 ++++++++++- src/cache/parser.rs | 12 ++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8af78d7..5e1309f 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -177,7 +177,16 @@ impl Cache { .json() .await?; debug!("{:#?}", &json); - parser::desc(&mut rdesc, json).ok_or(Error::NoneError)?; + match parser::desc(&mut rdesc, json) { + None => return Err(Error::NoneError), + Some(false) => return + if self.is_session_bad().await { + Err(Error::CookieError) + } else { + Err(Error::PremiumError) + }, + Some(true) => () + } // update the question let sdesc = serde_json::to_string(&rdesc)?; diff --git a/src/cache/parser.rs b/src/cache/parser.rs index 130fbee..fb9ad2b 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -29,7 +29,11 @@ pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { } /// desc parser -pub fn desc(q: &mut Question, v: Value) -> Option<()> { +pub fn desc(q: &mut Question, v: Value) -> Option { + /* None - parsing failed + * Some(false) - content was null (premium?) + * Some(true) - content was parsed + */ let o = &v .as_object()? .get("data")? @@ -37,6 +41,10 @@ pub fn desc(q: &mut Question, v: Value) -> Option<()> { .get("question")? .as_object()?; + if *o.get("content")? == Value::Null { + return Some(false); + } + *q = Question { content: o.get("content")?.as_str().unwrap_or("").to_string(), stats: serde_json::from_str(o.get("stats")?.as_str()?).ok()?, @@ -55,7 +63,7 @@ pub fn desc(q: &mut Question, v: Value) -> Option<()> { .to_string(), }; - Some(()) + Some(true) } /// tag parser From 0f881e25a3cb57b9dd4a88e9bdef32d2fcb9944a Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 08:57:27 +0800 Subject: [PATCH 09/38] clippy fix --- src/cache/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 5e1309f..7b9662d 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -74,11 +74,7 @@ impl Cache { } async fn is_session_bad(&self) -> bool { - // i.e. self.get_user_info().contains_err(Error::CookieError) - match self.get_user_info().await { - Err(Error::CookieError) => true, - _ => false - } + matches!(self.get_user_info().await, Err(Error::CookieError)) } async fn resp_to_json(&self, resp: Response) -> Result { From 22248f252fd78a710c72e82545dada53308880cb Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 08:59:05 +0800 Subject: [PATCH 10/38] slight refactor: use multiline strings instead of `vec![].join("")`, use `function_name!()` instead of manually writing function names --- src/plugins/leetcode.rs | 83 ++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 4394532..41ee789 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -9,6 +9,7 @@ use reqwest::{ Client, ClientBuilder, Response, }; use std::{collections::HashMap, str::FromStr, time::Duration}; +use ::function_name::named; /// LeetCode API set #[derive(Clone)] @@ -66,6 +67,7 @@ impl LeetCode { } /// Get category problems + #[named] pub async fn get_category_problems(self, category: &str) -> Result { trace!("Requesting {} problems...", &category); let url = &self @@ -81,13 +83,14 @@ impl LeetCode { info: false, json: None, mode: Mode::Get, - name: "get_category_problems", + name: function_name!(), url: url.to_string(), } .send(&self.client) .await } + #[named] pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { trace!("Requesting {} ref problems...", &slug); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; @@ -96,16 +99,13 @@ impl LeetCode { json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); json.insert( "query", - vec![ - "query getTopicTag($slug: String!) {", - " topicTag(slug: $slug) {", - " questions {", - " questionId", - " }", - " }", - "}", - ] - .join("\n"), + "query getTopicTag($slug: String!) { + topicTag(slug: $slug) { + questions { + questionId + } + } + }".to_owned() ); Req { @@ -114,13 +114,14 @@ impl LeetCode { info: false, json: Some(json), mode: Mode::Post, - name: "get_question_ids_by_tag", + name: function_name!(), url: (*url).to_string(), } .send(&self.client) .await } + #[named] pub async fn get_user_info(self) -> Result { trace!("Requesting user info..."); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; @@ -142,7 +143,7 @@ impl LeetCode { info: false, json: Some(json), mode: Mode::Post, - name: "get_user_info", + name: function_name!(), url: (*url).to_string(), } .send(&self.client) @@ -150,6 +151,7 @@ impl LeetCode { } /// Get daily problem + #[named] pub async fn get_question_daily(self) -> Result { trace!("Requesting daily problem..."); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; @@ -157,16 +159,13 @@ impl LeetCode { json.insert("operationName", "daily".to_string()); json.insert( "query", - vec![ - "query daily {", - " activeDailyCodingChallengeQuestion {", - " question {", - " questionFrontendId", - " }", - " }", - "}", - ] - .join("\n"), + "query daily { + activeDailyCodingChallengeQuestion { + question { + questionFrontendId + } + } + }".to_owned() ); Req { @@ -175,7 +174,7 @@ impl LeetCode { info: false, json: Some(json), mode: Mode::Post, - name: "get_question_daily", + name: function_name!(), url: (*url).to_string(), } .send(&self.client) @@ -183,27 +182,25 @@ impl LeetCode { } /// Get specific problem detail + #[named] pub async fn get_question_detail(self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); let refer = self.conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", slug); let mut json: Json = HashMap::new(); json.insert( "query", - vec![ - "query getQuestionDetail($titleSlug: String!) {", - " question(titleSlug: $titleSlug) {", - " content", - " stats", - " codeDefinition", - " sampleTestCase", - " exampleTestcases", - " enableRunCode", - " metaData", - " translatedContent", - " }", - "}", - ] - .join("\n"), + "query getQuestionDetail($titleSlug: String!) { + question(titleSlug: $titleSlug) { + content + stats + codeDefinition + sampleTestCase + exampleTestcases + enableRunCode + metaData + translatedContent + } + }".to_owned() ); json.insert( @@ -219,7 +216,7 @@ impl LeetCode { info: false, json: Some(json), mode: Mode::Post, - name: "get_problem_detail", + name: function_name!(), url: (&self.conf.sys.urls["graphql"]).to_string(), } .send(&self.client) @@ -227,6 +224,7 @@ impl LeetCode { } /// Send code to judge + #[named] pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { info!("Sending code to judge..."); Req { @@ -235,7 +233,7 @@ impl LeetCode { info: false, json: Some(j), mode: Mode::Post, - name: "run_code", + name: function_name!(), url, } .send(&self.client) @@ -243,6 +241,7 @@ impl LeetCode { } /// Get the result of submission / testing + #[named] pub async fn verify_result(self, id: String) -> Result { trace!("Verifying result..."); let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); @@ -252,7 +251,7 @@ impl LeetCode { info: false, json: None, mode: Mode::Get, - name: "verify_result", + name: function_name!(), url, } .send(&self.client) From d7114f73abf491fb1fc0e70ea61f109fea9c471d Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 13:47:04 +0800 Subject: [PATCH 11/38] function_name --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 85699e8..4691a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" toml = "0.5.5" regex = "1" +function_name = "0.3" [dependencies.diesel] version = "1.4.3" From b623957b1aba860cac7f155ad796f38ee9b85e9f Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 14:01:40 +0800 Subject: [PATCH 12/38] Req.info is a useless field --- src/plugins/leetcode.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 41ee789..00e203c 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -80,7 +80,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - info: false, json: None, mode: Mode::Get, name: function_name!(), @@ -111,7 +110,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)), - info: false, json: Some(json), mode: Mode::Post, name: function_name!(), @@ -140,7 +138,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - info: false, json: Some(json), mode: Mode::Post, name: function_name!(), @@ -171,7 +168,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - info: false, json: Some(json), mode: Mode::Post, name: function_name!(), @@ -213,7 +209,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some(refer), - info: false, json: Some(json), mode: Mode::Post, name: function_name!(), @@ -230,7 +225,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some(refer), - info: false, json: Some(j), mode: Mode::Post, name: function_name!(), @@ -248,7 +242,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - info: false, json: None, mode: Mode::Get, name: function_name!(), @@ -280,7 +273,6 @@ mod req { pub default_headers: HeaderMap, pub refer: Option, pub json: Option, - pub info: bool, pub mode: Mode, pub name: &'static str, pub url: String, @@ -289,9 +281,6 @@ mod req { impl Req { pub async fn send(self, client: &Client) -> Result { trace!("Running leetcode::{}...", &self.name); - if self.info { - info!("{}", &self.name); - } let url = self.url.to_owned(); let headers = LeetCode::headers( self.default_headers, From c7c85ffc11117e640cea9113b92f86ae41c48eb4 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 14:10:46 +0800 Subject: [PATCH 13/38] merge `Req.mode` and `Req.json` into a single enum --- src/plugins/leetcode.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 00e203c..f1d9d52 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -80,7 +80,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - json: None, mode: Mode::Get, name: function_name!(), url: url.to_string(), @@ -110,8 +109,7 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)), - json: Some(json), - mode: Mode::Post, + mode: Mode::Post(json), name: function_name!(), url: (*url).to_string(), } @@ -138,8 +136,7 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - json: Some(json), - mode: Mode::Post, + mode: Mode::Post(json), name: function_name!(), url: (*url).to_string(), } @@ -168,8 +165,7 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - json: Some(json), - mode: Mode::Post, + mode: Mode::Post(json), name: function_name!(), url: (*url).to_string(), } @@ -209,8 +205,7 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some(refer), - json: Some(json), - mode: Mode::Post, + mode: Mode::Post(json), name: function_name!(), url: (&self.conf.sys.urls["graphql"]).to_string(), } @@ -225,8 +220,7 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: Some(refer), - json: Some(j), - mode: Mode::Post, + mode: Mode::Post(j), name: function_name!(), url, } @@ -242,7 +236,6 @@ impl LeetCode { Req { default_headers: self.default_headers, refer: None, - json: None, mode: Mode::Get, name: function_name!(), url, @@ -265,14 +258,13 @@ mod req { /// Standardize request mode pub enum Mode { Get, - Post, + Post(Json), } /// LeetCode request prototype pub struct Req { pub default_headers: HeaderMap, pub refer: Option, - pub json: Option, pub mode: Mode, pub name: &'static str, pub url: String, @@ -289,7 +281,7 @@ mod req { let req = match self.mode { Mode::Get => client.get(&self.url), - Mode::Post => client.post(&self.url).json(&self.json), + Mode::Post(ref json) => client.post(&self.url).json(json), }; Ok(req.headers(headers).send().await?) From b541443a6d54558ea81e9b14df7d6975d73a2c5d Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 14:56:13 +0800 Subject: [PATCH 14/38] simplify Req construction --- Cargo.toml | 1 + src/plugins/leetcode.rs | 84 ++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4691a7d..b24f6ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ serde_json = "1.0.44" toml = "0.5.5" regex = "1" function_name = "0.3" +derive-new = "0.5" [dependencies.diesel] version = "1.4.3" diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index f1d9d52..e6cfccb 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -19,6 +19,12 @@ pub struct LeetCode { default_headers: HeaderMap, } +macro_rules! make_req { + ($self:ident, $url:expr) => { + Req::new($self.default_headers, function_name!(), $url) + } +} + impl LeetCode { /// Parse reqwest headers fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result { @@ -77,13 +83,7 @@ impl LeetCode { .get("problems").ok_or(Error::NoneError)? .replace("$category", category); - Req { - default_headers: self.default_headers, - refer: None, - mode: Mode::Get, - name: function_name!(), - url: url.to_string(), - } + make_req!(self, url.to_string()) .send(&self.client) .await } @@ -106,13 +106,10 @@ impl LeetCode { }".to_owned() ); - Req { - default_headers: self.default_headers, - refer: Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)), - mode: Mode::Post(json), - name: function_name!(), - url: (*url).to_string(), - } + let mut req = make_req!(self, url.to_string()); + req.mode = Mode::Post(json); + req.refer = Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)); + req .send(&self.client) .await } @@ -133,13 +130,9 @@ impl LeetCode { }".to_owned() ); - Req { - default_headers: self.default_headers, - refer: None, - mode: Mode::Post(json), - name: function_name!(), - url: (*url).to_string(), - } + let mut req = make_req!(self, url.to_string()); + req.mode = Mode::Post(json); + req .send(&self.client) .await } @@ -162,13 +155,9 @@ impl LeetCode { }".to_owned() ); - Req { - default_headers: self.default_headers, - refer: None, - mode: Mode::Post(json), - name: function_name!(), - url: (*url).to_string(), - } + let mut req = make_req!(self, url.to_string()); + req.mode = Mode::Post(json); + req .send(&self.client) .await } @@ -202,13 +191,11 @@ impl LeetCode { json.insert("operationName", "getQuestionDetail".to_string()); - Req { - default_headers: self.default_headers, - refer: Some(refer), - mode: Mode::Post(json), - name: function_name!(), - url: (&self.conf.sys.urls["graphql"]).to_string(), - } + let mut req = make_req!(self, + (&self.conf.sys.urls["graphql"]).to_string()); + req.mode = Mode::Post(json); + req.refer = Some(refer); + req .send(&self.client) .await } @@ -217,13 +204,10 @@ impl LeetCode { #[named] pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { info!("Sending code to judge..."); - Req { - default_headers: self.default_headers, - refer: Some(refer), - mode: Mode::Post(j), - name: function_name!(), - url, - } + let mut req = make_req!(self, url); + req.mode = Mode::Post(j); + req.refer = Some(refer); + req .send(&self.client) .await } @@ -233,13 +217,7 @@ impl LeetCode { pub async fn verify_result(self, id: String) -> Result { trace!("Verifying result..."); let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); - Req { - default_headers: self.default_headers, - refer: None, - mode: Mode::Get, - name: function_name!(), - url, - } + make_req!(self, url) .send(&self.client) .await } @@ -251,6 +229,7 @@ mod req { use crate::err::Error; use reqwest::{header::HeaderMap, Client, Response}; use std::collections::HashMap; + use derive_new::new; /// Standardize json format pub type Json = HashMap<&'static str, String>; @@ -262,12 +241,15 @@ mod req { } /// LeetCode request prototype + #[derive(new)] pub struct Req { pub default_headers: HeaderMap, - pub refer: Option, - pub mode: Mode, pub name: &'static str, pub url: String, + #[new(value = "Mode::Get")] + pub mode: Mode, + #[new(default)] + pub refer: Option, } impl Req { From b3937d59b48f0c5a1ab0ccdc1eb8e8b94639a5ea Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 15:01:32 +0800 Subject: [PATCH 15/38] remove `trace!`/`info!` calls that are effectively duplicates of other trace/info calls --- src/plugins/leetcode.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index e6cfccb..108f0dc 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -116,7 +116,6 @@ impl LeetCode { #[named] pub async fn get_user_info(self) -> Result { - trace!("Requesting user info..."); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); json.insert("operationName", "a".to_string()); @@ -140,7 +139,6 @@ impl LeetCode { /// Get daily problem #[named] pub async fn get_question_daily(self) -> Result { - trace!("Requesting daily problem..."); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); json.insert("operationName", "daily".to_string()); @@ -167,6 +165,7 @@ impl LeetCode { pub async fn get_question_detail(self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); let refer = self.conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", slug); + let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); json.insert( "query", @@ -191,8 +190,7 @@ impl LeetCode { json.insert("operationName", "getQuestionDetail".to_string()); - let mut req = make_req!(self, - (&self.conf.sys.urls["graphql"]).to_string()); + let mut req = make_req!(self, url.to_string()); req.mode = Mode::Post(json); req.refer = Some(refer); req @@ -203,7 +201,6 @@ impl LeetCode { /// Send code to judge #[named] pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { - info!("Sending code to judge..."); let mut req = make_req!(self, url); req.mode = Mode::Post(j); req.refer = Some(refer); @@ -215,7 +212,6 @@ impl LeetCode { /// Get the result of submission / testing #[named] pub async fn verify_result(self, id: String) -> Result { - trace!("Verifying result..."); let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); make_req!(self, url) .send(&self.client) From 500894d39150f3ece5933ae7fb7104a3df7e40ab Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月25日 15:08:50 +0800 Subject: [PATCH 16/38] refactor `Leetcode` methods to take `&self`; remove unnecessary `.clone()`s --- src/cache/mod.rs | 12 +++--------- src/plugins/leetcode.rs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 7b9662d..1fb5152 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -62,7 +62,7 @@ impl Cache { async fn get_user_info(&self) -> Result<(string,bool), Error> { let user = parser::user( - self.clone().0 + self.0 .get_user_info().await? .json().await? ); @@ -92,7 +92,6 @@ impl Cache { for i in &self.0.conf.sys.categories.to_owned() { let json = self .0 - .clone() .get_category_problems(i) .await? .json() // does not require LEETCODE_SESSION @@ -122,8 +121,7 @@ impl Cache { /// Get daily problem pub async fn get_daily_problem_id(&self) -> Result { parser::daily( - self.clone() - .0 + self.0 .get_question_daily() .await? .json() // does not require LEETCODE_SESSION @@ -167,7 +165,6 @@ impl Cache { } else { let json: Value = self .0 - .clone() .get_question_detail(&target.slug) .await? .json() @@ -205,8 +202,7 @@ impl Cache { ids = serde_json::from_str(&t.refs)?; } else { ids = parser::tags( - self.clone() - .0 + self.0 .get_question_ids_by_tag(rslug) .await? .json() @@ -307,7 +303,6 @@ impl Cache { let json: VerifyResult = self.resp_to_json( self - .clone() .0 .verify_result(rid.clone()) .await? @@ -329,7 +324,6 @@ impl Cache { let run_res: RunCode = self .0 - .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? .json() // does not require LEETCODE_SESSION (very oddly) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 108f0dc..3d11b0f 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -21,7 +21,7 @@ pub struct LeetCode { macro_rules! make_req { ($self:ident, $url:expr) => { - Req::new($self.default_headers, function_name!(), $url) + Req::new($self.default_headers.to_owned(), function_name!(), $url) } } @@ -74,7 +74,7 @@ impl LeetCode { /// Get category problems #[named] - pub async fn get_category_problems(self, category: &str) -> Result { + pub async fn get_category_problems(&self, category: &str) -> Result { trace!("Requesting {} problems...", &category); let url = &self .conf @@ -89,7 +89,7 @@ impl LeetCode { } #[named] - pub async fn get_question_ids_by_tag(self, slug: &str) -> Result { + pub async fn get_question_ids_by_tag(&self, slug: &str) -> Result { trace!("Requesting {} ref problems...", &slug); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); @@ -115,7 +115,7 @@ impl LeetCode { } #[named] - pub async fn get_user_info(self) -> Result { + pub async fn get_user_info(&self) -> Result { let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); json.insert("operationName", "a".to_string()); @@ -138,7 +138,7 @@ impl LeetCode { /// Get daily problem #[named] - pub async fn get_question_daily(self) -> Result { + pub async fn get_question_daily(&self) -> Result { let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); json.insert("operationName", "daily".to_string()); @@ -162,7 +162,7 @@ impl LeetCode { /// Get specific problem detail #[named] - pub async fn get_question_detail(self, slug: &str) -> Result { + pub async fn get_question_detail(&self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); let refer = self.conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", slug); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; @@ -200,7 +200,7 @@ impl LeetCode { /// Send code to judge #[named] - pub async fn run_code(self, j: Json, url: String, refer: String) -> Result { + pub async fn run_code(&self, j: Json, url: String, refer: String) -> Result { let mut req = make_req!(self, url); req.mode = Mode::Post(j); req.refer = Some(refer); @@ -211,7 +211,7 @@ impl LeetCode { /// Get the result of submission / testing #[named] - pub async fn verify_result(self, id: String) -> Result { + pub async fn verify_result(&self, id: String) -> Result { let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); make_req!(self, url) .send(&self.client) From d757aff2e44f058b24395a6375fc890284572e45 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月26日 09:45:37 +0800 Subject: [PATCH 17/38] put back the info!() call I accidentally removed --- src/plugins/leetcode.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 3d11b0f..f0d1fde 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -114,6 +114,7 @@ impl LeetCode { .await } + /// Get user info #[named] pub async fn get_user_info(&self) -> Result { let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; @@ -201,6 +202,7 @@ impl LeetCode { /// Send code to judge #[named] pub async fn run_code(&self, j: Json, url: String, refer: String) -> Result { + info!("Sending code to judge..."); let mut req = make_req!(self, url); req.mode = Mode::Post(j); req.refer = Some(refer); From cecf5714724f8bfe3c5f097c04a77112d24fa982 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:39:39 +0800 Subject: [PATCH 18/38] add ContestNode and ContestQuestionNode types to models.rs --- src/cache/models.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/cache/models.rs b/src/cache/models.rs index 2624233..ddc5200 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -13,6 +13,53 @@ pub struct Tag { pub refs: String, } + +// TODO: figure out how to put these things into db +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ContestQuestionStub { + pub question_id: i32, + pub credit: i32, + pub title: String, + pub title_slug: String, +} +/// Contest model +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Contest { + pub id: i32, + pub duration: i32, + pub start_time: i64, + pub title: String, + pub title_slug: String, + pub description: String, + pub is_virtual: bool, + pub contains_premium: bool, + pub registered: bool, + pub questions: Vec, +} +// TODO: improve Display for Contest* +impl std::fmt::Display for ContestQuestionStub { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:5} |{:5} | {}", + self.question_id.to_string().bold(), self.credit, self.title)?; + Ok(()) + } +} +impl std::fmt::Display for Contest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "[{}] {}", + self.title_slug.dimmed(), + self.title)?; + if !self.questions.is_empty() { + writeln!(f,"fID Points Title")?; + writeln!(f,"------|------|----------------------")?; + for q in &self.questions { + writeln!(f,"{}", q)?; + } + } + Ok(()) + } +} + /// Problem model #[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Serialize, Debug)] #[table_name = "problems"] @@ -160,7 +207,7 @@ mod question { #[serde(alias = "totalSubmissionRaw")] tsmr: i32, #[serde(alias = "acRate")] - rate: String, + pub rate: String, // TODO: remove this pub } /// Algorithm metadata From 78fbba606a94a8a91b77fbaa5cfbc67709774569 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:40:45 +0800 Subject: [PATCH 19/38] add a parser for Contest type --- src/cache/parser.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/cache/parser.rs b/src/cache/parser.rs index fb9ad2b..e9aaf25 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -2,6 +2,30 @@ use super::models::*; use serde_json::Value; +/// contest parser +pub fn contest(v: Value) -> Option { + let o = v.as_object()?; + let contest = o.get("contest")?.as_object()?; + let questions: Vec = o + .get("questions")?.as_array()? + .into_iter().map(|q| { + let stub: Result = serde_json::from_value(q.clone()); + stub.unwrap() + }).collect(); + Some(Contest { + id: contest.get("id")?.as_i64()? as i32, + duration: contest.get("duration")?.as_i64()? as i32, + start_time: contest.get("start_time")?.as_i64()?, + title: contest.get("title")?.as_str()?.to_string(), + title_slug: contest.get("title_slug")?.as_str()?.to_owned(), + description: "".to_owned(), // TODO: display description. contest.get("description")?.as_str()?.to_owned(), + is_virtual: contest.get("is_virtual")?.as_bool()?, + contains_premium: o.get("containsPremium")?.as_bool()?, + registered: o.get("registered")?.as_bool()?, + questions + }) +} + /// problem parser pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { let pairs = v.get("stat_status_pairs")?.as_array()?; From 12af5fdddd8a8a25fee331d2bd1d803f6e75695a Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:42:12 +0800 Subject: [PATCH 20/38] add stubs for new commands `contest` and `fun` (which may or may not be changed/removed) --- src/cli.rs | 6 +++++- src/cmds/mod.rs | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index df86b7b..9da63fc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use crate::{ cmds::{ Command, DataCommand, EditCommand, ExecCommand, ListCommand, PickCommand, StatCommand, - TestCommand, + TestCommand, ContestCommand, FunCommand }, err::Error, flag::{Debug, Flag}, @@ -36,6 +36,8 @@ pub async fn main() -> Result<(), Error> { PickCommand::usage().display_order(5), StatCommand::usage().display_order(6), TestCommand::usage().display_order(7), + ContestCommand::usage().display_order(8), + FunCommand::usage().display_order(9), ]) .arg(Debug::usage()) .setting(AppSettings::ArgRequiredElseHelp) @@ -57,6 +59,8 @@ pub async fn main() -> Result<(), Error> { ("pick", Some(sub_m)) => Ok(PickCommand::handler(sub_m).await?), ("stat", Some(sub_m)) => Ok(StatCommand::handler(sub_m).await?), ("test", Some(sub_m)) => Ok(TestCommand::handler(sub_m).await?), + ("contest", Some(sub_m)) => Ok(ContestCommand::handler(sub_m).await?), + ("fun", Some(sub_m)) => Ok(FunCommand::handler(sub_m).await?), _ => Err(Error::MatchError), } } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 224d3e0..d420bf0 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -31,6 +31,8 @@ mod list; mod pick; mod stat; mod test; +mod contest; +mod fun; pub use data::DataCommand; pub use edit::EditCommand; pub use exec::ExecCommand; @@ -38,3 +40,5 @@ pub use list::ListCommand; pub use pick::PickCommand; pub use stat::StatCommand; pub use test::TestCommand; +pub use contest::ContestCommand; +pub use fun::FunCommand; From 07aefe9f04a8173c4cb37f5a9ef49fd6bce6f8c9 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:43:46 +0800 Subject: [PATCH 21/38] add TODO for get_question_tags_by_id (should probably use .get_graphql) --- src/plugins/leetcode.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index f0d1fde..5a52e3f 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -88,16 +88,17 @@ impl LeetCode { .await } + /// TODO: check if refer is necessary #[named] pub async fn get_question_ids_by_tag(&self, slug: &str) -> Result { trace!("Requesting {} ref problems...", &slug); let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; let mut json: Json = HashMap::new(); - json.insert("operationName", "getTopicTag".to_string()); + json.insert("operationName", "a".to_string()); json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); json.insert( "query", - "query getTopicTag($slug: String!) { + "query a($slug: String!) { topicTag(slug: $slug) { questions { questionId From 4c581491a1c2f2182264edfbbd5cddf29d22dedc Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:51:53 +0800 Subject: [PATCH 22/38] add a generic graphql query function --- src/plugins/leetcode.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 5a52e3f..4a54a96 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -72,6 +72,26 @@ impl LeetCode { }) } + /// Generic GraphQL query + #[named] + pub async fn get_graphql(&self, query: String, variables: Option) -> Result { + let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; + let refer = self.conf.sys.urls.get("base").ok_or(Error::NoneError)?; + let mut json: Json = HashMap::new(); + json.insert("operationName", "a".to_string()); + if let Some(v) = variables { + json.insert("variables", v); + } + json.insert("query", query); + + let mut req = make_req!(self, url.to_string()); + req.mode = Mode::Post(json); + req.refer = Some(refer.to_string()); + req + .send(&self.client) + .await + } + /// Get category problems #[named] pub async fn get_category_problems(&self, category: &str) -> Result { From 98885a0f60b0412fd4b08711fd6de9062b5ac552 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:54:57 +0800 Subject: [PATCH 23/38] deduplicate code using `.get_graphql()` --- src/plugins/leetcode.rs | 59 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 4a54a96..0991839 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -136,52 +136,31 @@ impl LeetCode { } /// Get user info - #[named] pub async fn get_user_info(&self) -> Result { - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; - let mut json: Json = HashMap::new(); - json.insert("operationName", "a".to_string()); - json.insert( - "query", - "query a { - user { - username - isCurrentUserPremium - } - }".to_owned() - ); - - let mut req = make_req!(self, url.to_string()); - req.mode = Mode::Post(json); - req - .send(&self.client) - .await + self.get_graphql("query a { + user { + username + isCurrentUserPremium + } + }".to_owned(), + None).await } /// Get daily problem - #[named] pub async fn get_question_daily(&self) -> Result { - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; - let mut json: Json = HashMap::new(); - json.insert("operationName", "daily".to_string()); - json.insert( - "query", - "query daily { - activeDailyCodingChallengeQuestion { - question { - questionFrontendId - } + trace!("Requesting daily problem..."); + self.get_graphql( + "query a { + activeDailyCodingChallengeQuestion { + question { + questionFrontendId } - }".to_owned() - ); - - let mut req = make_req!(self, url.to_string()); - req.mode = Mode::Post(json); - req - .send(&self.client) - .await + } + }".to_owned(), None + ).await } + //TODO: check if refer is necessary /// Get specific problem detail #[named] pub async fn get_question_detail(&self, slug: &str) -> Result { @@ -191,7 +170,7 @@ impl LeetCode { let mut json: Json = HashMap::new(); json.insert( "query", - "query getQuestionDetail($titleSlug: String!) { + "query a($titleSlug: String!) { question(titleSlug: $titleSlug) { content stats @@ -210,7 +189,7 @@ impl LeetCode { r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug), ); - json.insert("operationName", "getQuestionDetail".to_string()); + json.insert("operationName", "a".to_string()); let mut req = make_req!(self, url.to_string()); req.mode = Mode::Post(json); From 9102d6c6830fa1ff5295c1717b2ff8d6a0feb13e Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:56:28 +0800 Subject: [PATCH 24/38] add a minor test for parsing ContestQuestionNode --- tests/de.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/de.rs b/tests/de.rs index 06d30df..88fefe7 100644 --- a/tests/de.rs +++ b/tests/de.rs @@ -1,5 +1,11 @@ -use leetcode_cli::cache::models::VerifyResult; + +// TODO: add a lot more tests +use leetcode_cli::cache::models::{ + VerifyResult, ContestQuestionStub, Contest +}; use serde_json; +use serde_json::Error as SerdeError; +type SerdeResult = Result; #[test] fn de_from_test_success() { @@ -80,3 +86,17 @@ fn de_unknown_error() { ); assert!(r.is_ok()); } + +#[test] +fn de_from_contest_question_stub() { + let r: SerdeResult = + serde_json::from_str(r#"{ + "id": 2701, + "question_id": 2368, + "credit": 6, + "title": "Sum of Total Strength of Wizards", + "title_slug": "sum-of-total-strength-of-wizards" + }"#); + assert!(r.is_ok()); +} + From b14ca1056063fcce2bf36db506494e32a0ab0818 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:56:54 +0800 Subject: [PATCH 25/38] add contest query methods to leetcode.rs --- src/plugins/leetcode.rs | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 0991839..463aaf3 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -199,6 +199,64 @@ impl LeetCode { .await } + /// Register for a contest + #[named] + pub async fn register_contest(&self, contest: &str) -> Result { + let url = self.conf.sys.urls.get("contest_register") + .ok_or(Error::NoneError)? + .replace("$contest_slug", contest); + let mut req = make_req!(self, url); + req.mode = Mode::Post(HashMap::new()); + req + .send(&self.client) + .await + } + + /// Get contest info + #[named] + pub async fn get_contest_info(&self, contest: &str) -> Result { + trace!("Requesting {} detail...", contest); + let url = &self.conf.sys.urls + .get("contest_info") + .ok_or(Error::NoneError)? + .replace("$contest_slug", contest); + make_req!(self, url.to_string()) + .send(&self.client) + .await + } + + /// Get contest problem detail + pub async fn get_contest_question_detail(&self, problem: &str) -> Result { + self.get_graphql("query a($s: String!) { + question(titleSlug: $s) { + title + titleSlug + questionId + questionFrontendId + categoryTitle + content + codeDefinition + status + metaData + codeSnippets { + langSlug + lang + code + } + exampleTestcases + sampleTestCase + enableRunCode + stats + translatedContent + isFavor + difficulty + } + }".to_owned(), Some( + r#"{"s": "$s"}"#.replace("$s", problem) + )).await + } + + /// Send code to judge #[named] pub async fn run_code(&self, j: Json, url: String, refer: String) -> Result { From 69cf80468a4c0ec9537ecc665f7679074d5c2908 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 20:57:46 +0800 Subject: [PATCH 26/38] add parser for contest questions (it is actually a parser for questions in general. will refactor later) --- src/cache/parser.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/cache/parser.rs b/src/cache/parser.rs index e9aaf25..62d986b 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -52,6 +52,36 @@ pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { Some(()) } +// TODO: implement test for this +/// graphql problem && question parser +pub fn graphql_problem_and_question(v: Value) -> Option<(problem,question)> { + let mut qn = Question::default(); + assert_eq!(Some(true), desc(&mut qn, v.clone())); + let percent = &qn.stats.rate; + let percent = percent[..percent.len()-1].parse::().ok()?; + let v = v.as_object()?.get("data")? + .as_object()?.get("question")? + .as_object()?; + Some((Problem { + category: v.get("categoryTitle")?.as_str()?.to_ascii_lowercase(), // dangerous, since this is not actually the slug. But currently (May 2022) ok + fid: v.get("questionFrontendId")?.as_str()?.parse().ok()?, + id: v.get("questionId")?.as_str()?.parse().ok()?, + level: match v.get("difficulty")?.as_str()?.chars().next()? { + 'E' => 1, + 'M' => 2, + 'H' => 3, + _ => 0, + }, + locked: false, // lazy + name: v.get("title")?.as_str()?.to_string(), + percent, + slug: v.get("titleSlug")?.as_str()?.to_string(), + starred: v.get("isFavor")?.as_bool()?, + status: v.get("status")?.as_str().unwrap_or("Null").to_owned(), + desc: serde_json::to_string(&qn).ok()?, + }, qn)) +} + /// desc parser pub fn desc(q: &mut Question, v: Value) -> Option { /* None - parsing failed From d5ee9bffe89502027877ed58e0688a26c1f19f8b Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:00:24 +0800 Subject: [PATCH 27/38] add contest/contestquestion handlers to cache mod --- src/cache/mod.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 1fb5152..fef9b52 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -106,10 +106,30 @@ impl Cache { Ok(ps) } + // TODO: get rid of this + pub fn push_problem(&self, p: Problem) -> Result<(), Error> { + diesel::replace_into(problems) + .values(&vec![p]) + .execute(&self.conn()?)?; + Ok(()) + } + + /// TODO: implement caching + /// Get contest + pub async fn get_contest(&self, contest: &str) -> Result { + let ctest = self.0 + .get_contest_info(contest) + .await? + .json() + .await?; + let ctest = parser::contest(ctest).ok_or(Error::NoneError)?; + Ok(ctest) + } + /// Get problem pub fn get_problem(&self, rfid: i32) -> Result { let p: Problem = problems.filter(fid.eq(rfid)).first(&self.conn()?)?; - if p.category != "algorithms" { + if p.category != "algorithms" && p.category != "contest" { return Err(Error::FeatureError( "Not support database and shell questions for now".to_string(), )); @@ -191,6 +211,17 @@ impl Cache { Ok(rdesc) } + // TODO: we can probably use this for all questions in general, actually + /// Get contest question + pub async fn get_contest_qnp(&self, problem: &str) -> Result<(problem,question), Error> { + let graphql_res = self.0 + .get_contest_question_detail(problem) + .await? + .json() + .await?; + parser::graphql_problem_and_question(graphql_res).ok_or(Error::NoneError) + } + pub async fn get_tagged_questions(self, rslug: &str) -> Result, Error> { trace!("Geting {} questions...", &rslug); let ids: Vec; @@ -231,6 +262,7 @@ impl Cache { run: Run, rfid: i32, testcase: Option, + contest: Option<&str> ) -> Result<(hashmap<&'static str, String>, [String; 2]), Error> { trace!("pre run code..."); use crate::helper::{code_path, test_cases_path}; @@ -277,11 +309,21 @@ impl Cache { json.insert("name", p.name.to_string()); json.insert("data_input", testcase); + // TODO: make this less ugly + let make_url = |s: &str| { + if let Some(c) = contest { + let s = format!("{}_contest", s); + conf.sys.urls.get(&s).map(|u| u.replace("$contest", &c)) + } else { + conf.sys.urls.get(s).map(|u| u.to_owned()) + }.ok_or(Error::NoneError) + }; + let url = match run { - Run::Test => conf.sys.urls.get("test").ok_or(Error::NoneError)?.replace("$slug", &p.slug), + Run::Test => make_url("test")?.replace("$slug", &p.slug), Run::Submit => { json.insert("judge_type", "large".to_string()); - conf.sys.urls.get("submit").ok_or(Error::NoneError)?.replace("$slug", &p.slug) + make_url("submit")?.replace("$slug", &p.slug) } }; @@ -317,9 +359,10 @@ impl Cache { rfid: i32, run: Run, testcase: Option, + contest: Option<&str> ) -> Result { trace!("Exec problem filter —— Test or Submit"); - let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, testcase).await?; + let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, testcase, contest).await?; trace!("Pre run code result {:?}, {:?}, {:?}", json, url, refer); let run_res: RunCode = self From 81df50626d5a567ef40716c7bb0092da1d8f1d5a Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:01:07 +0800 Subject: [PATCH 28/38] change exec/test commands to allow for contest problem submissions --- src/cmds/exec.rs | 8 +++++++- src/cmds/test.rs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index 755fd37..7994c28 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -36,6 +36,12 @@ impl Command for ExecCommand { .required(true) .help("question id"), ) + .arg( + Arg::with_name("contest") + .help("submit to contest") + .short("c") + .takes_value(true) + ) } /// `exec` handler @@ -44,7 +50,7 @@ impl Command for ExecCommand { let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let cache = Cache::new()?; - let res = cache.exec_problem(id, Run::Submit, None).await?; + let res = cache.exec_problem(id, Run::Submit, None, m.value_of("contest")).await?; println!("{}", res); Ok(()) diff --git a/src/cmds/test.rs b/src/cmds/test.rs index 37c0e7f..8c5798c 100644 --- a/src/cmds/test.rs +++ b/src/cmds/test.rs @@ -42,6 +42,12 @@ impl Command for TestCommand { .required(false) .help("custom testcase"), ) + .arg( + Arg::with_name("contest") + .help("submit to contest") + .short("c") + .takes_value(true) + ) } /// `test` handler @@ -55,7 +61,7 @@ impl Command for TestCommand { _ => case_str = None, } let cache = Cache::new()?; - let res = cache.exec_problem(id, Run::Test, case_str).await?; + let res = cache.exec_problem(id, Run::Test, case_str, m.value_of("contest")).await?; println!("{}", res); Ok(()) From e567b4687569707a83ed362308caff4d92c09606 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:01:31 +0800 Subject: [PATCH 29/38] Add `contest` command. --- src/cmds/contest.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/cmds/contest.rs diff --git a/src/cmds/contest.rs b/src/cmds/contest.rs new file mode 100644 index 0000000..6a124fb --- /dev/null +++ b/src/cmds/contest.rs @@ -0,0 +1,109 @@ +// TODO: clean this entire file +//! Pick command +use super::Command; +use crate::err::Error; +use async_trait::async_trait; +use clap::{App, Arg, ArgMatches, SubCommand}; + +/// TODO: put the actual correct docstring here +/// ``` +pub struct ContestCommand; + +// TODO: and here +static _QUERY_HELP: &str = r#"Fliter questions by conditions: +Uppercase means negative +e = easy E = m+h +m = medium M = e+h +h = hard H = e+m +d = done D = not done +l = locked L = not locked +s = starred S = not starred"#; + +/* + * +*/ + +fn time_diff_from_now(time_since_epoch: i64) -> i64 { + use chrono::{Utc,TimeZone}; + let now = Utc::now(); + let time = Utc.timestamp(time_since_epoch, 0); + let diff = now.signed_duration_since(time); + -diff.num_seconds() +} + +#[async_trait] +impl Command for ContestCommand { + /// `contest` usage + fn usage<'a, 'contest>() -> App<'a, 'contest> { + SubCommand::with_name("contest") + .about("Run a contest") + .visible_alias("c") + .arg( + Arg::with_name("title") + .help("Contest title (e.g. 'weekly-contest-999')") + .takes_value(true) + .required(true) + ).arg( + Arg::with_name("update") + .help("push contest problems into db") + .short("u") + ).arg( + Arg::with_name("register") + .help("register for contest") + .short("r") + ) + } + + /// `contest` handler + async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + use crate::cache::Cache; + use std::io::{stdout, Write}; + use std::thread::sleep; + use std::time::Duration; + + let cache = Cache::new()?; + let contest_slug = m.value_of("title").unwrap(); + let mut contest = cache.get_contest(contest_slug).await?; + debug!("{:#?}", contest); + if m.is_present("register") { + if contest.registered { + println!("You are already registered for this contest."); + } else { + println!("Registering for contest..."); + cache.0.register_contest(contest_slug).await?; + println!("Registered!"); + contest = cache.get_contest(contest_slug).await?; + } + } + + let tdiff = time_diff_from_now(contest.start_time); + if tdiff> 0 { + loop { + let tdiff = time_diff_from_now(contest.start_time); + if tdiff < 0 { break; } + print!("starts in {} seconds \r", tdiff); + stdout().flush().unwrap(); + sleep(Duration::from_secs(1)); + } + println!(""); + contest = cache.get_contest(contest_slug).await?; + } else { + println!("started {} seconds ago", -tdiff); + }; + + println!("{}", contest); + + for question_stub in contest.questions { + let slug = &question_stub.title_slug; + let (problem,_question) = cache.get_contest_qnp(slug).await?; + debug!("{:#?}", problem); + //println!("{:#?}", cache.get_problem(problem.fid)?); + debug!("----------------------------------"); + if m.is_present("update") { + cache.push_problem(problem)?; + } + } + + Ok(()) + } +} From 375004bf0b24601ed140219bc5a9a15e48c94d92 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:01:50 +0800 Subject: [PATCH 30/38] add debug fun command (will probably remove later) --- src/cmds/fun.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/cmds/fun.rs diff --git a/src/cmds/fun.rs b/src/cmds/fun.rs new file mode 100644 index 0000000..cd276bd --- /dev/null +++ b/src/cmds/fun.rs @@ -0,0 +1,118 @@ +// TODO: get rid of this debug command? clean it && make it permanent? + +//! Pick command +use super::Command; +use crate::err::Error; +use async_trait::async_trait; +use clap::{App, Arg, ArgMatches, SubCommand}; +/// Abstract contest command +/// +/// ```sh +/// leetcode-contest +/// Pick a problem +/// +/// USAGE: +/// leetcode contest [OPTIONS] [id] +/// +/// FLAGS: +/// -h, --help Prints help information +/// -V, --version Prints version information +/// +/// OPTIONS: +/// -q, --query Fliter questions by conditions: +/// Uppercase means negative +/// e = easy E = m+h +/// m = medium M = e+h +/// h = hard H = e+m +/// d = done D = not done +/// l = locked L = not locked +/// s = starred S = not starred +/// +/// ARGS: +/// Problem id +/// ``` +pub struct FunCommand; + +static _QUERY_HELP: &str = r#"Fliter questions by conditions: +Uppercase means negative +e = easy E = m+h +m = medium M = e+h +h = hard H = e+m +d = done D = not done +l = locked L = not locked +s = starred S = not starred"#; + +#[async_trait] +impl Command for FunCommand { + /// `contest` usage + fn usage<'a, 'contest>() -> App<'a, 'contest> { + SubCommand::with_name("fun") + .about("fun") + .visible_alias("f") + .arg( + Arg::with_name("query") + .help("GraphQL query - MUST be of the format `query a { ... }`") + .takes_value(true) + .short("q") + .conflicts_with("type") + .required(true) + ).arg( + Arg::with_name("variables") + .help("Variables to pass to the GraphQL query, e.g. `{'slug': 'two-sum'}`") + .takes_value(true) + .short("v") + .requires("query") + ).arg( + Arg::with_name("type") + .help("type to get the definition of, e.g. `ContestNode`") + .takes_value(true) + .short("t") + .required(true) + .conflicts_with("query") + ) + } + + /// `contest` handler + async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + use crate::cache::Cache; + + let cache = Cache::new()?; + let query = if let Some(q) = m.value_of("query") { q.to_string() } + else if let Some(t) = m.value_of("type"){ + "query a { + __type(name: \"$type\") { + name + fields { + name + args { + name + description + defaultValue + type { + name + kind + ofType { + name + kind + } + } + } + type { + name + kind + ofType { + name + kind + } + } + } + } + }".replace("$type", t) + } else { unreachable!() }; + let vars = m.value_of("variables") + .map(|v| v.to_string()); + println!("{}", cache.0.get_graphql(query, vars).await?.text().await?); + + Ok(()) + } +} From c02986ec51969d50285e889128ab10a81cbfce85 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:02:09 +0800 Subject: [PATCH 31/38] add chrono package for contest command --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index b24f6ce..42f5724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ toml = "0.5.5" regex = "1" function_name = "0.3" derive-new = "0.5" +chrono = "0.4" [dependencies.diesel] version = "1.4.3" From dcec6ffe76a593a6b0805108e55875c622a3b60f Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月30日 21:04:51 +0800 Subject: [PATCH 32/38] add necessary URLs to cfg.rs for contest command --- src/cfg.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cfg.rs b/src/cfg.rs index ba57d59..c629aea 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -46,13 +46,17 @@ problems = "https://leetcode.com/api/problems/$category/" problem = "https://leetcode.com/problems/$slug/description/" tag = "https://leetcode.com/tag/$slug/" test = "https://leetcode.com/problems/$slug/interpret_solution/" +test_contest = "https://leetcode.com/contest/api/$contest/problems/$slug/interpret_solution/" session = "https://leetcode.com/session/" submit = "https://leetcode.com/problems/$slug/submit/" +submit_contest = "https://leetcode.com/contest/api/$contest/problems/$slug/submit/" submissions = "https://leetcode.com/api/submissions/$slug" submission = "https://leetcode.com/submissions/detail/$id/" verify = "https://leetcode.com/submissions/detail/$id/check/" favorites = "https://leetcode.com/list/api/questions" favorite_delete = "https://leetcode.com/list/api/questions/$hash/$id" +contest_info = "https://leetcode.com/contest/api/info/$contest_slug" +contest_register = "https://leetcode.com/contest/api/$contest_slug/register" [code] editor = "vim" From b3eb0e293cf4062c00836f98b931f3d2729c3e25 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月31日 10:12:30 +0800 Subject: [PATCH 33/38] revert contest println formatting to previous working edition --- src/cache/models.rs | 14 -------------- src/cmds/contest.rs | 8 ++++++++ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/cache/models.rs b/src/cache/models.rs index ddc5200..f102f79 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -37,25 +37,11 @@ pub struct Contest { pub questions: Vec, } // TODO: improve Display for Contest* -impl std::fmt::Display for ContestQuestionStub { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:5} |{:5} | {}", - self.question_id.to_string().bold(), self.credit, self.title)?; - Ok(()) - } -} impl std::fmt::Display for Contest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "[{}] {}", self.title_slug.dimmed(), self.title)?; - if !self.questions.is_empty() { - writeln!(f,"fID Points Title")?; - writeln!(f,"------|------|----------------------")?; - for q in &self.questions { - writeln!(f,"{}", q)?; - } - } Ok(()) } } diff --git a/src/cmds/contest.rs b/src/cmds/contest.rs index 6a124fb..646b383 100644 --- a/src/cmds/contest.rs +++ b/src/cmds/contest.rs @@ -92,10 +92,18 @@ impl Command for ContestCommand { }; println!("{}", contest); + println!("fID Points Difficulty Title"); + println!("------|------|----------|--------------------"); for question_stub in contest.questions { let slug = &question_stub.title_slug; let (problem,_question) = cache.get_contest_qnp(slug).await?; + println!("{:5} |{:5} |{:9} |{}", + problem.fid, + question_stub.credit, + problem.level, + problem.name + ); debug!("{:#?}", problem); //println!("{:#?}", cache.get_problem(problem.fid)?); debug!("----------------------------------"); From b3d8cf5564846fb0058fd1998bf3ea5f5fb16046 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: 2022年5月31日 11:29:55 +0800 Subject: [PATCH 34/38] clippy patch --- src/cache/mod.rs | 2 +- src/cache/parser.rs | 2 +- src/cmds/contest.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index fef9b52..0ceee29 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -313,7 +313,7 @@ impl Cache { let make_url = |s: &str| { if let Some(c) = contest { let s = format!("{}_contest", s); - conf.sys.urls.get(&s).map(|u| u.replace("$contest", &c)) + conf.sys.urls.get(&s).map(|u| u.replace("$contest", c)) } else { conf.sys.urls.get(s).map(|u| u.to_owned()) }.ok_or(Error::NoneError) diff --git a/src/cache/parser.rs b/src/cache/parser.rs index 62d986b..8419eb5 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -8,7 +8,7 @@ pub fn contest(v: Value) -> Option { let contest = o.get("contest")?.as_object()?; let questions: Vec = o .get("questions")?.as_array()? - .into_iter().map(|q| { + .iter().map(|q| { let stub: Result = serde_json::from_value(q.clone()); stub.unwrap() }).collect(); diff --git a/src/cmds/contest.rs b/src/cmds/contest.rs index 646b383..67e6bc8 100644 --- a/src/cmds/contest.rs +++ b/src/cmds/contest.rs @@ -85,7 +85,7 @@ impl Command for ContestCommand { stdout().flush().unwrap(); sleep(Duration::from_secs(1)); } - println!(""); + println!(); contest = cache.get_contest(contest_slug).await?; } else { println!("started {} seconds ago", -tdiff); From eefa2f0b1020bd2e90d470277c936c09e1862247 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:29:05 +0800 Subject: [PATCH 35/38] clean up contest/fun commands code --- src/cmds/contest.rs | 44 +++++++++++++++++++++++++++----------------- src/cmds/fun.rs | 43 ++++--------------------------------------- 2 files changed, 31 insertions(+), 56 deletions(-) diff --git a/src/cmds/contest.rs b/src/cmds/contest.rs index 67e6bc8..6748fdd 100644 --- a/src/cmds/contest.rs +++ b/src/cmds/contest.rs @@ -1,27 +1,30 @@ -// TODO: clean this entire file -//! Pick command +//! Contest command (WIP) +/** TODO: + * Improve pretty printing of contest info + * (maybe) make a UI to play a full contest in + */ use super::Command; use crate::err::Error; use async_trait::async_trait; use clap::{App, Arg, ArgMatches, SubCommand}; -/// TODO: put the actual correct docstring here -/// ``` -pub struct ContestCommand; +/** +leetcode-contest +Run a contest + +USAGE: + leetcode contest [FLAGS] -// TODO: and here -static _QUERY_HELP: &str = r#"Fliter questions by conditions: -Uppercase means negative -e = easy E = m+h -m = medium M = e+h -h = hard H = e+m -d = done D = not done -l = locked L = not locked -s = starred S = not starred"#; +FLAGS: + -h, --help Prints help information + -r, --register register for contest + -u, --update push contest problems into db + -V, --version Prints version information -/* - * +ARGS: + <title> Contest title (e.g. 'weekly-contest-999') */ +pub struct ContestCommand; fn time_diff_from_now(time_since_epoch: i64) -> i64 { use chrono::{Utc,TimeZone}; @@ -47,10 +50,12 @@ impl Command for ContestCommand { Arg::with_name("update") .help("push contest problems into db") .short("u") + .long("update") ).arg( Arg::with_name("register") .help("register for contest") .short("r") + .long("register") ) } @@ -61,10 +66,13 @@ impl Command for ContestCommand { use std::thread::sleep; use std::time::Duration; + // get contest info let cache = Cache::new()?; let contest_slug = m.value_of("title").unwrap(); let mut contest = cache.get_contest(contest_slug).await?; debug!("{:#?}", contest); + + // if requested, register for contest && update contest info if m.is_present("register") { if contest.registered { println!("You are already registered for this contest."); @@ -76,6 +84,7 @@ impl Command for ContestCommand { } } + // if contest has not started, print a countdown let tdiff = time_diff_from_now(contest.start_time); if tdiff> 0 { loop { @@ -91,10 +100,12 @@ impl Command for ContestCommand { println!("started {} seconds ago", -tdiff); }; + // display contest header println!("{}", contest); println!("fID Points Difficulty Title"); println!("------|------|----------|--------------------"); + // get contest problems (pushing them to db if necessary), and display them for question_stub in contest.questions { let slug = &question_stub.title_slug; let (problem,_question) = cache.get_contest_qnp(slug).await?; @@ -105,7 +116,6 @@ impl Command for ContestCommand { problem.name ); debug!("{:#?}", problem); - //println!("{:#?}", cache.get_problem(problem.fid)?); debug!("----------------------------------"); if m.is_present("update") { cache.push_problem(problem)?; diff --git a/src/cmds/fun.rs b/src/cmds/fun.rs index cd276bd..b71dc4a 100644 --- a/src/cmds/fun.rs +++ b/src/cmds/fun.rs @@ -1,51 +1,16 @@ // TODO: get rid of this debug command? clean it && make it permanent? -//! Pick command +//! Fun command use super::Command; use crate::err::Error; use async_trait::async_trait; use clap::{App, Arg, ArgMatches, SubCommand}; -/// Abstract contest command -/// -/// ```sh -/// leetcode-contest -/// Pick a problem -/// -/// USAGE: -/// leetcode contest [OPTIONS] [id] -/// -/// FLAGS: -/// -h, --help Prints help information -/// -V, --version Prints version information -/// -/// OPTIONS: -/// -q, --query <query> Fliter questions by conditions: -/// Uppercase means negative -/// e = easy E = m+h -/// m = medium M = e+h -/// h = hard H = e+m -/// d = done D = not done -/// l = locked L = not locked -/// s = starred S = not starred -/// -/// ARGS: -/// <id> Problem id -/// ``` pub struct FunCommand; -static _QUERY_HELP: &str = r#"Fliter questions by conditions: -Uppercase means negative -e = easy E = m+h -m = medium M = e+h -h = hard H = e+m -d = done D = not done -l = locked L = not locked -s = starred S = not starred"#; - #[async_trait] impl Command for FunCommand { - /// `contest` usage - fn usage<'a, 'contest>() -> App<'a, 'contest> { + /// `fun` usage + fn usage<'a, 'fun>() -> App<'a, 'fun> { SubCommand::with_name("fun") .about("fun") .visible_alias("f") @@ -72,7 +37,7 @@ impl Command for FunCommand { ) } - /// `contest` handler + /// `fun` handler async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { use crate::cache::Cache; From 1d32cfc81fe6e7d06716b72d1860d64464652b49 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: Sun, 5 Jun 2022 10:16:34 +0800 Subject: [PATCH 36/38] use serde_json to autoparse where possible --- src/cache/models.rs | 39 +++++++++++++++++++++-- src/cache/parser.rs | 69 +++++++++++++++++------------------------ src/plugins/leetcode.rs | 2 ++ 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/cache/models.rs b/src/cache/models.rs index f102f79..118dbb4 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -23,17 +23,22 @@ pub struct ContestQuestionStub { pub title_slug: String, } /// Contest model -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] pub struct Contest { pub id: i32, pub duration: i32, pub start_time: i64, pub title: String, pub title_slug: String, + #[serde(skip)] // the description is not used at the moment, + #[serde(default)] // so we use an empty string instead pub description: String, pub is_virtual: bool, + #[serde(skip)] pub contains_premium: bool, + #[serde(skip)] pub registered: bool, + #[serde(skip)] pub questions: Vec<contestquestionstub>, } // TODO: improve Display for Contest* @@ -46,20 +51,50 @@ impl std::fmt::Display for Contest { } } +fn deserialize_string<'de,d,t>(deserializer: D) -> Result<t, D::Error> +where D: serde::Deserializer<'de>, + T: std::str::FromStr { + let s: String = Deserialize::deserialize(deserializer)?; + s.parse::<t>().map_err(|_| serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&s), + &"valid string" + )) +} + /// Problem model -#[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Serialize, Debug)] +#[derive(AsChangeset, Clone, Identifiable, Insertable, Queryable, Deserialize, Debug)] #[table_name = "problems"] pub struct Problem { + #[serde(alias="category_slug")] + #[serde(alias="categoryTitle")] pub category: String, + #[serde(alias="frontend_question_id")] + #[serde(alias="questionFrontendId")] + #[serde(deserialize_with="deserialize_string")] pub fid: i32, + #[serde(alias="question_id")] + #[serde(alias="questionId")] + #[serde(deserialize_with="deserialize_string")] pub id: i32, + #[serde(skip)] pub level: i32, + #[serde(alias="paid_only")] + #[serde(alias="isPaidOnly")] pub locked: bool, + #[serde(alias="question__title")] + #[serde(alias="title")] pub name: String, + #[serde(skip)] pub percent: f32, + #[serde(alias="question__title_slug")] + #[serde(alias="titleSlug")] pub slug: String, + #[serde(alias="is_favor")] + #[serde(alias="isFavor")] pub starred: bool, + #[serde(skip)] pub status: String, + #[serde(skip)] pub desc: String, } diff --git a/src/cache/parser.rs b/src/cache/parser.rs index 8419eb5..24ed739 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -1,29 +1,20 @@ //! Sub-Module for parsing resp data use super::models::*; -use serde_json::Value; +use serde_json::{Value,from_value}; + /// contest parser pub fn contest(v: Value) -> Option<contest> { let o = v.as_object()?; - let contest = o.get("contest")?.as_object()?; - let questions: Vec<contestquestionstub> = o - .get("questions")?.as_array()? - .iter().map(|q| { - let stub: Result<contestquestionstub, _> = serde_json::from_value(q.clone()); - stub.unwrap() - }).collect(); - Some(Contest { - id: contest.get("id")?.as_i64()? as i32, - duration: contest.get("duration")?.as_i64()? as i32, - start_time: contest.get("start_time")?.as_i64()?, - title: contest.get("title")?.as_str()?.to_string(), - title_slug: contest.get("title_slug")?.as_str()?.to_owned(), - description: "".to_owned(), // TODO: display description. contest.get("description")?.as_str()?.to_owned(), - is_virtual: contest.get("is_virtual")?.as_bool()?, - contains_premium: o.get("containsPremium")?.as_bool()?, - registered: o.get("registered")?.as_bool()?, - questions - }) + let mut contest: Contest = from_value( + o.get("contest")?.clone() + ).ok()?; + contest.questions = from_value( + o.get("questions")?.clone() + ).ok()?; + contest.contains_premium = o.get("containsPremium")?.as_bool()?; + contest.registered = o.get("registered")?.as_bool()?; + Some(contest) } /// problem parser @@ -55,31 +46,29 @@ pub fn problem(problems: &mut Vec<problem>, v: Value) -> Option<()> { // TODO: implement test for this /// graphql problem && question parser pub fn graphql_problem_and_question(v: Value) -> Option<(problem,question)> { + // parse top-level data from API let mut qn = Question::default(); assert_eq!(Some(true), desc(&mut qn, v.clone())); let percent = &qn.stats.rate; let percent = percent[..percent.len()-1].parse::<f32>().ok()?; + + // parse v.question specifically let v = v.as_object()?.get("data")? - .as_object()?.get("question")? - .as_object()?; - Some((Problem { - category: v.get("categoryTitle")?.as_str()?.to_ascii_lowercase(), // dangerous, since this is not actually the slug. But currently (May 2022) ok - fid: v.get("questionFrontendId")?.as_str()?.parse().ok()?, - id: v.get("questionId")?.as_str()?.parse().ok()?, - level: match v.get("difficulty")?.as_str()?.chars().next()? { - 'E' => 1, - 'M' => 2, - 'H' => 3, - _ => 0, - }, - locked: false, // lazy - name: v.get("title")?.as_str()?.to_string(), - percent, - slug: v.get("titleSlug")?.as_str()?.to_string(), - starred: v.get("isFavor")?.as_bool()?, - status: v.get("status")?.as_str().unwrap_or("Null").to_owned(), - desc: serde_json::to_string(&qn).ok()?, - }, qn)) + .as_object()?.get("question")?; + let mut p: Problem = from_value(v.clone()).unwrap(); + p.percent = percent; + p.level = match v.as_object()?.get("difficulty")?.as_str()?.chars().next()? { + 'E' => 1, + 'M' => 2, + 'H' => 3, + _ => 0, + }; + p.status = v.get("status")?.as_str().unwrap_or("Null").to_owned(); + p.desc = serde_json::to_string(&qn).ok()?; + /* The graphql API doesn't return the category slug, only the printed category title. */ + p.category = p.category.to_ascii_lowercase(); // Currently working (June 2022) + /* But lowercasing is stupid. This will break if a category with whitespaces appears. */ + Some((p,qn)) } /// desc parser diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 463aaf3..9da9bf8 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -216,6 +216,7 @@ impl LeetCode { #[named] pub async fn get_contest_info(&self, contest: &str) -> Result<response, Error> { trace!("Requesting {} detail...", contest); + // cannot use the graphql API here because it does not provide registration status let url = &self.conf.sys.urls .get("contest_info") .ok_or(Error::NoneError)? @@ -243,6 +244,7 @@ impl LeetCode { lang code } + isPaidOnly exampleTestcases sampleTestCase enableRunCode From 64962b17e13bfc33757be926d34f9aaacbd13731 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: Sun, 5 Jun 2022 20:29:30 +0800 Subject: [PATCH 37/38] merge `get_question_detail` and `get_contest_question_detail` --- src/cache/mod.rs | 6 +++--- src/plugins/leetcode.rs | 43 ++--------------------------------------- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 0ceee29..7133ab4 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -117,11 +117,12 @@ impl Cache { /// TODO: implement caching /// Get contest pub async fn get_contest(&self, contest: &str) -> Result<contest, Error> { - let ctest = self.0 + let ctest: Value = self.0 .get_contest_info(contest) .await? .json() .await?; + debug!("{:?}", ctest.to_string()); let ctest = parser::contest(ctest).ok_or(Error::NoneError)?; Ok(ctest) } @@ -211,11 +212,10 @@ impl Cache { Ok(rdesc) } - // TODO: we can probably use this for all questions in general, actually /// Get contest question pub async fn get_contest_qnp(&self, problem: &str) -> Result<(problem,question), Error> { let graphql_res = self.0 - .get_contest_question_detail(problem) + .get_question_detail(problem) .await? .json() .await?; diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 9da9bf8..f53c67b 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -160,45 +160,6 @@ impl LeetCode { ).await } - //TODO: check if refer is necessary - /// Get specific problem detail - #[named] - pub async fn get_question_detail(&self, slug: &str) -> Result<response, Error> { - trace!("Requesting {} detail...", &slug); - let refer = self.conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", slug); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; - let mut json: Json = HashMap::new(); - json.insert( - "query", - "query a($titleSlug: String!) { - question(titleSlug: $titleSlug) { - content - stats - codeDefinition - sampleTestCase - exampleTestcases - enableRunCode - metaData - translatedContent - } - }".to_owned() - ); - - json.insert( - "variables", - r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug), - ); - - json.insert("operationName", "a".to_string()); - - let mut req = make_req!(self, url.to_string()); - req.mode = Mode::Post(json); - req.refer = Some(refer); - req - .send(&self.client) - .await - } - /// Register for a contest #[named] pub async fn register_contest(&self, contest: &str) -> Result<response,error> { @@ -226,8 +187,8 @@ impl LeetCode { .await } - /// Get contest problem detail - pub async fn get_contest_question_detail(&self, problem: &str) -> Result<response,error> { + /// Get full question detail + pub async fn get_question_detail(&self, problem: &str) -> Result<response,error> { self.get_graphql("query a($s: String!) { question(titleSlug: $s) { title From 8c862bdbe0739f4c34ef56e4dfb44f0fcdc49744 Mon Sep 17 00:00:00 2001 From: 152334H <54623771+152334h@users.noreply.github.com> Date: Sun, 5 Jun 2022 20:34:33 +0800 Subject: [PATCH 38/38] use `get_graphql` more --- src/plugins/leetcode.rs | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index f53c67b..e7948dd 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -108,31 +108,16 @@ impl LeetCode { .await } - /// TODO: check if refer is necessary - #[named] pub async fn get_question_ids_by_tag(&self, slug: &str) -> Result<response, Error> { - trace!("Requesting {} ref problems...", &slug); - let url = &self.conf.sys.urls.get("graphql").ok_or(Error::NoneError)?; - let mut json: Json = HashMap::new(); - json.insert("operationName", "a".to_string()); - json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); - json.insert( - "query", - "query a($slug: String!) { - topicTag(slug: $slug) { - questions { - questionId - } - } - }".to_owned() - ); - - let mut req = make_req!(self, url.to_string()); - req.mode = Mode::Post(json); - req.refer = Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)); - req - .send(&self.client) - .await + self.get_graphql("query a { + topicTag(slug: \"$slug\") { + questions { + questionId + } + } + }" + .replace("$slug", slug), + None).await } /// Get user info </div><div class="naked_ctrl"> <form action="/index.cgi/contrast" method="get" name="gate"> <p><a href="http://altstyle.alfasado.net">AltStyle</a> によって変換されたページ <a href="https://patch-diff.githubusercontent.com/raw/clearloop/leetcode-cli/pull/69.patch">(->オリジナル)</a> / <label>アドレス: <input type="text" name="naked_post_url" value="https://patch-diff.githubusercontent.com/raw/clearloop/leetcode-cli/pull/69.patch" size="22" /></label> <label>モード: <select name="naked_post_mode"> <option value="default">デフォルト</option> <option value="speech">音声ブラウザ</option> <option value="ruby">ルビ付き</option> <option value="contrast" selected="selected">配色反転</option> <option value="larger-text">文字拡大</option> <option value="mobile">モバイル</option> </select> <input type="submit" value="表示" /> </p> </form> </div>