diff --git a/CONFIG.md b/CONFIG.md
index 4b9a767..f41fd01 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -1,16 +1,13 @@
# 設定
-設定に必須な情報はcontest_dir, source_file_path, need_to_compile, execute_command,
-(language_id または language_name)です.
+設定に必須な情報はcontest_dir, source_file_path, need_to_compile, execute_commandです.
| 項目 | 説明 |
| --- | --- |
| contest_dir | ac-ninjaを実行するディレクトリです.
{{contesty_type}},{{contest_id}}を特定できる必要があります. |
-| source_file_path | ac-ninjaで提出するファイルのパスです. |
+| source_file_path | ソースファイルのパスです. |
| need_to_compile | プログラムの実行にコンパイルが必要かどうかを指定します.
trueの場合, {{compile_command}}を指定する必要があります. |
| execute_command | プログラムを実行するためのコマンドです. |
-| language_id | ac-ninjaでの提出に用いる言語のidです.
AtCoderの提出セレクトボックスをディベロッパーツールから見ることで
確認できますが, [早見表](./LANG_ID.md)が便利です. |
-| language_name | language_idの代わりに, language_nameを指定することができます.
AtCoderの提出言語セレクトボックスの表示の通りに指定してください.
\"C++(GCC 9.2.1)\", \"Python (3.8.2)\", \"Rust (1.42.0)\"など.
こちらも, [早見表](./LANG_ID.md)の文字列をコピペすると便利です. |
ファイルパスや, 実行コマンドには{{変数}}を含むことができます.
@@ -38,7 +35,6 @@ output_file_path = "{{contest_dir}}/a.out"
source_file_path = "{{contest_dir}}/{{problem_id}}.cpp"
compile_command = "g++ {{source_file_path}} -std=c++17 -o {{output_file_path}}"
execute_command = "{{output_file_path}}"
-language_id = 5001 # language_nameの場合 "C++ 20 (gcc 12.2)"
```
以下はPythonでの設定例です
@@ -49,6 +45,4 @@ need_to_compile = false
contest_dir = "{{work_space}}/{{CONTEST_TYPE}}/{{contest_id_0_pad}}"
source_file_path = "{{contest_dir}}/{{problem_id}}/main.py"
execute_command = "python3 {{source_file_path}}"
-language_name = "Python (CPython 3.11.4)" # language_idの場合 5055
```
-
diff --git a/Cargo.lock b/Cargo.lock
index 97648ac..056187f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7,18 +7,16 @@ name = "ac-ninja"
version = "0.1.1"
dependencies = [
"anyhow",
- "chrono",
"clap",
"colored",
"dialoguer",
- "indicatif",
"prettytable-rs",
"regex",
"reqwest",
"scraper",
"serde",
"shellexpand",
- "time 0.3.47",
+ "time",
"tokio",
"toml",
]
@@ -32,15 +30,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
[[package]]
name = "anyhow"
version = "1.0.69"
@@ -106,21 +95,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-[[package]]
-name = "chrono"
-version = "0.4.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-integer",
- "num-traits",
- "time 0.1.45",
- "wasm-bindgen",
- "winapi",
-]
-
[[package]]
name = "clap"
version = "4.1.8"
@@ -158,16 +132,6 @@ dependencies = [
"os_str_bytes",
]
-[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
-
[[package]]
name = "colored"
version = "2.0.0"
@@ -205,7 +169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
- "time 0.3.47",
+ "time",
"version_check",
]
@@ -221,7 +185,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
- "time 0.3.47",
+ "time",
"url",
]
@@ -289,50 +253,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "cxx"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72"
-dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613"
-dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
[[package]]
name = "deranged"
version = "0.5.5"
@@ -728,30 +648,6 @@ dependencies = [
"tokio-native-tls",
]
-[[package]]
-name = "iana-time-zone"
-version = "0.1.53"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "wasm-bindgen",
- "winapi",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
-dependencies = [
- "cxx",
- "cxx-build",
-]
-
[[package]]
name = "idna"
version = "0.2.3"
@@ -783,18 +679,6 @@ dependencies = [
"hashbrown",
]
-[[package]]
-name = "indicatif"
-version = "0.17.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729"
-dependencies = [
- "console",
- "number_prefix",
- "portable-atomic",
- "unicode-width",
-]
-
[[package]]
name = "instant"
version = "0.1.12"
@@ -865,15 +749,6 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
-[[package]]
-name = "link-cplusplus"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
-dependencies = [
- "cc",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
@@ -985,25 +860,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
-[[package]]
-name = "num-integer"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
-dependencies = [
- "autocfg",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
-dependencies = [
- "autocfg",
-]
-
[[package]]
name = "num_cpus"
version = "1.15.0"
@@ -1014,12 +870,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "number_prefix"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
-
[[package]]
name = "once_cell"
version = "1.17.1"
@@ -1215,12 +1065,6 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
-[[package]]
-name = "portable-atomic"
-version = "0.3.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b"
-
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -1540,12 +1384,6 @@ dependencies = [
"tendril",
]
-[[package]]
-name = "scratch"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
-
[[package]]
name = "security-framework"
version = "2.8.2"
@@ -1843,17 +1681,6 @@ dependencies = [
"syn 1.0.109",
]
-[[package]]
-name = "time"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
-dependencies = [
- "libc",
- "wasi 0.10.0+wasi-snapshot-preview1",
- "winapi",
-]
-
[[package]]
name = "time"
version = "0.3.47"
@@ -2091,12 +1918,6 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-[[package]]
-name = "wasi"
-version = "0.10.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
-
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index c8c6d37..b811d34 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,4 @@ regex = "1.7.1"
clap = { version = "4.1.8", features = ["derive"] }
reqwest = { version = "0.11.14", features = ["cookies", "json"] }
dialoguer = "0.10.3"
-indicatif = "0.17.3"
-chrono = "0.4.23"
time = "0.3.36"
diff --git a/LANG_ID.md b/LANG_ID.md
deleted file mode 100644
index d6387d0..0000000
--- a/LANG_ID.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# language_id早見表
-
-| language_name | language_id |
-| :---: | :---: |
-| "><> (fishr 0.1.0)" | 6001 |
-| "Ada 2022 (GNAT 15.2.0)" | 6002 |
-| "APL (GNU APL 1.9)" | 6003 |
-| "Assembly MIPS O32 ABI (GNU assembler 2.42)" | 6004 |
-| "Assembly x64 (NASM 2.16.03)" | 6005 |
-| "AWK (GNU awk 5.2.1)" | 6006 |
-| "A言語 (interpreter af48a2a)" | 6007 |
-| "Bash (bash 5.3)" | 6008 |
-| "BASIC (FreeBASIC 1.10.1)" | 6009 |
-| "bc (GNU bc 1.08.2)" | 6010 |
-| "Befunge93 (TBC 1.0)" | 6011 |
-| "Brainfuck (Tritium 1.2.73)" | 6012 |
-| "C23 (Clang 21.1.0)" | 6013 |
-| "C23 (GCC 14.2.0)" | 6014 |
-| "C# 13.0 (.NET 9.0.8)" | 6015 |
-| "C# 13.0 (.NET Native AOT 9.0.8)" | 6016 |
-| "C++23 (GCC 15.2.0)" | 6017 |
-| "C3 (c3c 0.7.5)" | 6018 |
-| "Carp(Carp 0.5.5)" | 6019 |
-| "cLay (cLay 20250308-1 (GCC 15.2.0))" | 6020 |
-| "Clojure (babashka 1.12.208)" | 6021 |
-| "Clojure (clojure 1.12.2)" | 6022 |
-| "Clojure (Clojure AOT 1.12.2)" | 6023 |
-| "Clojure (ClojureScript 1.12.42 (Clojure 1.12.2 Node.js 22.19.0))" | 6025 |
-| "COBOL (Free) (GnuCOBOL 3.2)" | 6026 |
-| "Common Lisp (SBCL 2.5.8)" | 6027 |
-| "Crystal (Crystal 1.17.0)" | 6028 |
-| "Cyber (Cyber v0.3)" | 6029 |
-| "D (DMD 2.111.0)" | 6030 |
-| "D (GDC 15.2)" | 6031 |
-| "D (LDC 1.41.0)" | 6032 |
-| "Dart (Dart 3.9.2)" | 6033 |
-| "dc 1.5.2 (GNU bc 1.08.2)" | 6034 |
-| "ECLiPSe (ECLiPSe 7.1_13)" | 6035 |
-| "Eiffel (Gobo Eiffel 22.01)" | 6036 |
-| "Eiffel (Liberty Eiffel 07829e3)" | 6037 |
-| "Elixir (Elixir 1.18.4 (OTP 28.0.2))" | 6038 |
-| "Emacs Lisp(Native Compile)(GNU Emacs 29.4)" | 6039 |
-| "Emojicode 1.0 beta 2 (emojicodec 1.0 beta 2)" | 6040 |
-| "Erlang (Erlang 28.0.2)" | 6041 |
-| "F# 9.0 (.NET 9.0.8)" | 6042 |
-| "Factor (Factor 0.100)" | 6043 |
-| "Fish (fish 4.0.2)" | 6044 |
-| "Forth (gforth 0.7.3)" | 6045 |
-| "Fortran2018 (Flang 20.1.7)" | 6046 |
-| "Fortran2023 (GCC 14.2.0)" | 6047 |
-| "FORTRAN77 (GCC 14.2.0)" | 6048 |
-| "Gleam (Gleam 1.12.0 (OTP 28.0.2))" | 6049 |
-| "Go 1.18 (gccgo 15.2.0)" | 6050 |
-| "Go (go 1.25.1)" | 6051 |
-| "Haskell (GHC 9.8.4)" | 6052 |
-| "Haxe/JVM 4.3.7 (hxjava 4.2.0)" | 6053 |
-| "C++ IOI-Style(GNU++20) (GCC 14.2.0)" | 6054 |
-| "ISLisp (Easy-ISLisp 5.43)" | 6055 |
-| "Java24 (OpenJDK 24.0.2)" | 6056 |
-| "JavaScript (Bun 1.2.21)" | 6057 |
-| "JavaScript (Deno 2.4.5)" | 6058 |
-| "JavaScript (Node.js 22.19.0)" | 6059 |
-| "Jule (jule 0.1.6)" | 6060 |
-| "Koka (koka v3.2.2)" | 6061 |
-| "Kotlin (Kotlin/JVM 2.2.10)" | 6062 |
-| "Kuin (kuincl v.2021.8.17)" | 6063 |
-| "Lazy K (irori v1.0.0)" | 6064 |
-| "Lean (lean v4.22.0)" | 6065 |
-| "LLVM IR (Clang 21.1.0)" | 6066 |
-| "Lua (Lua 5.4.7)" | 6067 |
-| "Lua (LuaJIT 2.1.1703358377)" | 6068 |
-| "Mercury (Mercury 22.01.8)" | 6069 |
-| "Nim (Nim 1.6.20)" | 6071 |
-| "Nim (Nim 2.2.4)" | 6072 |
-| "OCaml (ocamlopt 5.3.0)" | 6073 |
-| "Octave (GNU Octave 10.2.0)" | 6074 |
-| "Pascal (fpc 3.2.2)" | 6075 |
-| "Perl (perl 5.38.2)" | 6076 |
-| "PHP (PHP 8.4.12)" | 6077 |
-| "Piet (your-diary/piet_programming_language 3.0.0) (PPM image)" | 6078 |
-| "Pony (ponyc 0.59.0)" | 6079 |
-| "PowerShell (PowerShell 7.5.2)" | 6080 |
-| "Prolog (SWI-Prolog 9.2.9)" | 6081 |
-| "Python (CPython 3.13.7)" | 6082 |
-| "Python (PyPy 3.11-v7.3.20)" | 6083 |
-| "R (GNU R 4.5.0)" | 6084 |
-| "ReasonML (reson 3.16.0)" | 6085 |
-| "Ruby 3.3 (truffleruby 25.0.0)" | 6086 |
-| "Ruby 3.4 (ruby 3.4.5)" | 6087 |
-| "Rust (rustc 1.89.0)" | 6088 |
-| "SageMath (SageMath 10.7)" | 6089 |
-| "Scala (Dotty 3.7.2)" | 6090 |
-| "Scala 3.7.2 (Scala Native 0.5.8)" | 6091 |
-| "Scheme (ChezScheme 10.2.0)" | 6092 |
-| "Scheme (Gauche 0.9.15)" | 6093 |
-| "Seed7 (Seed7 3.5.0)" | 6094 |
-| "Swift 6.2" | 6095 |
-| "Tcl (tclsh 9.0.1)" | 6096 |
-| "Terra (Terra 1.2.0)" | 6097 |
-| "TeX (tex 3.141592653)" | 6098 |
-| "Text (cat 9.4)" | 6099 |
-| "TypeScript 5.8 (Deno 2.4.5)" | 6100 |
-| "TypeScript 5.9 (tsc 5.9.2 (Bun 1.2.21))" | 6101 |
-| "TypeScript 5.9 (tsc 5.9.2 (Node.js 22.19.0))" | 6102 |
-| "Uiua (uiua 0.16.2)" | 6103 |
-| "Unison (Unison 0.5.47)" | 6104 |
-| "V (0.4.10)" | 6105 |
-| "Vala (valac 0.56.18)" | 6106 |
-| "Verilog 2012 (Icarus Verilog 12.0)" | 6107 |
-| "Veryl (veryl 0.16.4)" | 6108 |
-| "WebAssembly (wabt 1.0.34 + iwasm 2.4.1)" | 6109 |
-| "Whitespace (whitespacers 1.3.0)" | 6110 |
-| "Zig (Zig 0.15.1)" | 6111 |
-| "なでしこ (cnako3 3.7.8 (Node.js 22.19.0))" | 6112 |
-| "プロデル (mono版プロデル 2.0.1353)" | 6113 |
-| "Julia (Julia 1.11.6)" | 6114 |
-| "Python (Codon 0.19.3)" | 6115 |
-| "C++23 (Clang 21.1.0)" | 6116 |
-| "Fix (1.1.0-alpha.12)" | 6117 |
-| "SQL (DuckDB 1.3.2)" | 6118 |
diff --git a/README.md b/README.md
index 0014ab8..f61707b 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,13 @@
# AtCoderNinja
-サンプルケースの自動実行・ACであれば自動提出を可能にするCLIです.
+サンプルケースの自動実行と、ACならソースをクリップボードにコピーするCLIです.
+提出はブラウザで行います(Turnstileのため自動提出は不可)。
設定の仕方によって, C++やPythonなど, 様々な言語での環境に対応することができます.
このプロジェクトは開発段階です.
気になる点があったら, [issue](https://github.com/UUGTech/AtCoderNinja/issues)や[pull request](https://github.com/UUGTech/AtCoderNinja/pulls)にお願いします!
-## !!言語アップデート対応
-
-言語アップデートに対応しました。最新のmasterブランチにしてください。また、config内のlang_idやlang_nameを[LANG_ID一覧](./LANG_ID.md)に記載されているものに合わせてください。
-古いままだと提出が出来ません。
-
## インストール
以下のコマンドでインストールできます
@@ -53,7 +49,8 @@ AtCoderNinjaの機能を十分に使うためには, AtCoderにログインす
ac-ninja login
```
-usernameやpasswordは保存されません. セッション情報が`~/.ac-ninja/session.txt`に保存されます.
+ブラウザでログインして、`REVEL_SESSION` を貼り付けます。
+セッション情報が`~/.ac-ninja/session.txt`に保存されます.
- ログアウト
@@ -63,7 +60,15 @@ ac-ninja logout
のようにすることで, 上記`~/.ac-ninija/session.txt`は削除され, ログアウトします.
-- サンプルでACであればそのまま提出する場合
+- ログイン状態の確認
+
+```bash
+ac-ninja login-check
+```
+
+ログインCookieが有効かどうかを確認できます.
+
+- サンプルでACならソースをクリップボードにコピーする場合
``` bash
ac-ninja a
@@ -71,21 +76,21 @@ ac-ninja a
`ac-ninja `のように, 問題を指定します.
-- 提出はせずに、ローカルでのみ実行する場合
+- クリップボードにコピーせず、ローカルでのみ実行する場合
``` bash
ac-ninja a -l
```
-のように`-l`オプションをつけることで, 提出は行いません.
+のように`-l`オプションをつけることで, クリップボードコピーは行いません.
-- サンプルの結果に関わらず提出をする場合
+- サンプルの結果に関わらずコピーする場合
``` bash
ac-ninja a -f
```
-のように`-f`オプションをつけることで, サンプルの結果がACでなくても提出を行います.
+のように`-f`オプションをつけることで, サンプルの結果がACでなくてもコピーを行います.
これは, 正解が複数あり得る場合などに役立つオプションです.
- 手動の入力で確かめたい場合
@@ -95,7 +100,7 @@ ac-ninja a -i
```
のようにすると, サンプルケースではなく, 手動の標準入力で動作を確認することが出来ます.
-もちろん提出は行われません.
+もちろんコピーは行われません.
また、
diff --git a/src/ac_scraper.rs b/src/ac_scraper.rs
index 8298c7a..955e61a 100644
--- a/src/ac_scraper.rs
+++ b/src/ac_scraper.rs
@@ -1,25 +1,22 @@
use std::{
collections::HashMap,
fs::{self, create_dir_all, File},
- io::{BufRead, Write},
+ io::Write,
path::PathBuf,
str::FromStr,
};
use anyhow::{anyhow, Context, Result};
use colored::*;
-use indicatif::{ProgressBar, ProgressStyle};
-use regex::Regex;
use reqwest::{
header::{HeaderMap, HeaderValue, COOKIE},
- Response, StatusCode,
+ Response,
};
use scraper::{ElementRef, Html, Selector};
use shellexpand::full;
use crate::{
- check_samples::Status,
- config::{ConfigMap, ConfigStrMap, ProblemInfo, ProblemStrInfo},
+ config::{ProblemInfo, ProblemStrInfo},
data::ACN,
util::str_format,
};
@@ -29,9 +26,6 @@ const INPUT_HEADER: &str = "入力例";
const OUTPUT_HEADER: &str = "出力例";
const TASKS_URL: &str = "https://atcoder.jp/contests/{{contest_type}}{{contest_id_0_pad}}/tasks";
const PROBLEM_URL: &str = "https://atcoder.jp/contests/{{contest_type}}{{contest_id_0_pad}}/tasks/{{task_screen_name}}?lang=ja";
-const SUBMIT_URL: &str = "https://atcoder.jp/contests/{{contest_type}}{{contest_id_0_pad}}/submit";
-const SUBMISSIONS_URL: &str =
- "https://atcoder.jp/contests/{{contest_type}}{{contest_id_0_pad}}/submissions/me";
const LOGIN_URL: &str = "https://atcoder.jp/login";
const LOCAL_SESSION_PATH: &str = "~/.ac-ninja/session.txt";
const LOCAL_DIR: &str = "~/.ac-ninja";
@@ -63,15 +57,16 @@ pub async fn add_task_name_to_problem_info(
mut problem_str_info: ProblemStrInfo,
) -> Result<(ProblemInfo, ProblemStrInfo)> {
let tasks_url = str_format(TASKS_URL.to_string(), &problem_str_info);
- let body = acn
+ let cookies = load_cookie_headers()?;
+ let resp = acn
.client
.get(tasks_url.clone())
- .headers(acn.cookies.clone().unwrap_or_default())
+ .headers(cookies)
.send()
.await?
- .error_for_status()?
- .text()
- .await?;
+ .error_for_status()?;
+ save_cookie(&resp).await?;
+ let body = resp.text().await?;
let doc = Html::parse_document(&body);
let selctor = Selector::parse("table tbody tr td:nth-child(1)").unwrap();
@@ -112,43 +107,74 @@ pub async fn add_task_name_to_problem_info(
))
}
-async fn get_csrf_token(acn: &ACN, url: &str) -> Result {
- let login_body = acn
- .client
- .get(url)
- .headers(acn.cookies.clone().unwrap_or_default())
- .send()
- .await?
- .error_for_status()?
- .text()
- .await?;
- let login_doc = Html::parse_document(&login_body);
- let selector = Selector::parse("input[name=\"csrf_token\"]").unwrap();
- if let Some(element) = login_doc.select(&selector).next() {
- if let Some(token) = element.value().attr("value") {
- return Ok(token.to_string());
+fn parse_cookie_string(raw: &str) -> HashMap {
+ let mut map = HashMap::new();
+ let mut s = raw.trim();
+ if let Some(stripped) = s.strip_prefix("Cookie:") {
+ s = stripped.trim();
+ }
+ for part in s.split(';') {
+ let part = part.trim();
+ if part.is_empty() {
+ continue;
+ }
+ let mut iter = part.splitn(2, '=');
+ if let (Some(k), Some(v)) = (iter.next(), iter.next()) {
+ let key = k.trim();
+ let val = v.trim();
+ if !key.is_empty() {
+ map.insert(key.to_string(), val.to_string());
+ }
}
}
- Err(anyhow!("Failed to get csrf_token"))
+ map
}
-async fn save_cookie(resp: &Response) -> Result<()> {
- let cookies_str: String = resp
- .cookies()
- .map(|c| format!("{}={}", c.name(), c.value()))
- .collect::>()
- .join(";");
-
+fn write_cookie_map(map: &HashMap) -> Result<()> {
+ if map.is_empty() {
+ return Err(anyhow!("Cookie is empty"));
+ }
let local_dir = PathBuf::from_str(&full(&LOCAL_DIR).unwrap())?;
if !local_dir.is_dir() {
create_dir_all(local_dir)?;
}
+ let mut keys: Vec<&String> = map.keys().collect();
+ keys.sort();
+ let cookies_str = keys
+ .into_iter()
+ .map(|k| format!("{}={}", k, map.get(k).unwrap()))
+ .collect::>()
+ .join("; ");
let mut file = File::create(full(&LOCAL_SESSION_PATH).unwrap().to_string())?;
file.write_all(cookies_str.as_bytes())?;
-
Ok(())
}
+fn save_cookie_string(cookies_str: &str) -> Result<()> {
+ let map = parse_cookie_string(cookies_str);
+ write_cookie_map(&map)
+}
+
+async fn save_cookie(resp: &Response) -> Result<()> {
+ let mut new_map: HashMap = HashMap::new();
+ for c in resp.cookies() {
+ new_map.insert(c.name().to_string(), c.value().to_string());
+ }
+ if new_map.is_empty() {
+ return Ok(());
+ }
+ let local_path = PathBuf::from_str(&full(&LOCAL_SESSION_PATH).unwrap())?;
+ let mut merged = if local_path.is_file() {
+ parse_cookie_string(&fs::read_to_string(local_path)?)
+ } else {
+ HashMap::new()
+ };
+ for (k, v) in new_map {
+ merged.insert(k, v);
+ }
+ write_cookie_map(&merged)
+}
+
pub async fn ac_logout() -> Result<()> {
let local_file = PathBuf::from_str(&full(&LOCAL_SESSION_PATH).unwrap())?;
if local_file.is_file() {
@@ -158,61 +184,115 @@ pub async fn ac_logout() -> Result<()> {
Ok(())
}
+fn load_cookie_headers() -> Result {
+ Ok(get_local_session()?.unwrap_or_default())
+}
+
pub async fn ac_login(acn: &ACN) -> Result<()> {
println!("{}", format!("{:-^30}", " Login ").blue());
- let shinobi = "🥷";
- let prompt = "Username".black();
- let username_prompt = format!(" {} {} ", shinobi, prompt).on_white().to_string();
- let username = dialoguer::Input::::new()
- .with_prompt(username_prompt)
+ let local_session_path = PathBuf::from_str(&full(&LOCAL_SESSION_PATH).unwrap())?;
+ let existing_cookie = if local_session_path.is_file() {
+ Some(fs::read_to_string(&local_session_path)?)
+ } else {
+ None
+ };
+ let mut existing_map: HashMap = HashMap::new();
+ if let Some(ref cookie) = existing_cookie {
+ for part in cookie.split(';') {
+ let part = part.trim();
+ if part.is_empty() {
+ continue;
+ }
+ let mut iter = part.splitn(2, '=');
+ if let (Some(k), Some(v)) = (iter.next(), iter.next()) {
+ existing_map.insert(k.trim().to_string(), v.trim().to_string());
+ }
+ }
+ }
+ println!(
+ "{}",
+ format!("Open {} in a browser and log in.", LOGIN_URL).green()
+ );
+ println!(
+ "{}",
+ "Copy REVEL_SESSION (required) and REVEL_FLASH (optional) from DevTools -> Application -> Cookies."
+ .green()
+ );
+ if existing_cookie.is_some() {
+ println!(
+ "{}",
+ "Existing cookie found. Press Enter to keep each value.".green()
+ );
+ }
+ let session = dialoguer::Password::new()
+ .with_prompt("REVEL_SESSION")
+ .allow_empty_password(existing_map.contains_key("REVEL_SESSION"))
.interact()?;
- let key = "🔒";
- let prompt = "Password".black();
- let password_prompt = format!(" {} {} ", key, prompt).on_white().to_string();
- let password = dialoguer::Password::new()
- .with_prompt(password_prompt)
+ let session = if session.trim().is_empty() {
+ existing_map
+ .get("REVEL_SESSION")
+ .cloned()
+ .ok_or_else(|| anyhow!("REVEL_SESSION is required"))?
+ } else {
+ session
+ };
+ let flash = dialoguer::Password::new()
+ .with_prompt("REVEL_FLASH (optional)")
+ .allow_empty_password(true)
.interact()?;
+ let flash = if flash.trim().is_empty() {
+ existing_map.get("REVEL_FLASH").cloned().unwrap_or_default()
+ } else {
+ flash
+ };
+ let cookie = if flash.trim().is_empty() {
+ format!("REVEL_SESSION={}", session)
+ } else {
+ format!("REVEL_SESSION={}; REVEL_FLASH={}", session, flash)
+ };
+ save_cookie_string(&cookie)?;
+ let _ = acn;
+ println!("{}", "Cookie saved. You are now logged in!".magenta());
- let csrf_token: String = get_csrf_token(acn, LOGIN_URL).await?;
-
- let params = [
- ("csrf_token", csrf_token.as_str()),
- ("username", username.as_str()),
- ("password", password.as_str()),
- ];
+ Ok(())
+}
+pub async fn ac_check_login(acn: &ACN) -> Result {
+ let cookies = load_cookie_headers()?;
+ if cookies.is_empty() {
+ return Ok(false);
+ }
let resp = acn
.client
- .post(LOGIN_URL)
- .headers(acn.cookies.clone().unwrap_or_default())
- .form(¶ms)
+ .get("https://atcoder.jp/home")
+ .headers(cookies)
.send()
- .await?;
+ .await?
+ .error_for_status()?;
save_cookie(&resp).await?;
-
- let doc = Html::parse_document(&resp.text().await?);
-
- if let Some(err) = doc
- .select(&Selector::parse("div.alert-danger").unwrap())
- .next()
+ let final_url = resp.url().to_string();
+ let body = resp.text().await?;
+ if final_url.contains("/login") {
+ return Ok(false);
+ }
+ let doc = Html::parse_document(&body);
+ let login_link_selector = Selector::parse("a[href^=\"/login\"]").unwrap();
+ let login_link_selector_abs = Selector::parse("a[href^=\"https://atcoder.jp/login\"]").unwrap();
+ let logout_link_selector = Selector::parse("a[href^=\"/logout\"]").unwrap();
+ let logout_form_selector = Selector::parse("form[action^=\"/logout\"]").unwrap();
+ if doc.select(&login_link_selector).next().is_some()
+ || doc.select(&login_link_selector_abs).next().is_some()
+ || body.contains("Sign In")
+ || body.contains("ログイン")
{
- let err_msg = err.last_child().unwrap().value().as_text().unwrap().trim();
- if !["You have already signed in.", "すでにログインしています。"].contains(&err_msg)
- {
- ac_logout().await?;
- }
- return Err(anyhow!(
- "Login failed! {}",
- err.last_child().unwrap().value().as_text().unwrap().trim()
- ));
+ return Ok(false);
}
-
- println!(
- "{}",
- format!("Hello {}, you are now logged in!", username).magenta()
- );
-
- Ok(())
+ let has_logout = doc.select(&logout_link_selector).next().is_some()
+ || doc.select(&logout_form_selector).next().is_some()
+ || body.contains("Sign Out")
+ || body.contains("ログアウト")
+ || body.contains("/logout");
+ Ok(has_logout)
}
pub fn get_local_session() -> Result