diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit deleted file mode 100755 index f735ef4..0000000 --- a/.cargo-husky/hooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -cargo build -cargo test diff --git a/Cargo.lock b/Cargo.lock index 04b9ebb..e75ea22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "async-compression" version = "0.4.0" @@ -203,12 +209,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" -[[package]] -name = "cargo-husky" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" - [[package]] name = "cc" version = "1.0.79" @@ -232,18 +232,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.6" +version = "4.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6320c6d1c98b6981da7bb2dcecbd0be9dc98d42165fa8326b21000f7dbfde6d0" +checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.3.5" +version = "4.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae" +checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" dependencies = [ "anstream", "anstyle", @@ -520,6 +520,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -772,7 +778,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -785,6 +791,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -937,7 +949,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", ] [[package]] @@ -1025,10 +1047,10 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "leetcode-cli" -version = "0.3.13" +version = "0.4.0" dependencies = [ + "anyhow", "async-trait", - "cargo-husky", "clap", "colored", "diesel", @@ -2299,17 +2321,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index ce78e55..fcaafac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ path = "src/bin/lc.rs" [package] name = "leetcode-cli" -version = "0.3.13" -authors = ["clearloop "] +version = "0.4.0" +authors = ["clearloop "] edition = "2021" description = "Leet your code in command-line." repository = "https://github.com/clearloop/leetcode-cli" @@ -32,6 +32,7 @@ serde_json = "1.0.82" toml = "0.5.9" regex = "1.6.0" scraper = "0.13.0" +anyhow = "1.0.71" [dependencies.diesel] version = "2.0.3" @@ -41,11 +42,6 @@ features = ["sqlite"] version = "0.11.11" features = ["gzip", "json"] -[dev-dependencies.cargo-husky] -version = "1.5.0" -default-features = false -features = ["precommit-hook", "user-hooks"] - [features] pym = ["pyo3"] diff --git a/README.md b/README.md index 25d46a3..f254d92 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # leetcode-cli -![Rust](https://github.com/clearloop/leetcode-cli/workflows/Rust/badge.svg) + +![Rust](https://github.com/clearloop/leetcode-cli/workflows/leetcode-cli/badge.svg) [![crate](https://img.shields.io/crates/v/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) [![doc](https://img.shields.io/badge/current-docs-brightgreen.svg)](https://docs.rs/leetcode-cli/) [![downloads](https://img.shields.io/crates/d/leetcode-cli.svg)](https://crates.io/crates/leetcode-cli) -[![gitter](https://img.shields.io/gitter/room/odditypark/leetcode-cli)](https://gitter.im/Odditypark/leetcode-cli) +[![telegram](https://img.shields.io/badge/telegram-blue?logo=telegram)](https://t.me/+U_5si6PhWykxZTI1) [![LICENSE](https://img.shields.io/crates/l/leetcode-cli.svg)](https://choosealicense.com/licenses/mit/) ## Installing ```sh # Required dependencies: -# +# # gcc # libssl-dev # libdbus-1-dev @@ -48,14 +49,24 @@ SUBCOMMANDS: ## Example -For example, given this config (can be found in `~/.leetcode/leetcode.toml`, it can be generated automatically with command: `leetcode list` if you are a new user): +For example, given this config (could be found at `~/.leetcode/leetcode.toml`): ```toml [code] -lang = "rust" -editor = "emacs" +editor = emacs # Optional parameter -editor_args = ['-nw'] +editor-args = ['-nw'] +lang = 'rust' + +[cookies] +csrf = '' +session = '' + +[storage] +cache = 'Problems' +code = 'code' +root = '~/.leetcode' +scripts = 'scripts' ``` #### 1. pick @@ -144,7 +155,8 @@ leetcode exec 1 ## Cookies -The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, you can manually input your LeetCode Cookies to the configuration file. +The cookie plugin of leetcode-cli can work on OSX and [Linux][#1]. **If you are on a different platform, there are problems with caching the cookies**, +you can manually input your LeetCode Cookies to the configuration file. ```toml [cookies] @@ -154,7 +166,6 @@ session = "..." For Example, using Chrome (after logging in to LeetCode): - #### Step 1 Open Chrome and navigate to the link below: @@ -166,6 +177,7 @@ chrome://settings/cookies/detail?site=leetcode.com #### Step 2 Copy `Content` from `LEETCODE_SESSION` and `csrftoken` to `session` and `csrf` in your configuration file, respectively: + ```toml [cookies] csrf = "${csrftoken}" @@ -189,13 +201,13 @@ import json; def plan(sps, stags): ## - # `print` in python is supported, - # if you want to know the data structures of these two args, + # `print` in python is supported, + # if you want to know the data structures of these two args, # just print them ## problems = json.loads(sps) tags = json.loads(stags) - + ret = [] tm = {} for tag in tags: @@ -215,17 +227,15 @@ Then run `list` with the filter that you just wrote: leetcode list -p plan1 ``` -And that's it! Enjoy! +That's it! Enjoy! +## Contributions -## PR - -[PRs][pr] are more than welcome! +Feel free to add your names and emails in the `authors` field of `Cargo.toml` ! ## LICENSE MIT - [pr]: https://github.com/clearloop/leetcode-cli/pulls [#1]: https://github.com/clearloop/leetcode-cli/issues/1 diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 6967404..b15f677 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -7,7 +7,8 @@ use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; use crate::helper::test_cases_path; -use crate::{cfg, err::Error, plugins::LeetCode}; +use crate::{config::Config, err::Error, plugins::LeetCode}; +use anyhow::anyhow; use colored::Colorize; use diesel::prelude::*; use reqwest::Response; @@ -309,33 +310,14 @@ impl Cache { json.insert("data_input", test_case); let url = match run { - Run::Test => conf - .sys - .urls - .get("test") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), + Run::Test => conf.sys.urls.test(&p.slug), Run::Submit => { json.insert("judge_type", "large".to_string()); - conf.sys - .urls - .get("submit") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug) + conf.sys.urls.submit(&p.slug) } }; - Ok(( - json, - [ - url, - conf.sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$slug", &p.slug), - ], - )) + Ok((json, [url, conf.sys.urls.problem(&p.slug)])) } /// TODO: The real delay @@ -361,15 +343,20 @@ impl Cache { ) -> Result { trace!("Exec problem filter —— Test or Submit"); let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, test_case).await?; - trace!("Pre run code result {:?}, {:?}, {:?}", json, url, refer); + trace!("Pre run code result {:#?}, {}, {}", json, url, refer); - let run_res: RunCode = self + let text = self .0 .clone() .run_code(json.clone(), url.clone(), refer.clone()) .await? - .json() // does not require LEETCODE_SESSION (very oddly) + .text() .await?; + + let run_res: RunCode = serde_json::from_str(&text).map_err(|e| { + anyhow!("json error: {e}, plz double check your session and csrf config.") + })?; + trace!("Run code result {:#?}", run_res); // Check if leetcode accepted the Run request @@ -397,7 +384,7 @@ impl Cache { /// New cache pub fn new() -> Result { - let conf = cfg::locate()?; + let conf = Config::locate()?; let mut c = conn(conf.storage.cache()?); diesel::sql_query(CREATE_PROBLEMS_IF_NOT_EXISTS).execute(&mut c)?; diesel::sql_query(CREATE_TAGS_IF_NOT_EXISTS).execute(&mut c)?; diff --git a/src/cache/models.rs b/src/cache/models.rs index 2ac68b7..12c512e 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -39,6 +39,7 @@ impl Problem { _ => "Unknown", } } + pub fn desc_comment(&self, conf: &Config) -> String { let mut res = String::new(); let comment_leading = &conf.code.comment_leading; diff --git a/src/cfg.rs b/src/cfg.rs deleted file mode 100644 index de152a5..0000000 --- a/src/cfg.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Soft-link with `config.toml` -//! -//! leetcode-cli will generate a `leetcode.toml` by default, -//! if you wanna change to it, you can: -//! -//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly -//! + Use `leetcode config` to update it -use crate::Error; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::PathBuf}; - -pub const DEFAULT_CONFIG: &str = r##" -# usually you don't wanna change those -[sys] -categories = [ - "algorithms", - "concurrency", - "database", - "shell" -] - -langs = [ - "bash", - "c", - "cpp", - "csharp", - "elixir", - "golang", - "java", - "javascript", - "kotlin", - "mysql", - "php", - "python", - "python3", - "ruby", - "rust", - "scala", - "swift", - "typescript" -] - -[sys.urls] -base = "https://leetcode.com" -graphql = "https://leetcode.com/graphql" -login = "https://leetcode.com/accounts/login/" -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/" -session = "https://leetcode.com/session/" -submit = "https://leetcode.com/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" - -[code] -editor = "vim" -lang = "rust" -edit_code_marker = false -comment_problem_desc = false -comment_leading = "///" -start_marker = "@lc code=start" -end_marker = "@lc code=start" -test = true -pick = "${fid}.${slug}" -submission = "${fid}.${slug}.${sid}.${ac}" - -[cookies] -csrf = "" -session = "" - -[storage] -root = "~/.leetcode" -scripts = "scripts" -code = "code" -# absolutely path for the cache, other use root as parent dir -cache = "~/.cache/leetcode" -"##; - -/// Sync with `~/.leetcode/config.toml` -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Config { - pub sys: Sys, - pub code: Code, - pub cookies: Cookies, - pub storage: Storage, -} - -impl Config { - /// Sync new config to config.toml - pub fn sync(&self) -> Result<(), Error> { - let home = dirs::home_dir().ok_or(Error::NoneError)?; - let conf = home.join(".leetcode/leetcode.toml"); - fs::write(conf, toml::ser::to_string_pretty(&self)?)?; - - Ok(()) - } -} - -/// Cookie settings -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Cookies { - pub csrf: String, - pub session: String, -} - -/// System settings, for leetcode api mainly -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Sys { - pub categories: Vec, - pub langs: Vec, - pub urls: HashMap, -} - -/// Leetcode API -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Urls { - pub base: String, - pub graphql: String, - pub login: String, - pub problems: String, - pub problem: String, - pub test: String, - pub session: String, - pub submit: String, - pub submissions: String, - pub submission: String, - pub verify: String, - pub favorites: String, - pub favorite_delete: String, -} - -/// default editor and langs -/// -/// + support editor: [emacs, vim] -/// + support langs: all in config -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Code { - pub editor: String, - #[serde(rename(serialize = "editor_args", deserialize = "editor_args"))] - pub editor_args: Option>, - pub edit_code_marker: bool, - pub start_marker: String, - pub end_marker: String, - pub comment_problem_desc: bool, - pub comment_leading: String, - pub test: bool, - pub lang: String, - pub pick: String, - pub submission: String, -} - -/// Locate code files -/// -/// + cache -> the path to cache -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Storage { - cache: String, - code: String, - root: String, - scripts: Option, -} - -impl Storage { - /// convert root path - pub fn root(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = self.root.replace('~', &home); - Ok(path) - } - - /// get cache path - pub fn cache(&self) -> Result { - let home = dirs::home_dir() - .ok_or(Error::NoneError)? - .to_string_lossy() - .to_string(); - let path = PathBuf::from(self.cache.replace('~', &home)); - if !path.is_dir() { - info!("Generate cache dir at {:?}.", &path); - fs::DirBuilder::new().recursive(true).create(&path)?; - } - - Ok(path.join("Problems").to_string_lossy().to_string()) - } - - /// get code path - pub fn code(&self) -> Result { - let root = &self.root()?; - let p = PathBuf::from(root).join(&self.code); - if !PathBuf::from(&p).exists() { - fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } - - /// get scripts path - pub fn scripts(mut self) -> Result { - let root = &self.root()?; - if self.scripts.is_none() { - let tmp = toml::from_str::(DEFAULT_CONFIG)?; - self.scripts = Some(tmp.storage.scripts.ok_or(Error::NoneError)?); - } - - let p = PathBuf::from(root).join(self.scripts.ok_or(Error::NoneError)?); - if !PathBuf::from(&p).exists() { - std::fs::create_dir(&p)? - } - - Ok(p.to_string_lossy().to_string()) - } -} - -/// Locate lc's config file -pub fn locate() -> Result { - let conf = root()?.join("leetcode.toml"); - if !conf.is_file() { - fs::write(&conf, &DEFAULT_CONFIG[1..])?; - } - - let s = fs::read_to_string(&conf)?; - Ok(toml::from_str::(&s)?) -} - -/// Get root path of leetcode-cli -pub fn root() -> Result { - let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); - if !dir.is_dir() { - info!("Generate root dir at {:?}.", &dir); - fs::DirBuilder::new().recursive(true).create(&dir)?; - } - - Ok(dir) -} diff --git a/src/config/code.rs b/src/config/code.rs new file mode 100644 index 0000000..0384608 --- /dev/null +++ b/src/config/code.rs @@ -0,0 +1,54 @@ +//! Code in config +use serde::{Deserialize, Serialize}; + +fn default_pick() -> String { + "${fid}.${slug}".into() +} + +fn default_submission() -> String { + "${fid}.${slug}.${sid}.${ac}".into() +} + +/// Code config +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Code { + #[serde(default)] + pub editor: String, + #[serde(rename(serialize = "editor-args"), alias = "editor-args", default)] + pub editor_args: Option>, + #[serde(default, skip_serializing)] + pub edit_code_marker: bool, + #[serde(default, skip_serializing)] + pub start_marker: String, + #[serde(default, skip_serializing)] + pub end_marker: String, + #[serde(default, skip_serializing)] + pub comment_problem_desc: bool, + #[serde(default, skip_serializing)] + pub comment_leading: String, + #[serde(default, skip_serializing)] + pub test: bool, + pub lang: String, + #[serde(default = "default_pick", skip_serializing)] + pub pick: String, + #[serde(default = "default_submission", skip_serializing)] + pub submission: String, +} + +impl Default for Code { + fn default() -> Self { + Self { + editor: "vim".into(), + editor_args: None, + edit_code_marker: false, + start_marker: "".into(), + end_marker: "".into(), + comment_problem_desc: false, + comment_leading: "".into(), + test: true, + lang: "rust".into(), + pick: "${fid}.${slug}".into(), + submission: "${fid}.${slug}.${sid}.${ac}".into(), + } + } +} diff --git a/src/config/cookies.rs b/src/config/cookies.rs new file mode 100644 index 0000000..d03cd4a --- /dev/null +++ b/src/config/cookies.rs @@ -0,0 +1,15 @@ +//! Cookies in config +use serde::{Deserialize, Serialize}; + +/// Cookies settings +#[derive(Default, Clone, Debug, Deserialize, Serialize)] +pub struct Cookies { + pub csrf: String, + pub session: String, +} + +impl std::string::ToString for Cookies { + fn to_string(&self) -> String { + format!("LEETCODE_SESSION={};csrftoken={};", self.session, self.csrf) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..d2e7d1d --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,75 @@ +//! Soft-link with `config.toml` +//! +//! leetcode-cli will generate a `leetcode.toml` by default, +//! if you wanna change to it, you can: +//! +//! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly +//! + Use `leetcode config` to update it +use crate::{ + config::{code::Code, cookies::Cookies, storage::Storage, sys::Sys}, + Error, +}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +mod code; +mod cookies; +mod storage; +mod sys; + +/// Sync with `~/.leetcode/leetcode.toml` +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Config { + #[serde(default, skip_serializing)] + pub sys: Sys, + pub code: Code, + pub cookies: Cookies, + pub storage: Storage, +} + +impl Config { + fn write_default(p: impl AsRef) -> Result<(), crate::Error> { + fs::write(p.as_ref(), toml::ser::to_string_pretty(&Self::default())?)?; + + Ok(()) + } + + /// Locate lc's config file + pub fn locate() -> Result { + let conf = Self::root()?.join("leetcode.toml"); + + if !conf.is_file() { + Self::write_default(&conf)?; + } + + let s = fs::read_to_string(&conf)?; + match toml::from_str::(&s) { + Ok(config) => Ok(config), + Err(e) => { + let tmp = Self::root()?.join("leetcode.tmp.toml"); + Self::write_default(tmp)?; + Err(e.into()) + } + } + } + + /// Get root path of leetcode-cli + pub fn root() -> Result { + let dir = dirs::home_dir().ok_or(Error::NoneError)?.join(".leetcode"); + if !dir.is_dir() { + info!("Generate root dir at {:?}.", &dir); + fs::DirBuilder::new().recursive(true).create(&dir)?; + } + + Ok(dir) + } + + /// Sync new config to config.toml + pub fn sync(&self) -> Result<(), Error> { + let home = dirs::home_dir().ok_or(Error::NoneError)?; + let conf = home.join(".leetcode/leetcode.toml"); + fs::write(conf, toml::ser::to_string_pretty(&self)?)?; + + Ok(()) + } +} diff --git a/src/config/storage.rs b/src/config/storage.rs new file mode 100644 index 0000000..72a01fc --- /dev/null +++ b/src/config/storage.rs @@ -0,0 +1,75 @@ +//! Storage in config. +use crate::Error; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; + +/// Locate code files +/// +/// + cache -> the path to cache +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Storage { + cache: String, + code: String, + root: String, + scripts: Option, +} + +impl Default for Storage { + fn default() -> Self { + Self { + cache: "Problems".into(), + code: "code".into(), + scripts: Some("scripts".into()), + root: "~/.leetcode".into(), + } + } +} + +impl Storage { + /// convert root path + pub fn root(&self) -> Result { + let home = dirs::home_dir() + .ok_or(Error::NoneError)? + .to_string_lossy() + .to_string(); + let path = self.root.replace('~', &home); + Ok(path) + } + + /// get cache path + pub fn cache(&self) -> Result { + let root = PathBuf::from(self.root()?); + if !root.exists() { + info!("Generate cache dir at {:?}.", &root); + fs::DirBuilder::new().recursive(true).create(&root)?; + } + + Ok(root.join("Problems").to_string_lossy().to_string()) + } + + /// get code path + pub fn code(&self) -> Result { + let root = &self.root()?; + let p = PathBuf::from(root).join(&self.code); + if !PathBuf::from(&p).exists() { + fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } + + /// get scripts path + pub fn scripts(mut self) -> Result { + let root = &self.root()?; + if self.scripts.is_none() { + self.scripts = Some("scripts".into()); + } + + let p = PathBuf::from(root).join(&self.scripts.ok_or(Error::NoneError)?); + if !PathBuf::from(&p).exists() { + std::fs::create_dir(&p)? + } + + Ok(p.to_string_lossy().to_string()) + } +} diff --git a/src/config/sys.rs b/src/config/sys.rs new file mode 100644 index 0000000..7c6f77e --- /dev/null +++ b/src/config/sys.rs @@ -0,0 +1,102 @@ +//! System section +//! +//! This section is a set of constants after #88 + +use serde::{Deserialize, Serialize}; + +const CATEGORIES: [&str; 4] = ["algorithms", "concurrency", "database", "shell"]; + +// TODO: find a better solution. +fn categories() -> Vec { + CATEGORIES.into_iter().map(|s| s.into()).collect() +} + +/// Leetcode API +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Urls { + pub base: String, + pub graphql: String, + pub login: String, + pub problems: String, + pub problem: String, + pub tag: String, + pub test: String, + pub session: String, + pub submit: String, + pub submissions: String, + pub submission: String, + pub verify: String, + pub favorites: String, + pub favorite_delete: String, +} + +impl Default for Urls { + fn default() -> Self { + Self { + base: "https://leetcode.com".into(), + graphql: "https://leetcode.com/graphql".into(), + login: "https://leetcode.com/accounts/login/".into(), + problems: "https://leetcode.com/api/problems/$category/".into(), + problem: "https://leetcode.com/problems/$slug/description/".into(), + tag: "https://leetcode.com/tag/$slug/".into(), + test: "https://leetcode.com/problems/$slug/interpret_solution/".into(), + session: "https://leetcode.com/session/".into(), + submit: "https://leetcode.com/problems/$slug/submit/".into(), + submissions: "https://leetcode.com/submissions/detail/$id/".into(), + submission: "https://leetcode.com/submissions/detail/$id/".into(), + verify: "https://leetcode.com/submissions/detail/$id/check/".into(), + favorites: "https://leetcode.com/list/api/questions".into(), + favorite_delete: "https://leetcode.com/list/api/questions/$hash/$id".into(), + } + } +} + +impl Urls { + /// problem url with specific `$slug` + pub fn problem(&self, slug: &str) -> String { + self.problem.replace("$slug", slug) + } + + /// problems url with specific `$category` + pub fn problems(&self, category: &str) -> String { + self.problems.replace("$category", category) + } + + /// submit url with specific `$slug` + pub fn submit(&self, slug: &str) -> String { + self.submit.replace("$slug", slug) + } + + /// tag url with specific `$slug` + pub fn tag(&self, slug: &str) -> String { + self.tag.replace("$slug", slug) + } + + /// test url with specific `$slug` + pub fn test(&self, slug: &str) -> String { + self.test.replace("$slug", slug) + } + + /// verify url with specific `$id` + pub fn verify(&self, id: &str) -> String { + self.verify.replace("$id", id) + } +} + +/// System settings, for leetcode api mainly +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Sys { + #[serde(default = "categories")] + pub categories: Vec, + #[serde(default)] + pub urls: Urls, +} + +impl Default for Sys { + fn default() -> Self { + Self { + categories: CATEGORIES.into_iter().map(|s| s.into()).collect(), + urls: Default::default(), + } + } +} diff --git a/src/err.rs b/src/err.rs index 12d785e..6421cfd 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1,12 +1,10 @@ //! Errors in leetcode-cli -use crate::cfg::{root, DEFAULT_CONFIG}; use crate::cmds::{Command, DataCommand}; use colored::Colorize; use std::fmt; // fixme: use this_error /// Error enum -#[derive(Clone)] pub enum Error { MatchError, DownloadError(String), @@ -21,6 +19,7 @@ pub enum Error { SilentError, NoneError, ChromeNotLogin, + Anyhow(anyhow::Error), } impl std::fmt::Debug for Error { @@ -60,7 +59,8 @@ impl std::fmt::Debug for Error { "json from response parse failed, please open a new issue at: {}.", "https://github.com/clearloop/leetcode-cli/".underline(), ), - Error::ChromeNotLogin => write!(f, "maybe you not login on the Chrome, you can login and retry.") + Error::ChromeNotLogin => write!(f, "maybe you not login on the Chrome, you can login and retry."), + Error::Anyhow(e) => write!(f, "{} {}", e, e), } } } @@ -103,15 +103,13 @@ impl std::convert::From for Error { // toml impl std::convert::From for Error { fn from(_err: toml::de::Error) -> Self { - let conf = root().unwrap().join("leetcode_tmp.toml"); - std::fs::write(conf, &DEFAULT_CONFIG[1..]).unwrap(); #[cfg(debug_assertions)] let err_msg = format!( "{}, {}{}{}{}{}{}", _err, "Parse config file failed, ", "leetcode-cli has just generated a new leetcode.toml at ", - "~/.leetcode/leetcode_tmp.toml,".green().bold().underline(), + "~/.leetcode/leetcode.tmp.toml,".green().bold().underline(), " the current one at ", "~/.leetcode/leetcode.toml".yellow().bold().underline(), " seems missing some keys, Please compare the new file and add the missing keys.\n", @@ -126,7 +124,7 @@ impl std::convert::From for Error { "~/.leetcode/leetcode.toml".yellow().bold().underline(), " seems missing some keys, Please compare the new file and add the missing keys.\n", ); - Error::ParseError(err_msg) + Error::ParseError(err_msg.trim_start().into()) } } @@ -150,6 +148,12 @@ impl std::convert::From for Error { } } +impl From for Error { + fn from(err: anyhow::Error) -> Self { + Error::Anyhow(err) + } +} + // pyo3 #[cfg(feature = "pym")] impl std::convert::From for Error { diff --git a/src/helper.rs b/src/helper.rs index 454801d..db1a77e 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -192,7 +192,7 @@ mod file { /// Generate test cases path by fid pub fn test_cases_path(problem: &Problem) -> Result { - let conf = crate::cfg::locate()?; + let conf = crate::config::Config::locate()?; let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick); path = path.replace("${fid}", &problem.fid.to_string()); @@ -202,7 +202,7 @@ mod file { /// Generate code path by fid pub fn code_path(problem: &Problem, l: Option) -> Result { - let conf = crate::cfg::locate()?; + let conf = crate::config::Config::locate()?; let mut lang = conf.code.lang; if l.is_some() { lang = l.ok_or(Error::NoneError)?; @@ -225,7 +225,7 @@ mod file { pub fn load_script(module: &str) -> Result { use std::fs::File; use std::io::Read; - let conf = crate::cfg::locate()?; + let conf = crate::config::Config::locate()?; let mut script = "".to_string(); File::open(format!("{}/{}.py", conf.storage.scripts()?, module))? .read_to_string(&mut script)?; diff --git a/src/lib.rs b/src/lib.rs index e97fb6e..4bc2a53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,9 +235,9 @@ extern crate diesel; // show docs pub mod cache; -pub mod cfg; pub mod cli; pub mod cmds; +pub mod config; pub mod err; pub mod flag; pub mod helper; @@ -247,5 +247,5 @@ pub mod pym; // re-exports pub use cache::Cache; -pub use cfg::Config; +pub use config::Config; pub use err::Error; diff --git a/src/plugins/chrome.rs b/src/plugins/chrome.rs index f44008c..8ccc6f7 100644 --- a/src/plugins/chrome.rs +++ b/src/plugins/chrome.rs @@ -41,7 +41,7 @@ impl std::string::ToString for Ident { /// Get cookies from chrome storage pub fn cookies() -> Result { - let ccfg = crate::cfg::locate()?.cookies; + let ccfg = crate::config::Config::locate()?.cookies; if !ccfg.csrf.is_empty() && !ccfg.session.is_empty() { return Ok(Ident { csrf: ccfg.csrf, diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 141c765..6463ff1 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -1,8 +1,8 @@ use self::req::{Json, Mode, Req}; use crate::{ - cfg::{self, Config}, + config::{self, Config}, err::Error, - plugins::chrome, + // plugins::chrome, }; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, @@ -36,15 +36,15 @@ impl LeetCode { /// New LeetCode client pub fn new() -> Result { - let conf = cfg::locate()?; - let cookies = chrome::cookies()?; + let conf = config::Config::locate()?; + let cookies = conf.cookies.clone(); let default_headers = LeetCode::headers( HeaderMap::new(), vec![ ("Cookie", cookies.to_string().as_str()), ("x-csrftoken", &cookies.csrf), ("x-requested-with", "XMLHttpRequest"), - ("Origin", &conf.sys.urls["base"]), + ("Origin", &conf.sys.urls.base), ], )?; @@ -53,11 +53,6 @@ impl LeetCode { .connect_timeout(Duration::from_secs(30)) .build()?; - // Sync conf - if conf.cookies.csrf != cookies.csrf { - conf.sync()?; - } - Ok(LeetCode { conf, client, @@ -68,13 +63,7 @@ impl LeetCode { /// Get category problems pub async fn get_category_problems(self, category: &str) -> Result { trace!("Requesting {} problems...", &category); - let url = &self - .conf - .sys - .urls - .get("problems") - .ok_or(Error::NoneError)? - .replace("$category", category); + let url = &self.conf.sys.urls.problems(category); Req { default_headers: self.default_headers, @@ -91,7 +80,7 @@ impl LeetCode { 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 url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "getTopicTag".to_string()); json.insert("variables", r#"{"slug": "$slug"}"#.replace("$slug", slug)); @@ -111,9 +100,7 @@ impl LeetCode { Req { default_headers: self.default_headers, - refer: Some( - (self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug), - ), + refer: Some(self.conf.sys.urls.tag(slug)), info: false, json: Some(json), mode: Mode::Post, @@ -126,7 +113,7 @@ impl LeetCode { 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 url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "a".to_string()); json.insert( @@ -156,7 +143,7 @@ impl LeetCode { /// Get daily problem 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 url = &self.conf.sys.urls.graphql; let mut json: Json = HashMap::new(); json.insert("operationName", "daily".to_string()); json.insert( @@ -189,13 +176,7 @@ impl LeetCode { /// Get specific problem detail 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 refer = self.conf.sys.urls.problem(slug); let mut json: Json = HashMap::new(); json.insert( "query", @@ -230,7 +211,7 @@ impl LeetCode { json: Some(json), mode: Mode::Post, name: "get_problem_detail", - url: self.conf.sys.urls["graphql"].to_string(), + url: self.conf.sys.urls.graphql.into(), } .send(&self.client) .await @@ -255,13 +236,8 @@ impl LeetCode { /// Get the result of submission / testing 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); + let url = self.conf.sys.urls.verify(&id); + Req { default_headers: self.default_headers, refer: None, diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 5a84870..9b2d903 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -6,6 +6,8 @@ //! ## login to `leetcode.com` //! Leetcode-cli use chrome cookie directly, do not need to login, please make sure you have loggined in `leetcode.com` before usnig `leetcode-cli` //! -mod chrome; + +// FIXME: Read cookies from local storage. (issue #122) +// mod chrome; mod leetcode; pub use leetcode::LeetCode; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy