From 3d8d3662be4332240e26cbb3b2810ab95933105a Mon Sep 17 00:00:00 2001 From: UG Date: Sat, 7 Feb 2026 16:19:56 +0900 Subject: [PATCH 1/3] Remove language id and update docs --- CONFIG.md | 10 +- Cargo.lock | 185 +------------- Cargo.toml | 2 - LANG_ID.md | 120 --------- README.md | 31 ++- src/ac_scraper.rs | 587 ++++++++++++++------------------------------- src/config.rs | 34 +-- src/language_id.rs | 135 ----------- src/main.rs | 73 ++++-- 9 files changed, 259 insertions(+), 918 deletions(-) delete mode 100644 LANG_ID.md delete mode 100644 src/language_id.rs 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..f3ad0bb 100644 --- a/src/ac_scraper.rs +++ b/src/ac_scraper.rs @@ -1,25 +1,19 @@ 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, -}; +use reqwest::{header::{HeaderMap, HeaderValue, COOKIE}, 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 +23,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 +54,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 +104,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 +181,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> { @@ -220,345 +297,37 @@ pub fn get_local_session() -> Result> { if !local_session_path.is_file() { return Ok(None); } - let file = std::fs::File::open(local_session_path)?; - let reader = std::io::BufReader::new(file); - + let contents = fs::read_to_string(local_session_path)?; + let map = parse_cookie_string(&contents); + if map.is_empty() { + return Ok(None); + } + let cookies_str = map + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() + .join("; "); let mut cookie_headers = HeaderMap::new(); - reader.lines().for_each(|line| { - cookie_headers.insert(COOKIE, HeaderValue::from_str(&line.unwrap()).unwrap()); - }); + cookie_headers.insert(COOKIE, HeaderValue::from_str(&cookies_str).unwrap()); Ok(Some(cookie_headers)) } -struct Submission { - time: String, - name: String, - username: String, - lang: String, - score: String, - id: String, - status_str: String, -} - -fn get_submission_info_from_row(row: &ElementRef) -> Result { - let td_selector = Selector::parse("td").unwrap(); - let mut iter = row.select(&td_selector); - let time = iter - .next() - .unwrap() - .first_child() - .unwrap() - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - let time = chrono::DateTime::parse_from_str(&time, "%Y-%m-%d %H:%M:%S%z")?; - let time = time.format("%Y/%m/%d %H:%M:%S").to_string(); - let name = iter - .next() - .unwrap() - .first_child() - .unwrap() - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - let username = iter - .next() - .unwrap() - .first_child() - .unwrap() - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - let lang = iter - .next() - .unwrap() - .first_child() - .unwrap() - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - let score_parent = iter.next().unwrap(); - let id = score_parent.value().attr("data-id").unwrap().to_string(); - let score = score_parent - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - iter.next(); - let status_str = iter - .next() - .unwrap() - .first_child() - .unwrap() - .first_child() - .unwrap() - .value() - .as_text() - .unwrap() - .to_string(); - Ok(Submission { - time, - name, - username, - lang, - score, - id, - status_str, - }) -} - -fn make_submission_display(submission: &Submission) -> String { - let tate = " | ".blue(); - let score = format!("score: {}", submission.score); - format!( - "{}{}{}{}{}{}{}{}{}{}", - submission.time, - tate, - submission.name.green(), - tate, - submission.username, - tate, - submission.lang, - tate, - score, - tate - ) -} - -pub async fn ac_submit( - acn: &ACN, - problem_str_info: &ProblemStrInfo, - config_str_map: &ConfigStrMap, - config_map: &ConfigMap, -) -> Result<()> { - println!("{}", format!("{:-^30}", " Submit ").blue()); - let mut data_map: HashMap = HashMap::new(); - data_map.extend(config_str_map.iter().map(|(k, v)| (k.clone(), v.clone()))); - data_map.extend(problem_str_info.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let submit_file = str_format(config_str_map["source_file_path"].clone(), &data_map); - println!("{}{}", "Submit file: ".green(), submit_file); - let source = fs::read(full(&submit_file).unwrap().to_string()) - .with_context(|| format!("Failed to read {}", submit_file))?; - let source_str = String::from_utf8_lossy(&source); - - let submit_url = str_format(SUBMIT_URL.to_string(), &data_map); - let csrf_token: String = get_csrf_token(acn, submit_url.as_str()).await?; - - let task_screen_name = problem_str_info.get("task_screen_name").unwrap(); - let params = [ - ("data.TaskScreenName", task_screen_name.as_str()), - ( - "data.LanguageId", - &config_map.get("language_id").unwrap().to_string(), - ), - ("sourceCode", &source_str), - ("csrf_token", csrf_token.as_str()), - ]; - - println!( - "{}", - str_format( - "Submitting to {{CONTEST_TYPE}}{{CONTEST_ID}} {{PROBLEM_ID}} ...".to_string(), - &data_map - ) - .green() - ); - let resp = acn - .client - .post(submit_url.as_str()) - .headers(acn.cookies.clone().unwrap_or_default()) - .form(¶ms) - .send() - .await?; - - if resp.status() != StatusCode::OK { - ac_logout().await?; - return Err(anyhow!( - "Submission failed. You may need to login. Try again!" - )); - } - - println!("{}", "Submitted".green()); - - // check submission result - let mut submission_result: Status = Status::WJ; - let mut submission_id: Option = None; - let mut all: u64 = 5000; - let mut done: u64 = 0; - let bar_init_style = ProgressStyle::with_template("{msg} {bar:80.green/white}") - .unwrap() - .progress_chars("##-"); - let bar_green_style = - ProgressStyle::with_template("{msg} {bar:80.green/white} {pos:>3}/{len:>3}") - .unwrap() - .progress_chars("##-"); - let bar_red_style = ProgressStyle::with_template("{msg} {bar:80.red/white} {pos:>3}/{len:>3}") - .unwrap() - .progress_chars("##-"); - let bar_yellow_style = - ProgressStyle::with_template("{msg} {bar:80.yellow/white} {pos:>3}/{len:>3}") - .unwrap() - .progress_chars("##-"); - let bar_finish_style = ProgressStyle::with_template("{msg}") - .unwrap() - .progress_chars("##-"); - - let pb = ProgressBar::new(all) - .with_message(submission_result.as_display_string().reverse().to_string()) - .with_position(done) - .with_style(bar_init_style); - - let mut finish = false; - let mut finish_msg = String::from(""); - let mut timeout_cnt = 0; - let status_re = Regex::new(r"^(\d+) */ *(\d+) *(.*)$").unwrap(); - while !finish { - let submissions_url = str_format(SUBMISSIONS_URL.to_string(), &data_map); - let req = acn - .client - .get(submissions_url) - .headers(acn.cookies.clone().unwrap_or_default()) - .timeout(tokio::time::Duration::from_millis(2000)); - let resp = req.send().await; - - if let Err(e) = resp { - if e.is_timeout() { - timeout_cnt += 1; - if timeout_cnt > 20 { - return Err(anyhow!("A lot of timeouts happend. Something went wrong.")); - } - continue; - } else { - return Err(e.into()); - } - } - - let body = resp.unwrap().text().await; - - if let Err(e) = body { - if e.is_timeout() { - timeout_cnt += 1; - timeout_cnt += 1; - if timeout_cnt > 20 { - return Err(anyhow!("A lot of timeouts happend. Something went wrong.")); - } - continue; - } else { - return Err(e.into()); - } - } - - let doc = Html::parse_document(&body.unwrap()); - - finish_msg = if let Some(submission_id_value) = submission_id { - let td_selector = - Selector::parse(format!("td[data-id=\"{}\"]", submission_id_value).as_str()) - .unwrap(); - let target_row = - ElementRef::wrap(doc.select(&td_selector).next().unwrap().parent().unwrap()) - .unwrap(); - let submission = get_submission_info_from_row(&target_row)?; - let status = Status::from_table_str(&submission.status_str); - if status.as_str() != submission.status_str { - if let Some(caps) = status_re.captures(&submission.status_str) { - done = caps.get(1).unwrap().as_str().parse::().unwrap(); - all = caps.get(2).unwrap().as_str().parse::().unwrap(); - pb.set_length(all); - pb.set_position(done); - } - } else if status != Status::WJ { - finish = true; - } - if status != Status::WJ { - let style = match status { - Status::AC => bar_green_style.clone(), - Status::WA => bar_red_style.clone(), - _ => bar_yellow_style.clone(), - }; - pb.set_style(style); - pb.tick(); - } - let msg = format!( - "{} [ {} ]\n", - make_submission_display(&submission), - status.as_display_string().reverse() - ); - submission_result = status; - pb.set_message(msg.clone()); - msg - } else { - let tr_selector = Selector::parse("table tbody tr").unwrap(); - let latest_row = doc.select(&tr_selector).next().unwrap(); - let submission = get_submission_info_from_row(&latest_row)?; - submission_id = Some(submission.id.parse::().unwrap()); - let status = Status::from_table_str(&submission.status_str); - if status != Status::WJ { - let style = match status { - Status::AC => bar_green_style.clone(), - Status::WA => bar_red_style.clone(), - _ => bar_yellow_style.clone(), - }; - pb.set_style(style); - pb.tick(); - } - if status.as_str() != submission.status_str { - if let Some(caps) = status_re.captures(&submission.status_str) { - done = caps.get(1).unwrap().as_str().parse::().unwrap(); - all = caps.get(2).unwrap().as_str().parse::().unwrap(); - pb.set_length(all); - pb.set_position(done); - } - } else if status != Status::WJ { - finish = true; - } - let msg = format!( - "{} [ {} ]\n", - make_submission_display(&submission), - status.as_display_string().reverse() - ); - submission_result = status; - pb.set_message(msg.clone()); - msg - }; - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } - pb.set_style(bar_finish_style.clone()); - pb.tick(); - pb.finish_with_message(finish_msg); - - println!("\nFinished with {}", submission_result.as_display_string()); - - Ok(()) -} - pub async fn get_sample_cases( problem_str_info: &ProblemStrInfo, acn: &ACN, sample_case_id_arg: Option, ) -> Result { let problem_url = str_format(PROBLEM_URL.to_string(), problem_str_info); - let body = acn + let cookies = load_cookie_headers()?; + let resp = acn .client .get(problem_url) - .headers(acn.cookies.clone().unwrap_or_default()) + .headers(cookies) .send() .await? - .error_for_status()? + .error_for_status()?; + save_cookie(&resp).await?; + let body = resp .text() .await .with_context(|| "Failed to get sample cases. Please check you logged in and try again.")?; diff --git a/src/config.rs b/src/config.rs index 2334575..b3a50db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,6 @@ use std::{collections::HashMap, env, fs, io::Write}; use crate::ac_scraper::add_task_name_to_problem_info; use crate::data::ACN; -use crate::language_id::lang_to_id; use crate::{util::*, GlobalArgs}; use serde::{Deserialize, Serialize}; @@ -18,28 +17,19 @@ const DEFAULT_CONFIG: &str = "#config.toml # # 設定に必須な情報はcontest_dir, source_file_path, need_to_compile, -# execute_command, (language_id or language_name)です. +# execute_commandです. # # -------------------------------------------------------------------------------------- # contest_dir: ac-ninjaを実行するディレクトリです. # {{contesty_type}},{{contest_id}}を特定できる必要があります # -------------------------------------------------------------------------------------- -# source_file_path: ac-ninjaで提出するファイルのパスです. +# source_file_path: ac-ninjaで扱うソースファイルのパスです. # -------------------------------------------------------------------------------------- # need_to_compile: プログラムの実行にコンパイルが必要かどうかを指定します. # trueの場合, {{compile_command}}を指定する必要があります. # -------------------------------------------------------------------------------------- # execute_command: プログラムを実行するためのコマンドです. # -------------------------------------------------------------------------------------- -# language_id: ac-ninjaでの提出に用いる言語のidです. -# AtCoderの提出セレクトボックスをディベロッパーツールから見ることで -# 確認できますが, [早見表](https://github.com/UUGTech/AtCoderNinja/blob/main/LANG_ID.md)が便利です. -# -------------------------------------------------------------------------------------- -# language_name: language_idの代わりに, language_nameを指定することができます. -# AtCoderの提出言語セレクトボックスの表示の通りに指定してください. -# \"C++(GCC 9.2.1)\", \"Python (3.8.2)\", \"Rust (1.42.0)\"など. -# こちらも, [早見表](https://github.com/UUGTech/AtCoderNinja/blob/main/LANG_ID.md)の文字列をコピペすると便利です. -# -------------------------------------------------------------------------------------- # ファイルパスや, 実行コマンドには{{変数}}を含むことができます. # {{contest_type}}, {{contest_id}}, {{problem_id}}以外の変数は # config.toml内で解決可能である必要があります. @@ -60,8 +50,6 @@ const DEFAULT_CONFIG: &str = # 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 = 4003 - "; const PROBLEM_INFO_PRINT_FORMAT: &str = "{{CONTEST_TYPE}} {{contest_id}} {{PROBLEM_ID}}"; @@ -422,10 +410,6 @@ fn config_check(mut config_map: ConfigMap) -> Result { miss.push("compile_command"); } - if !config_map.contains_key("language_id") && !config_map.contains_key("language_name") { - miss.push("language_( id | name )"); - } - if !miss.is_empty() { let miss_str: Vec = miss.iter().map(|&s| s.to_string()).collect(); return Err(anyhow!( @@ -434,20 +418,6 @@ fn config_check(mut config_map: ConfigMap) -> Result { )); } - let lang_id: i64 = if config_map.contains_key("language_name") { - let lang_name = &config_map.get("language_name").unwrap().to_string(); - lang_to_id(lang_name)? - } else { - let id_in_config = &config_map.get("language_id").unwrap().to_string(); - let id = id_in_config.parse::(); - if id.is_err() { - return Err(anyhow!("language_id must be a number")); - } - id.unwrap() - }; - - config_map.insert_integer("language_id".to_string(), lang_id); - Ok(config_map) } diff --git a/src/language_id.rs b/src/language_id.rs deleted file mode 100644 index 1c43d74..0000000 --- a/src/language_id.rs +++ /dev/null @@ -1,135 +0,0 @@ -use anyhow::{anyhow, Result}; - -pub fn lang_to_id(lang_name: &str) -> Result { - match lang_name { - "><> (fishr 0.1.0)" => Ok(6001), - "Ada 2022 (GNAT 15.2.0)" => Ok(6002), - "APL (GNU APL 1.9)" => Ok(6003), - "Assembly MIPS O32 ABI (GNU assembler 2.42)" => Ok(6004), - "Assembly x64 (NASM 2.16.03)" => Ok(6005), - "AWK (GNU awk 5.2.1)" => Ok(6006), - "A言語 (interpreter af48a2a)" => Ok(6007), - "Bash (bash 5.3)" => Ok(6008), - "BASIC (FreeBASIC 1.10.1)" => Ok(6009), - "bc (GNU bc 1.08.2)" => Ok(6010), - "Befunge93 (TBC 1.0)" => Ok(6011), - "Brainfuck (Tritium 1.2.73)" => Ok(6012), - "C23 (Clang 21.1.0)" => Ok(6013), - "C23 (GCC 14.2.0)" => Ok(6014), - "C# 13.0 (.NET 9.0.8)" => Ok(6015), - "C# 13.0 (.NET Native AOT 9.0.8)" => Ok(6016), - "C++23 (GCC 15.2.0)" => Ok(6017), - "C3 (c3c 0.7.5)" => Ok(6018), - "Carp(Carp 0.5.5)" => Ok(6019), - "cLay (cLay 20250308-1 (GCC 15.2.0))" => Ok(6020), - "Clojure (babashka 1.12.208)" => Ok(6021), - "Clojure (clojure 1.12.2)" => Ok(6022), - "Clojure (Clojure AOT 1.12.2)" => Ok(6023), - "Clojure (ClojureScript 1.12.42 (Clojure 1.12.2 Node.js 22.19.0))" => Ok(6025), - "COBOL (Free) (GnuCOBOL 3.2)" => Ok(6026), - "Common Lisp (SBCL 2.5.8)" => Ok(6027), - "Crystal (Crystal 1.17.0)" => Ok(6028), - "Cyber (Cyber v0.3)" => Ok(6029), - "D (DMD 2.111.0)" => Ok(6030), - "D (GDC 15.2)" => Ok(6031), - "D (LDC 1.41.0)" => Ok(6032), - "Dart (Dart 3.9.2)" => Ok(6033), - "dc 1.5.2 (GNU bc 1.08.2)" => Ok(6034), - "ECLiPSe (ECLiPSe 7.1_13)" => Ok(6035), - "Eiffel (Gobo Eiffel 22.01)" => Ok(6036), - "Eiffel (Liberty Eiffel 07829e3)" => Ok(6037), - "Elixir (Elixir 1.18.4 (OTP 28.0.2))" => Ok(6038), - "Emacs Lisp(Native Compile)(GNU Emacs 29.4)" => Ok(6039), - "Emojicode 1.0 beta 2 (emojicodec 1.0 beta 2)" => Ok(6040), - "Erlang (Erlang 28.0.2)" => Ok(6041), - "F# 9.0 (.NET 9.0.8)" => Ok(6042), - "Factor (Factor 0.100)" => Ok(6043), - "Fish (fish 4.0.2)" => Ok(6044), - "Forth (gforth 0.7.3)" => Ok(6045), - "Fortran2018 (Flang 20.1.7)" => Ok(6046), - "Fortran2023 (GCC 14.2.0)" => Ok(6047), - "FORTRAN77 (GCC 14.2.0)" => Ok(6048), - "Gleam (Gleam 1.12.0 (OTP 28.0.2))" => Ok(6049), - "Go 1.18 (gccgo 15.2.0)" => Ok(6050), - "Go (go 1.25.1)" => Ok(6051), - "Haskell (GHC 9.8.4)" => Ok(6052), - "Haxe/JVM 4.3.7 (hxjava 4.2.0)" => Ok(6053), - "C++ IOI-Style(GNU++20) (GCC 14.2.0)" => Ok(6054), - "ISLisp (Easy-ISLisp 5.43)" => Ok(6055), - "Java24 (OpenJDK 24.0.2)" => Ok(6056), - "JavaScript (Bun 1.2.21)" => Ok(6057), - "JavaScript (Deno 2.4.5)" => Ok(6058), - "JavaScript (Node.js 22.19.0)" => Ok(6059), - "Jule (jule 0.1.6)" => Ok(6060), - "Koka (koka v3.2.2)" => Ok(6061), - "Kotlin (Kotlin/JVM 2.2.10)" => Ok(6062), - "Kuin (kuincl v.2021.8.17)" => Ok(6063), - "Lazy K (irori v1.0.0)" => Ok(6064), - "Lean (lean v4.22.0)" => Ok(6065), - "LLVM IR (Clang 21.1.0)" => Ok(6066), - "Lua (Lua 5.4.7)" => Ok(6067), - "Lua (LuaJIT 2.1.1703358377)" => Ok(6068), - "Mercury (Mercury 22.01.8)" => Ok(6069), - "Nim (Nim 1.6.20)" => Ok(6071), - "Nim (Nim 2.2.4)" => Ok(6072), - "OCaml (ocamlopt 5.3.0)" => Ok(6073), - "Octave (GNU Octave 10.2.0)" => Ok(6074), - "Pascal (fpc 3.2.2)" => Ok(6075), - "Perl (perl 5.38.2)" => Ok(6076), - "PHP (PHP 8.4.12)" => Ok(6077), - "Piet (your-diary/piet_programming_language 3.0.0) (PPM image)" => Ok(6078), - "Pony (ponyc 0.59.0)" => Ok(6079), - "PowerShell (PowerShell 7.5.2)" => Ok(6080), - "Prolog (SWI-Prolog 9.2.9)" => Ok(6081), - "Python (CPython 3.13.7)" => Ok(6082), - "Python (PyPy 3.11-v7.3.20)" => Ok(6083), - "R (GNU R 4.5.0)" => Ok(6084), - "ReasonML (reson 3.16.0)" => Ok(6085), - "Ruby 3.3 (truffleruby 25.0.0)" => Ok(6086), - "Ruby 3.4 (ruby 3.4.5)" => Ok(6087), - "Rust (rustc 1.89.0)" => Ok(6088), - "SageMath (SageMath 10.7)" => Ok(6089), - "Scala (Dotty 3.7.2)" => Ok(6090), - "Scala 3.7.2 (Scala Native 0.5.8)" => Ok(6091), - "Scheme (ChezScheme 10.2.0)" => Ok(6092), - "Scheme (Gauche 0.9.15)" => Ok(6093), - "Seed7 (Seed7 3.5.0)" => Ok(6094), - "Swift 6.2" => Ok(6095), - "Tcl (tclsh 9.0.1)" => Ok(6096), - "Terra (Terra 1.2.0)" => Ok(6097), - "TeX (tex 3.141592653)" => Ok(6098), - "Text (cat 9.4)" => Ok(6099), - "TypeScript 5.8 (Deno 2.4.5)" => Ok(6100), - "TypeScript 5.9 (tsc 5.9.2 (Bun 1.2.21))" => Ok(6101), - "TypeScript 5.9 (tsc 5.9.2 (Node.js 22.19.0))" => Ok(6102), - "Uiua (uiua 0.16.2)" => Ok(6103), - "Unison (Unison 0.5.47)" => Ok(6104), - "V (0.4.10)" => Ok(6105), - "Vala (valac 0.56.18)" => Ok(6106), - "Verilog 2012 (Icarus Verilog 12.0)" => Ok(6107), - "Veryl (veryl 0.16.4)" => Ok(6108), - "WebAssembly (wabt 1.0.34 + iwasm 2.4.1)" => Ok(6109), - "Whitespace (whitespacers 1.3.0)" => Ok(6110), - "Zig (Zig 0.15.1)" => Ok(6111), - "なでしこ (cnako3 3.7.8 (Node.js 22.19.0))" => Ok(6112), - "プロデル (mono版プロデル 2.0.1353)" => Ok(6113), - "Julia (Julia 1.11.6)" => Ok(6114), - "Python (Codon 0.19.3)" => Ok(6115), - "C++23 (Clang 21.1.0)" => Ok(6116), - "Fix (1.1.0-alpha.12)" => Ok(6117), - "SQL (DuckDB 1.3.2)" => Ok(6118), - _ => Err(anyhow!("Unknown language name error.")), - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_lang_to_id() { - assert_eq!(6001, lang_to_id("><> (fishr 0.1.0)").unwrap()); - assert_eq!(6118, lang_to_id("SQL (DuckDB 1.3.2)").unwrap()); - assert!(lang_to_id("Vim").is_err()); - } -} diff --git a/src/main.rs b/src/main.rs index 5a40236..f7effb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,15 @@ mod ac_scraper; mod check_samples; mod config; mod data; -mod language_id; mod util; -use std::path::PathBuf; +use std::{ + collections::HashMap, + fs, + io::Write, + path::PathBuf, + process::{Command, Stdio}, +}; use ac_scraper::*; use anyhow::Result; @@ -14,6 +19,8 @@ use clap::{Args, Parser, Subcommand, ValueEnum}; use colored::*; use config::*; use data::*; +use shellexpand::full; +use util::str_format; #[derive(Debug, Parser)] #[command( @@ -34,15 +41,15 @@ struct Cli { #[derive(Debug, Args)] pub struct GlobalArgs { - /// Without submit, only test samples + /// Only test samples (skip clipboard copy) #[arg(short, long)] pub local: bool, - /// Force to submit, even if ther result of the samples is not AC + /// Copy to clipboard even if sample results are not AC #[arg(short, long)] pub force: bool, - /// Manual input ** This option don't allow to submit ** + /// Manual input ** This option doesn't run sample check ** #[arg(short, long)] pub insert: bool, @@ -118,6 +125,8 @@ enum MiniCommand { Login, /// Logout, delete session file from local Logout, + /// Check if local session cookie is valid + LoginCheck, } #[tokio::main] @@ -138,6 +147,15 @@ async fn main() -> Result<()> { println!("{}", "You are now logged out".green()); return Ok(()); } + MiniCommand::LoginCheck => { + let ok = ac_check_login(&acn).await?; + if ok { + println!("{}", "Session is valid.".green()); + } else { + println!("{}", "Session is invalid. Run `ac-ninja login`.".yellow()); + } + return Ok(()); + } } } @@ -179,19 +197,40 @@ async fn main() -> Result<()> { display_failed_detail(sample_results.failed_details); } - if (sample_results.total_status != Status::AC && !cli_args.force) - || cli_args.local - || cli_args.sample_case_id_arg.is_some() - { - return Ok(()); + let should_copy = (sample_results.total_status == Status::AC || cli_args.force) + && !cli_args.local + && cli_args.sample_case_id_arg.is_none(); + if should_copy { + if let Err(e) = copy_source_to_clipboard(&problem_str_info, &acn.config_str_map) { + eprintln!("{} {}", "Failed to copy to clipboard:".red(), e); + } else { + println!("{}", "Source copied to clipboard.".green()); + } } - ac_submit( - &acn, - &problem_str_info, - &acn.config_str_map, - &acn.config_map, - ) - .await?; Ok(()) } + +fn copy_source_to_clipboard( + problem_str_info: &ProblemStrInfo, + config_str_map: &ConfigStrMap, +) -> Result<()> { + let mut data_map: HashMap = HashMap::new(); + data_map.extend(config_str_map.iter().map(|(k, v)| (k.clone(), v.clone()))); + data_map.extend(problem_str_info.iter().map(|(k, v)| (k.clone(), v.clone()))); + let source_file = str_format(config_str_map["source_file_path"].clone(), &data_map); + let source_path = full(&source_file)?.to_string(); + let source = fs::read(&source_path)?; + + let mut child = Command::new("pbcopy") + .stdin(Stdio::piped()) + .spawn()?; + if let Some(stdin) = child.stdin.as_mut() { + stdin.write_all(&source)?; + } + let status = child.wait()?; + if !status.success() { + return Err(anyhow::anyhow!("pbcopy failed")); + } + Ok(()) +} From 17920868cdbbbb75ad83ac12e89e0e587254896b Mon Sep 17 00:00:00 2001 From: UG Date: Sat, 7 Feb 2026 16:21:49 +0900 Subject: [PATCH 2/3] cargo fmt --- src/ac_scraper.rs | 5 ++++- src/config.rs | 3 +-- src/main.rs | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ac_scraper.rs b/src/ac_scraper.rs index f3ad0bb..955e61a 100644 --- a/src/ac_scraper.rs +++ b/src/ac_scraper.rs @@ -8,7 +8,10 @@ use std::{ use anyhow::{anyhow, Context, Result}; use colored::*; -use reqwest::{header::{HeaderMap, HeaderValue, COOKIE}, Response}; +use reqwest::{ + header::{HeaderMap, HeaderValue, COOKIE}, + Response, +}; use scraper::{ElementRef, Html, Selector}; use shellexpand::full; diff --git a/src/config.rs b/src/config.rs index b3a50db..26da721 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,8 +13,7 @@ use std::path::Path; const CONFIG_DIR: &str = "~/.config/ac-ninja"; const CONFIG_PATH: &str = "~/.config/ac-ninja/config.toml"; -const DEFAULT_CONFIG: &str = -"#config.toml +const DEFAULT_CONFIG: &str = "#config.toml # # 設定に必須な情報はcontest_dir, source_file_path, need_to_compile, # execute_commandです. diff --git a/src/main.rs b/src/main.rs index f7effb3..c48f9d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -222,9 +222,7 @@ fn copy_source_to_clipboard( let source_path = full(&source_file)?.to_string(); let source = fs::read(&source_path)?; - let mut child = Command::new("pbcopy") - .stdin(Stdio::piped()) - .spawn()?; + let mut child = Command::new("pbcopy").stdin(Stdio::piped()).spawn()?; if let Some(stdin) = child.stdin.as_mut() { stdin.write_all(&source)?; } From 1a30962925741a78962371de1a1b72bc0d60b3c9 Mon Sep 17 00:00:00 2001 From: UG Date: Sat, 7 Feb 2026 16:24:04 +0900 Subject: [PATCH 3/3] Remove unused sample statuses --- src/check_samples.rs | 48 -------------------------------------------- src/config.rs | 2 +- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/src/check_samples.rs b/src/check_samples.rs index 5e7553d..d6e6456 100644 --- a/src/check_samples.rs +++ b/src/check_samples.rs @@ -1,7 +1,6 @@ use anyhow::Result; use colored::*; use prettytable::{format, row, table, Table}; -use regex::Regex; use std::collections::HashMap; use std::io::Write; use std::process::{Command, Stdio}; @@ -18,55 +17,15 @@ use crate::util::split_one_line_command; pub enum Status { AC, WA, - TLE, - RE, CE, - WJ, } impl Status { - pub fn as_str(&self) -> &str { - match self { - Status::AC => "AC", - Status::WA => "WA", - Status::TLE => "TLE", - Status::RE => "RE", - Status::CE => "CE", - Status::WJ => "WJ", - } - } pub fn as_display_string(&self) -> ColoredString { match self { Status::AC => "AC".green(), Status::WA => "!! WA !!".red(), - Status::TLE => "!! TLE !!".yellow(), - Status::RE => "!! RE !!".yellow(), Status::CE => "!! CE !!".yellow(), - Status::WJ => "WJ".into(), - } - } - - pub fn from_table_str(v: &str) -> Self { - match v { - "AC" => Status::AC, - "WA" => Status::WA, - "RE" => Status::RE, - "TLE" => Status::TLE, - "CE" => Status::CE, - "WJ" => Status::WJ, - _ => { - let re = Regex::new(r"^(\d+) */ *(\d+) *(.*)$").unwrap(); - if let Some(caps) = re.captures(v) { - let status = caps[3].trim(); - if status.is_empty() { - Status::AC - } else { - Status::from_table_str(status) - } - } else { - Status::WJ - } - } } } } @@ -75,14 +34,10 @@ pub struct FailedDetail { pub index: usize, pub input: String, pub expected: String, - #[allow(dead_code)] - pub status: Status, pub output: String, } pub struct SampleResults { - #[allow(dead_code)] - pub size: usize, pub total_status: Status, pub failed_details: Vec, } @@ -187,7 +142,6 @@ pub fn sample_check( table = add_total_status_to_table(table, &total_status); table.printstd(); return Ok(SampleResults { - size: 0, total_status, failed_details: vec![], }); @@ -229,7 +183,6 @@ pub fn sample_check( failed_details.push(FailedDetail { index: sample_id, input: samples.inputs[i].clone().1, - status: Status::WA, expected, output: output_str, }); @@ -242,7 +195,6 @@ pub fn sample_check( table.printstd(); Ok(SampleResults { - size: samples.size, total_status, failed_details, }) diff --git a/src/config.rs b/src/config.rs index 26da721..82fbbf1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -386,7 +386,7 @@ pub fn get_config(args: &Option) -> Result { Ok(config_map) } -fn config_check(mut config_map: ConfigMap) -> Result { +fn config_check(config_map: ConfigMap) -> Result { let need = [ "need_to_compile", "contest_dir",