diff --git a/Cargo.toml b/Cargo.toml index 85699e8..42f5724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,9 @@ serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" toml = "0.5.5" regex = "1" +function_name = "0.3" +derive-new = "0.5" +chrono = "0.4" [dependencies.diesel] version = "1.4.3" diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 5e1309f..7133ab4 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? ); @@ -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 { @@ -96,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 @@ -111,10 +106,31 @@ 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: 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) + } + /// 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(), )); @@ -126,8 +142,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 @@ -171,7 +186,6 @@ impl Cache { } else { let json: Value = self .0 - .clone() .get_question_detail(&target.slug) .await? .json() @@ -198,6 +212,16 @@ impl Cache { Ok(rdesc) } + /// Get contest question + pub async fn get_contest_qnp(&self, problem: &str) -> Result<(problem,question), Error> { + let graphql_res = self.0 + .get_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; @@ -209,8 +233,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() @@ -239,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}; @@ -285,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) } }; @@ -311,7 +345,6 @@ impl Cache { let json: VerifyResult = self.resp_to_json( self - .clone() .0 .verify_result(rid.clone()) .await? @@ -326,14 +359,14 @@ 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 .0 - .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? .json() // does not require LEETCODE_SESSION (very oddly) diff --git a/src/cache/models.rs b/src/cache/models.rs index 2624233..118dbb4 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -13,20 +13,88 @@ 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, 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, +} +// TODO: improve Display for Contest* +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)?; + Ok(()) + } +} + +fn deserialize_string<'de,d,t>(deserializer: D) -> Result +where D: serde::Deserializer<'de>, + T: std::str::FromStr { + let s: String = Deserialize::deserialize(deserializer)?; + s.parse::().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, } @@ -160,7 +228,7 @@ mod question { #[serde(alias = "totalSubmissionRaw")] tsmr: i32, #[serde(alias = "acRate")] - rate: String, + pub rate: String, // TODO: remove this pub } /// Algorithm metadata diff --git a/src/cache/parser.rs b/src/cache/parser.rs index fb9ad2b..24ed739 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -1,6 +1,21 @@ //! 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 { + let o = v.as_object()?; + 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 pub fn problem(problems: &mut Vec, v: Value) -> Option<()> { @@ -28,6 +43,34 @@ 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)> { + // 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::().ok()?; + + // parse v.question specifically + let v = v.as_object()?.get("data")? + .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 pub fn desc(q: &mut Question, v: Value) -> Option { /* None - parsing failed 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" 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/contest.rs b/src/cmds/contest.rs new file mode 100644 index 0000000..6748fdd --- /dev/null +++ b/src/cmds/contest.rs @@ -0,0 +1,127 @@ +//! 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}; + +/** +leetcode-contest +Run a contest + +USAGE: + leetcode contest [FLAGS] + +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}; + 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") + .long("update") + ).arg( + Arg::with_name("register") + .help("register for contest") + .short("r") + .long("register") + ) + } + + /// `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; + + // 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."); + } else { + println!("Registering for contest..."); + cache.0.register_contest(contest_slug).await?; + println!("Registered!"); + contest = cache.get_contest(contest_slug).await?; + } + } + + // if contest has not started, print a countdown + 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); + }; + + // 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?; + println!("{:5} |{:5} |{:9} |{}", + problem.fid, + question_stub.credit, + problem.level, + problem.name + ); + debug!("{:#?}", problem); + debug!("----------------------------------"); + if m.is_present("update") { + cache.push_problem(problem)?; + } + } + + Ok(()) + } +} 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/fun.rs b/src/cmds/fun.rs new file mode 100644 index 0000000..b71dc4a --- /dev/null +++ b/src/cmds/fun.rs @@ -0,0 +1,83 @@ +// TODO: get rid of this debug command? clean it && make it permanent? + +//! Fun command +use super::Command; +use crate::err::Error; +use async_trait::async_trait; +use clap::{App, Arg, ArgMatches, SubCommand}; +pub struct FunCommand; + +#[async_trait] +impl Command for FunCommand { + /// `fun` usage + fn usage<'a, 'fun>() -> App<'a, 'fun> { + 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") + ) + } + + /// `fun` 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(()) + } +} 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; 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(()) diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 4394532..e7948dd 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)] @@ -18,6 +19,12 @@ pub struct LeetCode { default_headers: HeaderMap, } +macro_rules! make_req { + ($self:ident, $url:expr) => { + Req::new($self.default_headers.to_owned(), function_name!(), $url) + } +} + impl LeetCode { /// Parse reqwest headers fn headers(mut headers: HeaderMap, ts: Vec<(&str, &str)>) -> Result<head> <link rel="stylesheet" href="/common/contrast.css" type="text/css" media="all" /> <meta name="robots" content="noindex,nofollow"> <meta name="robots" content="none"> <meta name="robots" content="noarchive"> <meta name="Googlebot" content="noarchive"> { @@ -65,8 +72,29 @@ impl LeetCode { }) } + /// Generic GraphQL query + #[named] + pub async fn get_graphql(&self, query: String, variables: Option<string>) -> Result<response, Error> { + 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 - pub async fn get_category_problems(self, category: &str) -> Result<response, Error> { + #[named] + pub async fn get_category_problems(&self, category: &str) -> Result<response, Error> { trace!("Requesting {} problems...", &category); let url = &self .conf @@ -75,186 +103,125 @@ impl LeetCode { .get("problems").ok_or(Error::NoneError)? .replace("$category", category); - Req { - default_headers: self.default_headers, - refer: None, - info: false, - json: None, - mode: Mode::Get, - name: "get_category_problems", - url: url.to_string(), - } + make_req!(self, url.to_string()) .send(&self.client) .await } - 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", "getTopicTag".to_string()); - json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); - json.insert( - "query", - vec![ - "query getTopicTag($slug: String!) {", - " topicTag(slug: $slug) {", - " questions {", - " questionId", - " }", - " }", - "}", - ] - .join("\n"), - ); - - 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: "get_question_ids_by_tag", - url: (*url).to_string(), - } - .send(&self.client) - .await + pub async fn get_question_ids_by_tag(&self, slug: &str) -> Result<response, Error> { + self.get_graphql("query a { + topicTag(slug: \"$slug\") { + questions { + questionId + } + } + }" + .replace("$slug", slug), + None).await } - pub async fn get_user_info(self) -> Result<response, Error> { - 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 user info + pub async fn get_user_info(&self) -> Result<response, Error> { + self.get_graphql("query a { + user { + username + isCurrentUserPremium + } + }".to_owned(), + None).await } /// Get daily problem - pub async fn get_question_daily(self) -> Result<response, Error> { + pub async fn get_question_daily(&self) -> Result<response, Error> { 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()); - json.insert( - "query", - vec![ - "query daily {", - " activeDailyCodingChallengeQuestion {", - " question {", - " questionFrontendId", - " }", - " }", - "}", - ] - .join("\n"), - ); + self.get_graphql( + "query a { + activeDailyCodingChallengeQuestion { + question { + questionFrontendId + } + } + }".to_owned(), None + ).await + } - Req { - default_headers: self.default_headers, - refer: None, - info: false, - json: Some(json), - mode: Mode::Post, - name: "get_question_daily", - url: (*url).to_string(), - } + /// Register for a contest + #[named] + pub async fn register_contest(&self, contest: &str) -> Result<response,error> { + 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 specific problem detail - 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 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"), - ); - - json.insert( - "variables", - r#"{"titleSlug": "$titleSlug"}"#.replace("$titleSlug", slug), - ); - - json.insert("operationName", "getQuestionDetail".to_string()); - - Req { - default_headers: self.default_headers, - refer: Some(refer), - info: false, - json: Some(json), - mode: Mode::Post, - name: "get_problem_detail", - url: (&self.conf.sys.urls["graphql"]).to_string(), - } + /// Get contest info + #[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)? + .replace("$contest_slug", contest); + make_req!(self, url.to_string()) .send(&self.client) .await } + /// 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 + titleSlug + questionId + questionFrontendId + categoryTitle + content + codeDefinition + status + metaData + codeSnippets { + langSlug + lang + code + } + isPaidOnly + exampleTestcases + sampleTestCase + enableRunCode + stats + translatedContent + isFavor + difficulty + } + }".to_owned(), Some( + r#"{"s": "$s"}"#.replace("$s", problem) + )).await + } + + /// Send code to judge - pub async fn run_code(self, j: Json, url: String, refer: String) -> Result<response, Error> { + #[named] + pub async fn run_code(&self, j: Json, url: String, refer: String) -> Result<response, Error> { info!("Sending code to judge..."); - Req { - default_headers: self.default_headers, - refer: Some(refer), - info: false, - json: Some(j), - mode: Mode::Post, - name: "run_code", - url, - } + let mut req = make_req!(self, url); + req.mode = Mode::Post(j); + req.refer = Some(refer); + req .send(&self.client) .await } /// Get the result of submission / testing - pub async fn verify_result(self, id: String) -> Result<response, Error> { - trace!("Verifying result..."); + #[named] + pub async fn verify_result(&self, id: String) -> Result<response, Error> { let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); - Req { - default_headers: self.default_headers, - refer: None, - info: false, - json: None, - mode: Mode::Get, - name: "verify_result", - url, - } + make_req!(self, url) .send(&self.client) .await } @@ -266,6 +233,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>; @@ -273,26 +241,24 @@ mod req { /// Standardize request mode pub enum Mode { Get, - Post, + Post(Json), } /// LeetCode request prototype + #[derive(new)] pub struct Req { pub default_headers: HeaderMap, - pub refer: Option<string>, - pub json: Option<json>, - pub info: bool, - pub mode: Mode, pub name: &'static str, pub url: String, + #[new(value = "Mode::Get")] + pub mode: Mode, + #[new(default)] + pub refer: Option<string>, } impl Req { pub async fn send(self, client: &Client) -> Result<response, Error> { trace!("Running leetcode::{}...", &self.name); - if self.info { - info!("{}", &self.name); - } let url = self.url.to_owned(); let headers = LeetCode::headers( self.default_headers, @@ -301,7 +267,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?) 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<t> = Result<t, SerdeError>; #[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<contestquestionstub> = + 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()); +} + </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.diff">(->オリジナル)</a> / <label>アドレス: <input type="text" name="naked_post_url" value="https://patch-diff.githubusercontent.com/raw/clearloop/leetcode-cli/pull/69.diff" 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>