Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Leetcode contests #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
152334H wants to merge 41 commits into clearloop:master from 152334H:master
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c3c3df3
add graphql query for user info
152334H May 18, 2022
e8e83eb
add parser for graphql user info
152334H May 18, 2022
a22a4f6
return `CookieError` when leetcode rejects exec/test `Run`
152334H May 18, 2022
9d1a95c
add the `is_session_bad` method to somewhat-accurately determine when...
152334H May 18, 2022
916e936
When json parsing fails, if the underlying request requires user auth...
152334H May 18, 2022
a545657
Merge branch 'clearloop:master' into master
152334H May 20, 2022
bd740e8
get rid of ZWSPs in problem descriptions (see #56)
152334H May 20, 2022
1512f72
Merge branch 'master' of github.com:152334H/leetcode-cli
152334H May 20, 2022
ff7baf0
add Error::PremiumError
152334H May 20, 2022
496c332
throw PremiumError when locked questions are queried for details
152334H May 20, 2022
944b9a9
Merge branch 'clearloop:master' into master
152334H May 22, 2022
0f881e2
clippy fix
152334H May 25, 2022
22248f2
slight refactor: use multiline strings instead of `vec![].join("")`, ...
152334H May 25, 2022
d7114f7
function_name
152334H May 25, 2022
b623957
Req.info is a useless field
152334H May 25, 2022
c7c85ff
merge `Req.mode` and `Req.json` into a single enum
152334H May 25, 2022
b541443
simplify Req construction
152334H May 25, 2022
b3937d5
remove `trace!`/`info!` calls that are effectively duplicates of othe...
152334H May 25, 2022
500894d
refactor `Leetcode` methods to take `&self`; remove unnecessary `.clo...
152334H May 25, 2022
d757aff
put back the info!() call I accidentally removed
152334H May 26, 2022
cecf571
add ContestNode and ContestQuestionNode types to models.rs
152334H May 30, 2022
78fbba6
add a parser for Contest type
152334H May 30, 2022
12af5fd
add stubs for new commands `contest` and `fun` (which may or may not ...
152334H May 30, 2022
07aefe9
add TODO for get_question_tags_by_id (should probably use .get_graphql)
152334H May 30, 2022
4c58149
add a generic graphql query function
152334H May 30, 2022
98885a0
deduplicate code using `.get_graphql()`
152334H May 30, 2022
9102d6c
add a minor test for parsing ContestQuestionNode
152334H May 30, 2022
b14ca10
add contest query methods to leetcode.rs
152334H May 30, 2022
69cf804
add parser for contest questions (it is actually a parser for questio...
152334H May 30, 2022
d5ee9bf
add contest/contestquestion handlers to cache mod
152334H May 30, 2022
81df506
change exec/test commands to allow for contest problem submissions
152334H May 30, 2022
e567b46
Add `contest` command.
152334H May 30, 2022
375004b
add debug fun command (will probably remove later)
152334H May 30, 2022
c02986e
add chrono package for contest command
152334H May 30, 2022
dcec6ff
add necessary URLs to cfg.rs for contest command
152334H May 30, 2022
b3eb0e2
revert contest println formatting to previous working edition
152334H May 31, 2022
b3d8cf5
clippy patch
152334H May 31, 2022
eefa2f0
clean up contest/fun commands code
152334H Jun 3, 2022
1d32cfc
use serde_json to autoparse where possible
152334H Jun 5, 2022
64962b1
merge `get_question_detail` and `get_contest_question_detail`
152334H Jun 5, 2022
8c862bd
use `get_graphql` more
152334H Jun 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
69 changes: 51 additions & 18 deletions src/cache/mod.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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?
);
Expand All @@ -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<T: DeserializeOwned>(&self, resp: Response) -> Result<T, Error> {
Expand All @@ -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
Expand All @@ -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<Contest, Error> {
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<Problem, Error> {
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(),
));
Expand All @@ -126,8 +142,7 @@ impl Cache {
/// Get daily problem
pub async fn get_daily_problem_id(&self) -> Result<i32, Error> {
parser::daily(
self.clone()
.0
self.0
.get_question_daily()
.await?
.json() // does not require LEETCODE_SESSION
Expand Down Expand Up @@ -171,7 +186,6 @@ impl Cache {
} else {
let json: Value = self
.0
.clone()
.get_question_detail(&target.slug)
.await?
.json()
Expand All @@ -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<Vec<String>, Error> {
trace!("Geting {} questions...", &rslug);
let ids: Vec<String>;
Expand All @@ -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()
Expand Down Expand Up @@ -239,6 +262,7 @@ impl Cache {
run: Run,
rfid: i32,
testcase: Option<String>,
contest: Option<&str>
) -> Result<(HashMap<&'static str, String>, [String; 2]), Error> {
trace!("pre run code...");
use crate::helper::{code_path, test_cases_path};
Expand Down Expand Up @@ -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)
}
};

Expand All @@ -311,7 +345,6 @@ impl Cache {

let json: VerifyResult = self.resp_to_json(
self
.clone()
.0
.verify_result(rid.clone())
.await?
Expand All @@ -326,14 +359,14 @@ impl Cache {
rfid: i32,
run: Run,
testcase: Option<String>,
contest: Option<&str>
) -> Result<VerifyResult, Error> {
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)
Expand Down
72 changes: 70 additions & 2 deletions src/cache/models.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContestQuestionStub>,
}
// 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<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,
}

Expand Down Expand Up @@ -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
Expand Down
45 changes: 44 additions & 1 deletion src/cache/parser.rs
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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<Contest> {
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<Problem>, v: Value) -> Option<()> {
Expand Down Expand Up @@ -28,6 +43,34 @@ pub fn problem(problems: &mut Vec<Problem>, 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::<f32>().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<bool> {
/* None - parsing failed
Expand Down
4 changes: 4 additions & 0 deletions src/cfg.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion src/cli.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use crate::{
cmds::{
Command, DataCommand, EditCommand, ExecCommand, ListCommand, PickCommand, StatCommand,
TestCommand,
TestCommand, ContestCommand, FunCommand
},
err::Error,
flag::{Debug, Flag},
Expand Down Expand Up @@ -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)
Expand All @@ -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),
}
}
Loading

AltStyle によって変換されたページ (->オリジナル) /