├── .github ├── actions │ ├── add-target │ │ └── action.yml │ ├── build │ │ └── action.yml │ ├── check │ │ └── action.yml │ ├── make-archive │ │ └── action.yml │ ├── replace-version │ │ └── action.yml │ └── test │ │ └── action.yml └── workflows │ ├── build-dev.yml │ └── build-release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rust-toolchain.toml ├── shell.nix └── src ├── api ├── auth │ ├── mod.rs │ └── session.rs ├── fav │ ├── get_list.rs │ └── mod.rs ├── ing │ ├── comment.rs │ ├── get_comment_list.rs │ ├── get_list.rs │ ├── mod.rs │ └── publish.rs ├── mod.rs ├── news │ ├── get_body.rs │ ├── get_list.rs │ └── mod.rs ├── post │ ├── create.rs │ ├── del_one.rs │ ├── get_comment_list.rs │ ├── get_count.rs │ ├── get_meta_list.rs │ ├── get_one.rs │ ├── get_one_raw.rs │ ├── mod.rs │ ├── search.rs │ ├── search_self.rs │ ├── search_site.rs │ └── update.rs └── user │ ├── info.rs │ └── mod.rs ├── args ├── cmd │ ├── fav.rs │ ├── ing.rs │ ├── mod.rs │ ├── news.rs │ ├── post.rs │ └── user.rs ├── mod.rs └── parser │ ├── fav.rs │ ├── ing.rs │ ├── mod.rs │ ├── news.rs │ ├── post.rs │ └── user.rs ├── display ├── colorful │ ├── fav.rs │ ├── ing.rs │ ├── mod.rs │ ├── news.rs │ ├── post.rs │ └── user.rs ├── json │ ├── fav.rs │ ├── ing.rs │ ├── mod.rs │ ├── news.rs │ ├── post.rs │ └── user.rs ├── mod.rs └── normal │ ├── fav.rs │ ├── ing.rs │ ├── mod.rs │ ├── news.rs │ ├── post.rs │ └── user.rs ├── infra ├── fp.rs ├── http.rs ├── infer.rs ├── iter.rs ├── json.rs ├── mod.rs ├── option.rs ├── result.rs ├── str.rs ├── terminal.rs ├── time.rs └── vec.rs └── main.rs /.github/actions/add-target/action.yml: -------------------------------------------------------------------------------- 1 | name: Add Target 2 | description: Add target via rustup 3 | inputs: 4 | target: 5 | description: Target triple 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Add target 11 | shell: bash 12 | run: rustup target add ${{ inputs.target }} 13 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | description: Build artifacts 3 | inputs: 4 | release: 5 | description: Build in release mode 6 | required: true 7 | target: 8 | description: Build for the target triple 9 | required: true 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Build dev 14 | if: ${{ inputs.release == 'false' }} 15 | shell: bash 16 | run: cargo build --target ${{ inputs.target }} 17 | 18 | - name: Build release 19 | if: ${{ inputs.release == 'true' }} 20 | shell: bash 21 | run: cargo build --target ${{ inputs.target }} -r 22 | -------------------------------------------------------------------------------- /.github/actions/check/action.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | description: Check the code with audit, fmt and clippy 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Audit 7 | shell: bash 8 | run: | 9 | cargo install cargo-audit 10 | cargo audit 11 | 12 | - name: Fmt 13 | shell: bash 14 | run: cargo fmt --check 15 | 16 | - name: Clippy 17 | shell: bash 18 | # You need to run 'cargo clippy -r' in the local to get the same output with CI 19 | run: cargo clippy -- -D warnings 20 | -------------------------------------------------------------------------------- /.github/actions/make-archive/action.yml: -------------------------------------------------------------------------------- 1 | name: Make Archive 2 | description: Archive files to zip 3 | inputs: 4 | files: 5 | description: files to archive 6 | required: true 7 | out: 8 | description: Output path 9 | required: true 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Archive (UNIX) 14 | if: runner.os != 'Windows' 15 | shell: bash 16 | run: | 17 | zip ${{ inputs.out }} ${{ inputs.files }} -j 18 | 19 | - name: Archive (Windows) 20 | if: runner.os == 'Windows' 21 | shell: pwsh 22 | run: | 23 | Compress-Archive ${{ inputs.files }} ${{ inputs.out }} 24 | -------------------------------------------------------------------------------- /.github/actions/replace-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Replace version 2 | description: Replace package version in Cargo.toml 3 | inputs: 4 | version: 5 | description: Version to set 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Replace version 11 | shell: bash 12 | # There isn't a sed command working on all platforms to replace string in a file, so use perl. 13 | run: perl -i -pe's/0.0.0-dev/${{ inputs.version }}/' Cargo.toml 14 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | description: Run tests 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Test dev 7 | shell: bash 8 | run: cargo test --verbose 9 | 10 | - name: Test release 11 | shell: bash 12 | run: cargo test --verbose --release 13 | -------------------------------------------------------------------------------- /.github/workflows/build-dev.yml: -------------------------------------------------------------------------------- 1 | name: Build / Development 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request_target: 8 | types: 9 | - edited 10 | - opened 11 | - reopened 12 | - synchronize 13 | 14 | jobs: 15 | build-dev: 16 | name: ${{ matrix.targets.alias }} 17 | runs-on: ${{ matrix.targets.os }} 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | targets: 23 | # aarch64 24 | - { os: macos-11 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-11 } 25 | - { os: macos-12 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-12 } 26 | - { os: macos-13 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-13 } 27 | # amd64 28 | - { os: macos-12 , target: x86_64-apple-darwin , alias: amd64-darwin-macos-unknown } 29 | - { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-20.04 } 30 | - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-22.04 } 31 | - { os: ubuntu-22.04, target: x86_64-unknown-linux-musl, alias: amd64-musl-linux-unknown } 32 | - { os: windows-2019, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2019 } 33 | - { os: windows-2022, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2022 } 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v3 38 | with: 39 | ref: ${{ github.event.pull_request.head.sha }} 40 | 41 | - name: Setup Rust toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | override: true 46 | toolchain: nightly-2023-09-06 47 | components: rustfmt, clippy 48 | 49 | - name: Show Rust toolchain version 50 | shell: bash 51 | run: | 52 | cargo -V 53 | cargo clippy -V 54 | cargo fmt -- -V 55 | rustc -V 56 | 57 | - name: Setup musl-tools 58 | if: matrix.targets.target == 'x86_64-unknown-linux-musl' 59 | shell: bash 60 | run: sudo apt -y install musl-tools 61 | 62 | - name: Add target 63 | uses: ./.github/actions/add-target 64 | with: 65 | target: ${{ matrix.targets.target }} 66 | 67 | - name: Setup Rust cache 68 | uses: Swatinem/rust-cache@v2 69 | with: 70 | prefix-key: ${{ matrix.targets.alias }} 71 | 72 | - name: Generate version 73 | id: gen-version 74 | shell: bash 75 | run: echo 'VERSION=0.0.0-${{ github.sha }}' >> $GITHUB_OUTPUT 76 | 77 | - name: Replace version 78 | uses: ./.github/actions/replace-version 79 | with: 80 | version: ${{ steps.gen-version.outputs.VERSION }} 81 | 82 | - name: Run check 83 | uses: ./.github/actions/check 84 | 85 | - name: Run test 86 | uses: ./.github/actions/test 87 | 88 | - name: Run build 89 | uses: ./.github/actions/build 90 | with: 91 | target: ${{ matrix.targets.target }} 92 | release: false 93 | 94 | - name: Generate artifacts name 95 | id: gen-name 96 | shell: bash 97 | run: echo 'NAME=cnb-dev-${{ matrix.targets.alias }}' >> $GITHUB_OUTPUT 98 | 99 | - name: Generate binary extension 100 | id: gen-ext 101 | if: runner.os == 'Windows' 102 | shell: bash 103 | run: echo 'EXT=.exe' >> $GITHUB_OUTPUT 104 | 105 | - name: Upload artifacts 106 | uses: actions/upload-artifact@v3 107 | with: 108 | name: ${{ steps.gen-name.outputs.NAME }} 109 | path: ./target/${{ matrix.targets.target }}/debug/cnb${{ steps.gen-ext.outputs.EXT }} 110 | if-no-files-found: error 111 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build / Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | build-release: 10 | name: ${{ matrix.targets.alias }} 11 | runs-on: ${{ matrix.targets.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | targets: 17 | # aarch64 18 | - { os: macos-11 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-11 } 19 | - { os: macos-12 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-12 } 20 | - { os: macos-13 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-13 } 21 | # amd64 22 | - { os: macos-12 , target: x86_64-apple-darwin , alias: amd64-darwin-macos-unknown } 23 | - { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-20.04 } 24 | - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-22.04 } 25 | - { os: ubuntu-22.04, target: x86_64-unknown-linux-musl, alias: amd64-musl-linux-unknown } 26 | - { os: windows-2019, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2019 } 27 | - { os: windows-2022, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2022 } 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | with: 33 | ref: ${{ github.event.pull_request.head.sha }} 34 | 35 | - name: Setup Rust toolchain 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | profile: minimal 39 | override: true 40 | toolchain: nightly-2023-09-06 41 | components: rustfmt, clippy 42 | 43 | - name: Setup musl-tools 44 | if: matrix.targets.target == 'x86_64-unknown-linux-musl' 45 | shell: bash 46 | run: sudo apt -y install musl-tools 47 | 48 | - name: Add target 49 | uses: ./.github/actions/add-target 50 | with: 51 | target: ${{ matrix.targets.target }} 52 | 53 | - name: Setup Rust cache 54 | uses: Swatinem/rust-cache@v2 55 | with: 56 | prefix-key: ${{ matrix.targets.alias }} 57 | 58 | - name: Generate version 59 | id: gen-version 60 | shell: bash 61 | run: echo 'VERSION=${{ github.ref_name }}' | sed 's/v//' >> $GITHUB_OUTPUT 62 | 63 | - name: Replace version 64 | uses: ./.github/actions/replace-version 65 | with: 66 | version: ${{ steps.gen-version.outputs.VERSION }} 67 | 68 | - name: Run build 69 | uses: ./.github/actions/build 70 | with: 71 | target: ${{ matrix.targets.target }} 72 | release: true 73 | 74 | - name: Generate artifacts name 75 | id: gen-name 76 | shell: bash 77 | run: echo 'NAME=cnb-${{ steps.gen-version.outputs.VERSION }}-${{ matrix.targets.alias }}' >> $GITHUB_OUTPUT 78 | 79 | - name: Generate binary extension 80 | id: gen-ext 81 | if: runner.os == 'Windows' 82 | shell: bash 83 | run: echo 'EXT=.exe' >> $GITHUB_OUTPUT 84 | 85 | - name: Upload artifacts 86 | uses: actions/upload-artifact@v3 87 | with: 88 | name: ${{ steps.gen-name.outputs.NAME }} 89 | path: ./target/${{ matrix.targets.target }}/release/cnb${{ steps.gen-ext.outputs.EXT }} 90 | if-no-files-found: error 91 | 92 | - name: Archive binary 93 | uses: ./.github/actions/make-archive 94 | with: 95 | files: ./target/${{ matrix.targets.target }}/release/cnb${{ steps.gen-ext.outputs.EXT }} 96 | out: ${{ steps.gen-name.outputs.NAME }}.zip 97 | 98 | - name: Create GitHub release 99 | uses: softprops/action-gh-release@v1 100 | with: 101 | files: ${{ steps.gen-name.outputs.NAME }}.zip 102 | 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.0.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anstream" 46 | version = "0.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 49 | dependencies = [ 50 | "anstyle", 51 | "anstyle-parse", 52 | "anstyle-query", 53 | "anstyle-wincon", 54 | "colorchoice", 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle" 60 | version = "1.0.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 63 | 64 | [[package]] 65 | name = "anstyle-parse" 66 | version = "0.2.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 69 | dependencies = [ 70 | "utf8parse", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-query" 75 | version = "1.0.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 78 | dependencies = [ 79 | "windows-sys", 80 | ] 81 | 82 | [[package]] 83 | name = "anstyle-wincon" 84 | version = "2.1.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 87 | dependencies = [ 88 | "anstyle", 89 | "windows-sys", 90 | ] 91 | 92 | [[package]] 93 | name = "anyhow" 94 | version = "1.0.75" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 97 | 98 | [[package]] 99 | name = "autocfg" 100 | version = "1.1.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 103 | 104 | [[package]] 105 | name = "backtrace" 106 | version = "0.3.68" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" 109 | dependencies = [ 110 | "addr2line", 111 | "cc", 112 | "cfg-if", 113 | "libc", 114 | "miniz_oxide", 115 | "object", 116 | "rustc-demangle", 117 | ] 118 | 119 | [[package]] 120 | name = "base64" 121 | version = "0.21.4" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" 124 | 125 | [[package]] 126 | name = "base64url" 127 | version = "0.1.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "33de68096bac8e252e45589f42afd364c1dd28fbb3466ed726a941d5b9727d2c" 130 | dependencies = [ 131 | "base64", 132 | ] 133 | 134 | [[package]] 135 | name = "bitflags" 136 | version = "1.3.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 139 | 140 | [[package]] 141 | name = "bitflags" 142 | version = "2.4.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 145 | 146 | [[package]] 147 | name = "bumpalo" 148 | version = "3.13.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" 151 | 152 | [[package]] 153 | name = "bytes" 154 | version = "1.4.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 157 | 158 | [[package]] 159 | name = "cc" 160 | version = "1.0.82" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" 163 | dependencies = [ 164 | "libc", 165 | ] 166 | 167 | [[package]] 168 | name = "cfg-if" 169 | version = "1.0.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 172 | 173 | [[package]] 174 | name = "chrono" 175 | version = "0.4.30" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" 178 | dependencies = [ 179 | "android-tzdata", 180 | "iana-time-zone", 181 | "js-sys", 182 | "num-traits", 183 | "serde", 184 | "wasm-bindgen", 185 | "windows-targets", 186 | ] 187 | 188 | [[package]] 189 | name = "clap" 190 | version = "4.4.3" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" 193 | dependencies = [ 194 | "clap_builder", 195 | "clap_derive", 196 | ] 197 | 198 | [[package]] 199 | name = "clap_builder" 200 | version = "4.4.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" 203 | dependencies = [ 204 | "anstream", 205 | "anstyle", 206 | "clap_lex", 207 | "strsim", 208 | "terminal_size", 209 | ] 210 | 211 | [[package]] 212 | name = "clap_derive" 213 | version = "4.4.2" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 216 | dependencies = [ 217 | "heck", 218 | "proc-macro2", 219 | "quote", 220 | "syn", 221 | ] 222 | 223 | [[package]] 224 | name = "clap_lex" 225 | version = "0.5.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 228 | 229 | [[package]] 230 | name = "cnb" 231 | version = "0.0.0-dev" 232 | dependencies = [ 233 | "anyhow", 234 | "base64", 235 | "base64url", 236 | "chrono", 237 | "clap", 238 | "colored", 239 | "futures", 240 | "getrandom", 241 | "home", 242 | "lazy_static", 243 | "mime", 244 | "rand", 245 | "regex", 246 | "reqwest", 247 | "serde", 248 | "serde_json", 249 | "serde_qs", 250 | "serde_repr", 251 | "serde_with", 252 | "terminal_size", 253 | "tokio", 254 | "unicode-width", 255 | "words-count", 256 | ] 257 | 258 | [[package]] 259 | name = "colorchoice" 260 | version = "1.0.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 263 | 264 | [[package]] 265 | name = "colored" 266 | version = "2.0.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 269 | dependencies = [ 270 | "is-terminal", 271 | "lazy_static", 272 | "windows-sys", 273 | ] 274 | 275 | [[package]] 276 | name = "core-foundation-sys" 277 | version = "0.8.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 280 | 281 | [[package]] 282 | name = "darling" 283 | version = "0.20.3" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" 286 | dependencies = [ 287 | "darling_core", 288 | "darling_macro", 289 | ] 290 | 291 | [[package]] 292 | name = "darling_core" 293 | version = "0.20.3" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" 296 | dependencies = [ 297 | "fnv", 298 | "ident_case", 299 | "proc-macro2", 300 | "quote", 301 | "strsim", 302 | "syn", 303 | ] 304 | 305 | [[package]] 306 | name = "darling_macro" 307 | version = "0.20.3" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" 310 | dependencies = [ 311 | "darling_core", 312 | "quote", 313 | "syn", 314 | ] 315 | 316 | [[package]] 317 | name = "deranged" 318 | version = "0.3.7" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" 321 | dependencies = [ 322 | "serde", 323 | ] 324 | 325 | [[package]] 326 | name = "encoding_rs" 327 | version = "0.8.32" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 330 | dependencies = [ 331 | "cfg-if", 332 | ] 333 | 334 | [[package]] 335 | name = "equivalent" 336 | version = "1.0.1" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 339 | 340 | [[package]] 341 | name = "errno" 342 | version = "0.3.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" 345 | dependencies = [ 346 | "errno-dragonfly", 347 | "libc", 348 | "windows-sys", 349 | ] 350 | 351 | [[package]] 352 | name = "errno-dragonfly" 353 | version = "0.1.2" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 356 | dependencies = [ 357 | "cc", 358 | "libc", 359 | ] 360 | 361 | [[package]] 362 | name = "fnv" 363 | version = "1.0.7" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 366 | 367 | [[package]] 368 | name = "form_urlencoded" 369 | version = "1.2.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 372 | dependencies = [ 373 | "percent-encoding", 374 | ] 375 | 376 | [[package]] 377 | name = "futures" 378 | version = "0.3.28" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 381 | dependencies = [ 382 | "futures-channel", 383 | "futures-core", 384 | "futures-executor", 385 | "futures-io", 386 | "futures-sink", 387 | "futures-task", 388 | "futures-util", 389 | ] 390 | 391 | [[package]] 392 | name = "futures-channel" 393 | version = "0.3.28" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 396 | dependencies = [ 397 | "futures-core", 398 | "futures-sink", 399 | ] 400 | 401 | [[package]] 402 | name = "futures-core" 403 | version = "0.3.28" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 406 | 407 | [[package]] 408 | name = "futures-executor" 409 | version = "0.3.28" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 412 | dependencies = [ 413 | "futures-core", 414 | "futures-task", 415 | "futures-util", 416 | ] 417 | 418 | [[package]] 419 | name = "futures-io" 420 | version = "0.3.28" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 423 | 424 | [[package]] 425 | name = "futures-macro" 426 | version = "0.3.28" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn", 433 | ] 434 | 435 | [[package]] 436 | name = "futures-sink" 437 | version = "0.3.28" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 440 | 441 | [[package]] 442 | name = "futures-task" 443 | version = "0.3.28" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 446 | 447 | [[package]] 448 | name = "futures-util" 449 | version = "0.3.28" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 452 | dependencies = [ 453 | "futures-channel", 454 | "futures-core", 455 | "futures-io", 456 | "futures-macro", 457 | "futures-sink", 458 | "futures-task", 459 | "memchr", 460 | "pin-project-lite", 461 | "pin-utils", 462 | "slab", 463 | ] 464 | 465 | [[package]] 466 | name = "getrandom" 467 | version = "0.2.10" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 470 | dependencies = [ 471 | "cfg-if", 472 | "js-sys", 473 | "libc", 474 | "wasi", 475 | "wasm-bindgen", 476 | ] 477 | 478 | [[package]] 479 | name = "gimli" 480 | version = "0.27.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 483 | 484 | [[package]] 485 | name = "h2" 486 | version = "0.3.20" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" 489 | dependencies = [ 490 | "bytes", 491 | "fnv", 492 | "futures-core", 493 | "futures-sink", 494 | "futures-util", 495 | "http", 496 | "indexmap 1.9.3", 497 | "slab", 498 | "tokio", 499 | "tokio-util", 500 | "tracing", 501 | ] 502 | 503 | [[package]] 504 | name = "hashbrown" 505 | version = "0.12.3" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 508 | 509 | [[package]] 510 | name = "hashbrown" 511 | version = "0.14.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 514 | 515 | [[package]] 516 | name = "heck" 517 | version = "0.4.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 520 | 521 | [[package]] 522 | name = "hermit-abi" 523 | version = "0.3.2" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 526 | 527 | [[package]] 528 | name = "hex" 529 | version = "0.4.3" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 532 | 533 | [[package]] 534 | name = "home" 535 | version = "0.5.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 538 | dependencies = [ 539 | "windows-sys", 540 | ] 541 | 542 | [[package]] 543 | name = "http" 544 | version = "0.2.9" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 547 | dependencies = [ 548 | "bytes", 549 | "fnv", 550 | "itoa", 551 | ] 552 | 553 | [[package]] 554 | name = "http-body" 555 | version = "0.4.5" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 558 | dependencies = [ 559 | "bytes", 560 | "http", 561 | "pin-project-lite", 562 | ] 563 | 564 | [[package]] 565 | name = "httparse" 566 | version = "1.8.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 569 | 570 | [[package]] 571 | name = "httpdate" 572 | version = "1.0.3" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 575 | 576 | [[package]] 577 | name = "hyper" 578 | version = "0.14.27" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" 581 | dependencies = [ 582 | "bytes", 583 | "futures-channel", 584 | "futures-core", 585 | "futures-util", 586 | "h2", 587 | "http", 588 | "http-body", 589 | "httparse", 590 | "httpdate", 591 | "itoa", 592 | "pin-project-lite", 593 | "socket2 0.4.9", 594 | "tokio", 595 | "tower-service", 596 | "tracing", 597 | "want", 598 | ] 599 | 600 | [[package]] 601 | name = "hyper-rustls" 602 | version = "0.24.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" 605 | dependencies = [ 606 | "futures-util", 607 | "http", 608 | "hyper", 609 | "rustls", 610 | "tokio", 611 | "tokio-rustls", 612 | ] 613 | 614 | [[package]] 615 | name = "iana-time-zone" 616 | version = "0.1.57" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" 619 | dependencies = [ 620 | "android_system_properties", 621 | "core-foundation-sys", 622 | "iana-time-zone-haiku", 623 | "js-sys", 624 | "wasm-bindgen", 625 | "windows", 626 | ] 627 | 628 | [[package]] 629 | name = "iana-time-zone-haiku" 630 | version = "0.1.2" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 633 | dependencies = [ 634 | "cc", 635 | ] 636 | 637 | [[package]] 638 | name = "ident_case" 639 | version = "1.0.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 642 | 643 | [[package]] 644 | name = "idna" 645 | version = "0.4.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 648 | dependencies = [ 649 | "unicode-bidi", 650 | "unicode-normalization", 651 | ] 652 | 653 | [[package]] 654 | name = "indexmap" 655 | version = "1.9.3" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 658 | dependencies = [ 659 | "autocfg", 660 | "hashbrown 0.12.3", 661 | "serde", 662 | ] 663 | 664 | [[package]] 665 | name = "indexmap" 666 | version = "2.0.0" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 669 | dependencies = [ 670 | "equivalent", 671 | "hashbrown 0.14.0", 672 | "serde", 673 | ] 674 | 675 | [[package]] 676 | name = "io-lifetimes" 677 | version = "1.0.11" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 680 | dependencies = [ 681 | "hermit-abi", 682 | "libc", 683 | "windows-sys", 684 | ] 685 | 686 | [[package]] 687 | name = "ipnet" 688 | version = "2.8.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" 691 | 692 | [[package]] 693 | name = "is-terminal" 694 | version = "0.4.9" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 697 | dependencies = [ 698 | "hermit-abi", 699 | "rustix 0.38.8", 700 | "windows-sys", 701 | ] 702 | 703 | [[package]] 704 | name = "itoa" 705 | version = "1.0.9" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 708 | 709 | [[package]] 710 | name = "js-sys" 711 | version = "0.3.64" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 714 | dependencies = [ 715 | "wasm-bindgen", 716 | ] 717 | 718 | [[package]] 719 | name = "lazy_static" 720 | version = "1.4.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 723 | 724 | [[package]] 725 | name = "libc" 726 | version = "0.2.147" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 729 | 730 | [[package]] 731 | name = "linux-raw-sys" 732 | version = "0.3.8" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 735 | 736 | [[package]] 737 | name = "linux-raw-sys" 738 | version = "0.4.5" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" 741 | 742 | [[package]] 743 | name = "lock_api" 744 | version = "0.4.10" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 747 | dependencies = [ 748 | "autocfg", 749 | "scopeguard", 750 | ] 751 | 752 | [[package]] 753 | name = "log" 754 | version = "0.4.20" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 757 | 758 | [[package]] 759 | name = "memchr" 760 | version = "2.6.3" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 763 | 764 | [[package]] 765 | name = "mime" 766 | version = "0.3.17" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 769 | 770 | [[package]] 771 | name = "miniz_oxide" 772 | version = "0.7.1" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 775 | dependencies = [ 776 | "adler", 777 | ] 778 | 779 | [[package]] 780 | name = "mio" 781 | version = "0.8.8" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 784 | dependencies = [ 785 | "libc", 786 | "wasi", 787 | "windows-sys", 788 | ] 789 | 790 | [[package]] 791 | name = "num-traits" 792 | version = "0.2.16" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 795 | dependencies = [ 796 | "autocfg", 797 | ] 798 | 799 | [[package]] 800 | name = "num_cpus" 801 | version = "1.16.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 804 | dependencies = [ 805 | "hermit-abi", 806 | "libc", 807 | ] 808 | 809 | [[package]] 810 | name = "object" 811 | version = "0.31.1" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" 814 | dependencies = [ 815 | "memchr", 816 | ] 817 | 818 | [[package]] 819 | name = "once_cell" 820 | version = "1.18.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 823 | 824 | [[package]] 825 | name = "parking_lot" 826 | version = "0.12.1" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 829 | dependencies = [ 830 | "lock_api", 831 | "parking_lot_core", 832 | ] 833 | 834 | [[package]] 835 | name = "parking_lot_core" 836 | version = "0.9.8" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 839 | dependencies = [ 840 | "cfg-if", 841 | "libc", 842 | "redox_syscall", 843 | "smallvec", 844 | "windows-targets", 845 | ] 846 | 847 | [[package]] 848 | name = "percent-encoding" 849 | version = "2.3.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 852 | 853 | [[package]] 854 | name = "pin-project-lite" 855 | version = "0.2.12" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" 858 | 859 | [[package]] 860 | name = "pin-utils" 861 | version = "0.1.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 864 | 865 | [[package]] 866 | name = "ppv-lite86" 867 | version = "0.2.17" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 870 | 871 | [[package]] 872 | name = "proc-macro2" 873 | version = "1.0.66" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 876 | dependencies = [ 877 | "unicode-ident", 878 | ] 879 | 880 | [[package]] 881 | name = "quote" 882 | version = "1.0.32" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 885 | dependencies = [ 886 | "proc-macro2", 887 | ] 888 | 889 | [[package]] 890 | name = "rand" 891 | version = "0.8.5" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 894 | dependencies = [ 895 | "libc", 896 | "rand_chacha", 897 | "rand_core", 898 | ] 899 | 900 | [[package]] 901 | name = "rand_chacha" 902 | version = "0.3.1" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 905 | dependencies = [ 906 | "ppv-lite86", 907 | "rand_core", 908 | ] 909 | 910 | [[package]] 911 | name = "rand_core" 912 | version = "0.6.4" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 915 | dependencies = [ 916 | "getrandom", 917 | ] 918 | 919 | [[package]] 920 | name = "redox_syscall" 921 | version = "0.3.5" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 924 | dependencies = [ 925 | "bitflags 1.3.2", 926 | ] 927 | 928 | [[package]] 929 | name = "regex" 930 | version = "1.9.5" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 933 | dependencies = [ 934 | "aho-corasick", 935 | "memchr", 936 | "regex-automata", 937 | "regex-syntax", 938 | ] 939 | 940 | [[package]] 941 | name = "regex-automata" 942 | version = "0.3.8" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 945 | dependencies = [ 946 | "aho-corasick", 947 | "memchr", 948 | "regex-syntax", 949 | ] 950 | 951 | [[package]] 952 | name = "regex-syntax" 953 | version = "0.7.5" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 956 | 957 | [[package]] 958 | name = "reqwest" 959 | version = "0.11.20" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" 962 | dependencies = [ 963 | "base64", 964 | "bytes", 965 | "encoding_rs", 966 | "futures-core", 967 | "futures-util", 968 | "h2", 969 | "http", 970 | "http-body", 971 | "hyper", 972 | "hyper-rustls", 973 | "ipnet", 974 | "js-sys", 975 | "log", 976 | "mime", 977 | "once_cell", 978 | "percent-encoding", 979 | "pin-project-lite", 980 | "rustls", 981 | "rustls-pemfile", 982 | "serde", 983 | "serde_json", 984 | "serde_urlencoded", 985 | "tokio", 986 | "tokio-rustls", 987 | "tower-service", 988 | "url", 989 | "wasm-bindgen", 990 | "wasm-bindgen-futures", 991 | "web-sys", 992 | "webpki-roots", 993 | "winreg", 994 | ] 995 | 996 | [[package]] 997 | name = "ring" 998 | version = "0.16.20" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1001 | dependencies = [ 1002 | "cc", 1003 | "libc", 1004 | "once_cell", 1005 | "spin", 1006 | "untrusted", 1007 | "web-sys", 1008 | "winapi", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "rustc-demangle" 1013 | version = "0.1.23" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1016 | 1017 | [[package]] 1018 | name = "rustix" 1019 | version = "0.37.23" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" 1022 | dependencies = [ 1023 | "bitflags 1.3.2", 1024 | "errno", 1025 | "io-lifetimes", 1026 | "libc", 1027 | "linux-raw-sys 0.3.8", 1028 | "windows-sys", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "rustix" 1033 | version = "0.38.8" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" 1036 | dependencies = [ 1037 | "bitflags 2.4.0", 1038 | "errno", 1039 | "libc", 1040 | "linux-raw-sys 0.4.5", 1041 | "windows-sys", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "rustls" 1046 | version = "0.21.7" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" 1049 | dependencies = [ 1050 | "log", 1051 | "ring", 1052 | "rustls-webpki", 1053 | "sct", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "rustls-pemfile" 1058 | version = "1.0.3" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" 1061 | dependencies = [ 1062 | "base64", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "rustls-webpki" 1067 | version = "0.101.4" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" 1070 | dependencies = [ 1071 | "ring", 1072 | "untrusted", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "ryu" 1077 | version = "1.0.15" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 1080 | 1081 | [[package]] 1082 | name = "scopeguard" 1083 | version = "1.2.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1086 | 1087 | [[package]] 1088 | name = "sct" 1089 | version = "0.7.0" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1092 | dependencies = [ 1093 | "ring", 1094 | "untrusted", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "serde" 1099 | version = "1.0.188" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 1102 | dependencies = [ 1103 | "serde_derive", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "serde_derive" 1108 | version = "1.0.188" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 1111 | dependencies = [ 1112 | "proc-macro2", 1113 | "quote", 1114 | "syn", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "serde_json" 1119 | version = "1.0.107" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 1122 | dependencies = [ 1123 | "itoa", 1124 | "ryu", 1125 | "serde", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "serde_qs" 1130 | version = "0.12.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" 1133 | dependencies = [ 1134 | "percent-encoding", 1135 | "serde", 1136 | "thiserror", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "serde_repr" 1141 | version = "0.1.16" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" 1144 | dependencies = [ 1145 | "proc-macro2", 1146 | "quote", 1147 | "syn", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "serde_urlencoded" 1152 | version = "0.7.1" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1155 | dependencies = [ 1156 | "form_urlencoded", 1157 | "itoa", 1158 | "ryu", 1159 | "serde", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "serde_with" 1164 | version = "3.3.0" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" 1167 | dependencies = [ 1168 | "base64", 1169 | "chrono", 1170 | "hex", 1171 | "indexmap 1.9.3", 1172 | "indexmap 2.0.0", 1173 | "serde", 1174 | "serde_json", 1175 | "serde_with_macros", 1176 | "time", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "serde_with_macros" 1181 | version = "3.3.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" 1184 | dependencies = [ 1185 | "darling", 1186 | "proc-macro2", 1187 | "quote", 1188 | "syn", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "signal-hook-registry" 1193 | version = "1.4.1" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1196 | dependencies = [ 1197 | "libc", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "slab" 1202 | version = "0.4.8" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1205 | dependencies = [ 1206 | "autocfg", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "smallvec" 1211 | version = "1.11.0" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" 1214 | 1215 | [[package]] 1216 | name = "socket2" 1217 | version = "0.4.9" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1220 | dependencies = [ 1221 | "libc", 1222 | "winapi", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "socket2" 1227 | version = "0.5.3" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" 1230 | dependencies = [ 1231 | "libc", 1232 | "windows-sys", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "spin" 1237 | version = "0.5.2" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1240 | 1241 | [[package]] 1242 | name = "strsim" 1243 | version = "0.10.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1246 | 1247 | [[package]] 1248 | name = "syn" 1249 | version = "2.0.28" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 1252 | dependencies = [ 1253 | "proc-macro2", 1254 | "quote", 1255 | "unicode-ident", 1256 | ] 1257 | 1258 | [[package]] 1259 | name = "terminal_size" 1260 | version = "0.2.6" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" 1263 | dependencies = [ 1264 | "rustix 0.37.23", 1265 | "windows-sys", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "thiserror" 1270 | version = "1.0.45" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "dedd246497092a89beedfe2c9f176d44c1b672ea6090edc20544ade01fbb7ea0" 1273 | dependencies = [ 1274 | "thiserror-impl", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "thiserror-impl" 1279 | version = "1.0.45" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "7d7b1fadccbbc7e19ea64708629f9d8dccd007c260d66485f20a6d41bc1cf4b3" 1282 | dependencies = [ 1283 | "proc-macro2", 1284 | "quote", 1285 | "syn", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "time" 1290 | version = "0.3.25" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" 1293 | dependencies = [ 1294 | "deranged", 1295 | "itoa", 1296 | "serde", 1297 | "time-core", 1298 | "time-macros", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "time-core" 1303 | version = "0.1.1" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" 1306 | 1307 | [[package]] 1308 | name = "time-macros" 1309 | version = "0.2.11" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" 1312 | dependencies = [ 1313 | "time-core", 1314 | ] 1315 | 1316 | [[package]] 1317 | name = "tinyvec" 1318 | version = "1.6.0" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1321 | dependencies = [ 1322 | "tinyvec_macros", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "tinyvec_macros" 1327 | version = "0.1.1" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1330 | 1331 | [[package]] 1332 | name = "tokio" 1333 | version = "1.32.0" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" 1336 | dependencies = [ 1337 | "backtrace", 1338 | "bytes", 1339 | "libc", 1340 | "mio", 1341 | "num_cpus", 1342 | "parking_lot", 1343 | "pin-project-lite", 1344 | "signal-hook-registry", 1345 | "socket2 0.5.3", 1346 | "tokio-macros", 1347 | "windows-sys", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "tokio-macros" 1352 | version = "2.1.0" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 1355 | dependencies = [ 1356 | "proc-macro2", 1357 | "quote", 1358 | "syn", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "tokio-rustls" 1363 | version = "0.24.1" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 1366 | dependencies = [ 1367 | "rustls", 1368 | "tokio", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "tokio-util" 1373 | version = "0.7.8" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" 1376 | dependencies = [ 1377 | "bytes", 1378 | "futures-core", 1379 | "futures-sink", 1380 | "pin-project-lite", 1381 | "tokio", 1382 | "tracing", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "tower-service" 1387 | version = "0.3.2" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1390 | 1391 | [[package]] 1392 | name = "tracing" 1393 | version = "0.1.37" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1396 | dependencies = [ 1397 | "cfg-if", 1398 | "pin-project-lite", 1399 | "tracing-core", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "tracing-core" 1404 | version = "0.1.31" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 1407 | dependencies = [ 1408 | "once_cell", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "try-lock" 1413 | version = "0.2.4" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1416 | 1417 | [[package]] 1418 | name = "unicode-bidi" 1419 | version = "0.3.13" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1422 | 1423 | [[package]] 1424 | name = "unicode-blocks" 1425 | version = "0.1.8" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "c84398c527c802fbf222e5145f220382d60f1878e0e6cb4d22a3080949a8ddcd" 1428 | 1429 | [[package]] 1430 | name = "unicode-ident" 1431 | version = "1.0.11" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 1434 | 1435 | [[package]] 1436 | name = "unicode-normalization" 1437 | version = "0.1.22" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1440 | dependencies = [ 1441 | "tinyvec", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "unicode-width" 1446 | version = "0.1.10" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1449 | 1450 | [[package]] 1451 | name = "untrusted" 1452 | version = "0.7.1" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1455 | 1456 | [[package]] 1457 | name = "url" 1458 | version = "2.4.0" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" 1461 | dependencies = [ 1462 | "form_urlencoded", 1463 | "idna", 1464 | "percent-encoding", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "utf8parse" 1469 | version = "0.2.1" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1472 | 1473 | [[package]] 1474 | name = "want" 1475 | version = "0.3.1" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1478 | dependencies = [ 1479 | "try-lock", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "wasi" 1484 | version = "0.11.0+wasi-snapshot-preview1" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1487 | 1488 | [[package]] 1489 | name = "wasm-bindgen" 1490 | version = "0.2.87" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 1493 | dependencies = [ 1494 | "cfg-if", 1495 | "wasm-bindgen-macro", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "wasm-bindgen-backend" 1500 | version = "0.2.87" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 1503 | dependencies = [ 1504 | "bumpalo", 1505 | "log", 1506 | "once_cell", 1507 | "proc-macro2", 1508 | "quote", 1509 | "syn", 1510 | "wasm-bindgen-shared", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "wasm-bindgen-futures" 1515 | version = "0.4.37" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" 1518 | dependencies = [ 1519 | "cfg-if", 1520 | "js-sys", 1521 | "wasm-bindgen", 1522 | "web-sys", 1523 | ] 1524 | 1525 | [[package]] 1526 | name = "wasm-bindgen-macro" 1527 | version = "0.2.87" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 1530 | dependencies = [ 1531 | "quote", 1532 | "wasm-bindgen-macro-support", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "wasm-bindgen-macro-support" 1537 | version = "0.2.87" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 1540 | dependencies = [ 1541 | "proc-macro2", 1542 | "quote", 1543 | "syn", 1544 | "wasm-bindgen-backend", 1545 | "wasm-bindgen-shared", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "wasm-bindgen-shared" 1550 | version = "0.2.87" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 1553 | 1554 | [[package]] 1555 | name = "web-sys" 1556 | version = "0.3.64" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" 1559 | dependencies = [ 1560 | "js-sys", 1561 | "wasm-bindgen", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "webpki-roots" 1566 | version = "0.25.2" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" 1569 | 1570 | [[package]] 1571 | name = "winapi" 1572 | version = "0.3.9" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1575 | dependencies = [ 1576 | "winapi-i686-pc-windows-gnu", 1577 | "winapi-x86_64-pc-windows-gnu", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "winapi-i686-pc-windows-gnu" 1582 | version = "0.4.0" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1585 | 1586 | [[package]] 1587 | name = "winapi-x86_64-pc-windows-gnu" 1588 | version = "0.4.0" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1591 | 1592 | [[package]] 1593 | name = "windows" 1594 | version = "0.48.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 1597 | dependencies = [ 1598 | "windows-targets", 1599 | ] 1600 | 1601 | [[package]] 1602 | name = "windows-sys" 1603 | version = "0.48.0" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1606 | dependencies = [ 1607 | "windows-targets", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "windows-targets" 1612 | version = "0.48.2" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" 1615 | dependencies = [ 1616 | "windows_aarch64_gnullvm", 1617 | "windows_aarch64_msvc", 1618 | "windows_i686_gnu", 1619 | "windows_i686_msvc", 1620 | "windows_x86_64_gnu", 1621 | "windows_x86_64_gnullvm", 1622 | "windows_x86_64_msvc", 1623 | ] 1624 | 1625 | [[package]] 1626 | name = "windows_aarch64_gnullvm" 1627 | version = "0.48.2" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" 1630 | 1631 | [[package]] 1632 | name = "windows_aarch64_msvc" 1633 | version = "0.48.2" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" 1636 | 1637 | [[package]] 1638 | name = "windows_i686_gnu" 1639 | version = "0.48.2" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" 1642 | 1643 | [[package]] 1644 | name = "windows_i686_msvc" 1645 | version = "0.48.2" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" 1648 | 1649 | [[package]] 1650 | name = "windows_x86_64_gnu" 1651 | version = "0.48.2" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" 1654 | 1655 | [[package]] 1656 | name = "windows_x86_64_gnullvm" 1657 | version = "0.48.2" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" 1660 | 1661 | [[package]] 1662 | name = "windows_x86_64_msvc" 1663 | version = "0.48.2" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" 1666 | 1667 | [[package]] 1668 | name = "winreg" 1669 | version = "0.50.0" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1672 | dependencies = [ 1673 | "cfg-if", 1674 | "windows-sys", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "words-count" 1679 | version = "0.1.6" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "d28653ddaede5475c44a03e4014ae19f35aa9b231c423228b28963cb873e4869" 1682 | dependencies = [ 1683 | "unicode-blocks", 1684 | ] 1685 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cnb" 3 | # WRN: Version will be updated by CI while create a tag, NERVER change this. 4 | version = "0.0.0-dev" 5 | edition = "2021" 6 | description = "Cnblogs' command line tool" 7 | license = "MIT" 8 | repository = "https://github.com/cnblogs/cli" 9 | keywords = ["cli", "cnblogs", "blog"] 10 | categories = ["command-line-utilities"] 11 | 12 | [profile.dev] 13 | lto = true 14 | strip = true 15 | 16 | [profile.release] 17 | lto = true 18 | strip = true 19 | codegen-units = 1 20 | 21 | [dependencies] 22 | anyhow = "1.0.75" 23 | lazy_static = "1.4.0" 24 | base64 = "0.21.4" 25 | base64url = "0.1.0" 26 | getrandom = { version = "0.2.10", features = ["js"] } 27 | rand = { version = "0.8.5" } 28 | regex = "1.9.5" 29 | words-count = "0.1.6" 30 | unicode-width = "0.1.10" 31 | 32 | serde = { version = "1.0.188", features = ["derive"] } 33 | serde_qs = "0.12.0" 34 | serde_json = "1.0.107" 35 | serde_with = "3.3.0" 36 | serde_repr = "0.1.16" 37 | 38 | home = "0.5.5" 39 | chrono = "0.4.30" 40 | mime = "0.3.17" 41 | reqwest = { version = "0.11.20", default-features = false, features = ["json", "rustls-tls"] } 42 | tokio = { version = "1.32.0", features = ["full"] } 43 | futures = "0.3.28" 44 | 45 | clap = { version = "4.4.3", features = ["derive", "wrap_help"] } 46 | colored = "2.0.4" 47 | terminal_size = "0.2.6" 48 | 49 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cnblogs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cnblogs' command line tool 2 | 3 | [![Build / Release](https://github.com/cnblogs/cli/actions/workflows/build-release.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-release.yml) 4 | [![Build / Development](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml) 5 | 6 | Access cnblogs form CLI. 7 | 8 | ## Usage 9 | 10 | To use `cnb` directly, add it to your environment variables is required. 11 | 12 | ### Login 13 | 14 | You need to get your PAT from [https://account.cnblogs.com/settings/tokens](https://account.cnblogs.com/settings/tokens) to use this tool. 15 | 16 | Then run `cnb user --login 'YOUR_PAT_HERE'`. This will save your PAT to `~/.cnbrc`. 17 | 18 | If you want to logout, run `cnb user --logout` or just remove `~/.cnbrc`. 19 | 20 | ### Examples 21 | 22 | It's time to enjoy cnblogs. 23 | 24 | Here are some simple examples: 25 | 26 | ```shell 27 | # Check your post list 28 | cnb post --list 29 | # Check your post 30 | cnb --id 114514 post --show 31 | # Create and publish post 32 | cnb post create --title 'Hello' --body 'world!' --publish 33 | # Change your post body 34 | cnb --id 114514 post update --body 'niconiconiconi' 35 | 36 | # Show ing list 37 | cnb ing list 38 | # Publish ing 39 | cnb ing --publish 'Hello world!' 40 | # Comment to ing 41 | cnb --id 114514 ing --comment 'Awesome!' 42 | 43 | # Check your user infomation 44 | cnb user --info 45 | ``` 46 | 47 | For more information, try `cnb --help`. 48 | 49 | ## Installation 50 | 51 | ### From releases 52 | 53 | [Releases](https://github.com/cnblogs/cli/releases) 54 | 55 | ### Build locally 56 | 57 | This tool requires nightly toolchains(1.74.0+) to build. 58 | 59 | ```shell 60 | git clone --depth 1 https://github.com/cnblogs/cli.git 61 | cd cli 62 | cargo build -r 63 | ``` 64 | 65 | Or get binaries from [CI](https://github.com/cnblogs/cli/actions) artifacts. 66 | 67 | ## License 68 | 69 | [MIT](https://raw.githubusercontent.com/cnblogs/cli/main/LICENSE) 70 | 71 | ## Feedback 72 | 73 | We’d love to hear your thoughts on this project. Feel free to drop us a note! 74 | 75 | [Issues](https://github.com/cnblogs/cli/issues) 76 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "nightly-2023-12-27" 4 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | let 4 | packages = with pkgs; [ 5 | openssl_3 6 | pkg-config 7 | ]; 8 | in pkgs.mkShell { 9 | buildInputs = packages; 10 | 11 | shellHook = '' 12 | export LD_LIBRARY_PATH=${ 13 | pkgs.lib.makeLibraryPath packages 14 | }:$LD_LIBRARY_PATH 15 | ''; 16 | } 17 | -------------------------------------------------------------------------------- /src/api/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod session; 2 | -------------------------------------------------------------------------------- /src/api/auth/session.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::result::WrapResult; 2 | use anyhow::{anyhow, Result}; 3 | use home::home_dir; 4 | use std::fs; 5 | use std::fs::{metadata, remove_file, File}; 6 | use std::io::Write; 7 | use std::path::{Path, PathBuf}; 8 | 9 | fn remove_pat(path: &Path) -> Result<()> { 10 | if metadata(path).is_ok() { 11 | remove_file(path)?; 12 | } 13 | ().wrap_ok() 14 | } 15 | 16 | fn save_pat(pat: &str, path: &Path) -> Result<()> { 17 | let mut file = File::create(path)?; 18 | file.write_all(pat.as_bytes())?; 19 | ().wrap_ok() 20 | } 21 | 22 | fn get_cfg_path() -> Result { 23 | let home = home_dir().ok_or_else(|| anyhow!("Can not get home dir"))?; 24 | home.join(".cnbrc").wrap_ok() 25 | } 26 | 27 | pub fn login(pat: &str) -> Result { 28 | let cfg_path = get_cfg_path()?; 29 | let cfg_path = cfg_path.as_path(); 30 | 31 | remove_pat(cfg_path)?; 32 | save_pat(pat, cfg_path)?; 33 | 34 | cfg_path.to_owned().wrap_ok() 35 | } 36 | 37 | pub fn logout() -> Result { 38 | let cfg_path = get_cfg_path()?; 39 | let cfg_path = cfg_path.as_path(); 40 | 41 | remove_pat(cfg_path)?; 42 | 43 | cfg_path.to_owned().wrap_ok() 44 | } 45 | 46 | pub fn get_pat() -> Result { 47 | let cfg_path = get_cfg_path()?; 48 | let cfg_path = cfg_path.as_path(); 49 | 50 | fs::read_to_string(cfg_path) 51 | .map_err(|e| anyhow!("Can not read {:?}, please login first ({})", cfg_path, e)) 52 | } 53 | -------------------------------------------------------------------------------- /src/api/fav/get_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::fav::Fav; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::iter::IntoIteratorExt; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use crate::infra::vec::VecExt; 7 | use crate::openapi; 8 | use anyhow::Result; 9 | use serde::{Deserialize, Serialize}; 10 | use std::ops::ControlFlow; 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | #[serde(rename_all = "PascalCase")] 14 | pub struct FavEntry { 15 | pub title: String, 16 | #[serde(rename = "LinkUrl")] 17 | pub url: String, 18 | pub summary: String, 19 | pub tags: Vec, 20 | #[serde(rename = "DateAdded")] 21 | pub create_time: String, 22 | } 23 | 24 | impl Fav { 25 | pub async fn get_list(&self, skip: usize, take: usize) -> Result> { 26 | let client = &reqwest::Client::new(); 27 | 28 | let range = (skip + 1)..=(skip + take); 29 | let cf = range 30 | .map(|i| async move { 31 | let req = { 32 | let url = openapi!("/bookmarks"); 33 | let query = [("pageIndex", i), ("pageSize", 1)]; 34 | client.get(url).query(&query).pat_auth(&self.pat) 35 | }; 36 | 37 | let resp = req.send().await?; 38 | 39 | let body = body_or_err(resp).await?; 40 | 41 | json::deserialize::>(&body)? 42 | .pop() 43 | .wrap_ok::() 44 | }) 45 | .join_all() 46 | .await 47 | .into_iter() 48 | .try_fold(vec![], |acc, it| match it { 49 | Ok(maybe) => match maybe { 50 | Some(entry) => ControlFlow::Continue(acc.chain_push(entry)), 51 | None => ControlFlow::Break(Ok(acc)), 52 | }, 53 | Err(e) => ControlFlow::Break(Err(e)), 54 | }); 55 | 56 | match cf { 57 | ControlFlow::Continue(vec) => Ok(vec), 58 | ControlFlow::Break(result) => result, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/fav/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_list; 2 | 3 | // Aka cnblogs wz 4 | pub struct Fav { 5 | pat: String, 6 | } 7 | 8 | impl Fav { 9 | pub const fn new(pat: String) -> Self { 10 | Self { pat } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/api/ing/comment.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::Ing; 2 | use crate::infra::http::{unit_or_err, RequestBuilderExt}; 3 | use crate::openapi; 4 | use anyhow::Result; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | impl Ing { 8 | pub async fn comment( 9 | &self, 10 | ing_id: usize, 11 | content: String, 12 | reply_to: Option, 13 | parent_comment_id: Option, 14 | ) -> Result<()> { 15 | let client = reqwest::Client::new(); 16 | 17 | let req = { 18 | let url = openapi!("/statuses/{}/comments", ing_id); 19 | let body = { 20 | #[serde_with::skip_serializing_none] 21 | #[derive(Clone, Debug, Serialize, Deserialize)] 22 | struct Body { 23 | #[serde(rename(serialize = "replyTo"))] 24 | reply_to: Option, 25 | #[serde(rename(serialize = "parentCommentId"))] 26 | parent_comment_id: Option, 27 | content: String, 28 | } 29 | Body { 30 | reply_to, 31 | parent_comment_id, 32 | content, 33 | } 34 | }; 35 | client.post(url).json(&body).pat_auth(&self.pat) 36 | }; 37 | 38 | let resp = req.send().await?; 39 | 40 | unit_or_err(resp).await 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/api/ing/get_comment_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::Ing; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::json; 4 | use crate::infra::result::WrapResult; 5 | use crate::openapi; 6 | use anyhow::Result; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | #[serde(rename_all = "PascalCase")] 11 | pub struct IngCommentEntry { 12 | pub id: usize, 13 | pub content: String, 14 | #[serde(rename = "DateAdded")] 15 | pub create_time: String, 16 | pub status_id: usize, 17 | pub user_alias: String, 18 | #[serde(rename = "UserDisplayName")] 19 | pub user_name: String, 20 | pub user_icon_url: String, 21 | pub user_id: usize, 22 | pub user_guid: String, 23 | } 24 | 25 | impl Ing { 26 | pub async fn get_comment_list(&self, ing_id: usize) -> Result> { 27 | let client = reqwest::Client::new(); 28 | 29 | let req = { 30 | let url = openapi!("/statuses/{}/comments", ing_id); 31 | client.get(url).pat_auth(&self.pat) 32 | }; 33 | let resp = req.send().await?; 34 | 35 | let entry_vec = { 36 | let body = body_or_err(resp).await?; 37 | json::deserialize::>(&body)? 38 | }; 39 | 40 | entry_vec.wrap_ok() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/api/ing/get_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::{Ing, IngSendFrom, IngType}; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::iter::IntoIteratorExt; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use crate::infra::vec::VecExt; 7 | use crate::openapi; 8 | use anyhow::Result; 9 | use serde::{Deserialize, Serialize}; 10 | use std::ops::ControlFlow; 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | #[serde(rename_all = "PascalCase")] 14 | pub struct IngEntry { 15 | pub id: usize, 16 | pub content: String, 17 | pub is_private: bool, 18 | pub is_lucky: bool, 19 | pub comment_count: usize, 20 | #[serde(rename = "DateAdded")] 21 | pub create_time: String, 22 | pub user_alias: String, 23 | #[serde(rename = "UserDisplayName")] 24 | pub user_name: String, 25 | pub user_icon_url: String, 26 | pub user_id: usize, 27 | pub user_guid: String, 28 | pub send_from: IngSendFrom, 29 | #[serde(rename = "Icons")] 30 | pub icons: String, 31 | } 32 | 33 | impl Ing { 34 | pub async fn get_list( 35 | &self, 36 | skip: usize, 37 | take: usize, 38 | ing_type: &IngType, 39 | ) -> Result> { 40 | let client = &reqwest::Client::new(); 41 | 42 | let range = (skip + 1)..=(skip + take); 43 | let cf = range 44 | .map(|i| async move { 45 | let req = { 46 | let url = openapi!("/statuses/@{}", ing_type.clone() as usize); 47 | let query = [("pageIndex", i), ("pageSize", 1)]; 48 | client.get(url).query(&query).pat_auth(&self.pat) 49 | }; 50 | 51 | let resp = req.send().await?; 52 | 53 | let body = body_or_err(resp).await?; 54 | 55 | json::deserialize::>(&body)?.pop().wrap_ok() 56 | }) 57 | .join_all() 58 | .await 59 | .into_iter() 60 | .try_fold(vec![], |acc, it| match it { 61 | Ok(maybe) => match maybe { 62 | Some(entry) => ControlFlow::Continue(acc.chain_push(entry)), 63 | None => ControlFlow::Break(Ok(acc)), 64 | }, 65 | Err(e) => ControlFlow::Break(Err(e)), 66 | }); 67 | 68 | match cf { 69 | ControlFlow::Continue(vec) => Ok(vec), 70 | ControlFlow::Break(result) => result, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/api/ing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod comment; 2 | pub mod publish; 3 | 4 | use clap::{Parser, ValueEnum}; 5 | use lazy_static::lazy_static; 6 | use regex::Regex; 7 | use serde_repr::{Deserialize_repr, Serialize_repr}; 8 | 9 | pub mod get_comment_list; 10 | pub mod get_list; 11 | 12 | pub struct Ing { 13 | pat: String, 14 | } 15 | 16 | impl Ing { 17 | pub const fn new(pat: String) -> Self { 18 | Self { pat } 19 | } 20 | } 21 | 22 | #[derive(Clone, Debug, Parser, ValueEnum)] 23 | pub enum IngType { 24 | Follow = 1, 25 | Myself = 4, 26 | Public = 5, 27 | //RecentComment = 6, 28 | MyComment = 7, 29 | //Tag = 10, 30 | //Comment = 13, 31 | //Mention = 14, 32 | } 33 | 34 | #[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] 35 | #[repr(u8)] 36 | pub enum IngSendFrom { 37 | None = 0, 38 | Ms = 1, 39 | GTalk = 2, 40 | Qq = 3, 41 | Sms = 5, 42 | CellPhone = 6, 43 | Web = 8, 44 | VsCode = 9, 45 | Cli = 13, 46 | } 47 | 48 | pub fn ing_star_tag_to_text(tag: &str) -> String { 49 | lazy_static! { 50 | static ref REGEX: Regex = 51 | Regex::new(r#""#).expect("Invalid regexp"); 52 | } 53 | let caps = REGEX 54 | .captures(tag) 55 | .unwrap_or_else(|| panic!("No captures for: {}", tag)); 56 | let text = caps.get(1).expect("No capture at index 1").as_str(); 57 | text.to_string() 58 | } 59 | 60 | pub fn fmt_content(content: &str) -> String { 61 | lazy_static! { 62 | static ref REGEX: Regex = 63 | Regex::new(r#"(@.*?)"#) 64 | .expect("Invalid regexp"); 65 | } 66 | REGEX.captures(content).map_or_else( 67 | || content.to_owned(), 68 | |caps| { 69 | let at_user = caps.get(1).expect("No capture at index 1").as_str(); 70 | REGEX.replace(content, at_user).to_string() 71 | }, 72 | ) 73 | } 74 | 75 | pub fn rm_ing_at_user_tag(text: &str) -> String { 76 | lazy_static! { 77 | static ref REGEX: Regex = 78 | Regex::new(r#"(@.*?):"#) 79 | .expect("Invalid regexp"); 80 | } 81 | REGEX.replace(text, "").to_string() 82 | } 83 | 84 | pub fn get_ing_at_user_tag_text(text: &str) -> String { 85 | lazy_static! { 86 | static ref REGEX: Regex = 87 | Regex::new(r#"@(.*?):"#) 88 | .expect("Invalid regexp"); 89 | } 90 | REGEX.captures(text).map_or_else(String::new, |caps| { 91 | caps.get(1) 92 | .expect("No capture at index 1") 93 | .as_str() 94 | .to_string() 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /src/api/ing/publish.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::{Ing, IngSendFrom}; 2 | use crate::infra::http::{unit_or_err, RequestBuilderExt}; 3 | use crate::openapi; 4 | use anyhow::Result; 5 | use serde_json::json; 6 | 7 | impl Ing { 8 | pub async fn publish(&self, content: &str) -> Result<()> { 9 | let client = reqwest::Client::new(); 10 | 11 | let req = { 12 | let url = openapi!("/statuses"); 13 | let body = json!({ 14 | "content": content, 15 | "isPrivate": false, 16 | "clientType": IngSendFrom::Cli, 17 | }); 18 | 19 | client.post(url).json(&body).pat_auth(&self.pat) 20 | }; 21 | 22 | let resp = req.send().await?; 23 | 24 | unit_or_err(resp).await 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod fav; 3 | pub mod ing; 4 | pub mod news; 5 | pub mod post; 6 | pub mod user; 7 | 8 | pub const BLOG_BACKEND: &str = "https://i.cnblogs.com/api"; 9 | #[macro_export] 10 | macro_rules! blog_backend { 11 | ($($arg:tt)*) => {{ 12 | use $crate::api::BLOG_BACKEND; 13 | format!("{}{}", BLOG_BACKEND, format_args!($($arg)*)) 14 | }}; 15 | } 16 | 17 | pub const OPENAPI: &str = "https://api.cnblogs.com/api"; 18 | #[macro_export] 19 | macro_rules! openapi { 20 | ($($arg:tt)*) => {{ 21 | use $crate::api::OPENAPI; 22 | format!("{}{}", OPENAPI, format_args!($($arg)*)) 23 | }}; 24 | } 25 | 26 | pub const OAUTH: &str = "https://oauth.cnblogs.com"; 27 | #[macro_export] 28 | macro_rules! oauth { 29 | ($($arg:tt)*) => {{ 30 | use $crate::api::OAUTH; 31 | format!("{}{}", OAUTH, format_args!($($arg)*)) 32 | }}; 33 | } 34 | -------------------------------------------------------------------------------- /src/api/news/get_body.rs: -------------------------------------------------------------------------------- 1 | use crate::api::news::News; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use anyhow::Result; 5 | 6 | impl News { 7 | pub async fn get_body(&self, id: usize) -> Result { 8 | let client = reqwest::Client::new(); 9 | 10 | let req = { 11 | let url = blog_backend!("newsitems/{}/body", id); 12 | client.get(url).pat_auth(&self.pat) 13 | }; 14 | 15 | let resp = req.send().await?; 16 | 17 | body_or_err(resp).await 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/api/news/get_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::news::News; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::iter::IntoIteratorExt; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use crate::openapi; 7 | use anyhow::Result; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Clone, Debug, Serialize, Deserialize)] 11 | #[serde(rename_all = "PascalCase")] 12 | pub struct NewsEntry { 13 | pub id: usize, 14 | pub title: String, 15 | pub summary: String, 16 | pub topic_id: usize, 17 | #[serde(rename = "TopicIcon")] 18 | pub topic_icon_url: Option, 19 | pub view_count: usize, 20 | pub comment_count: usize, 21 | pub digg_count: usize, 22 | #[serde(rename = "DateAdded")] 23 | pub create_time: String, 24 | } 25 | 26 | impl News { 27 | pub async fn get_list(&self, skip: usize, take: usize) -> Result> { 28 | let client = &reqwest::Client::new(); 29 | 30 | let range = (skip + 1)..=(skip + take); 31 | range 32 | .map(|i| async move { 33 | let req = { 34 | let url = openapi!("/newsitems"); 35 | let query = [("pageIndex", i), ("pageSize", 1)]; 36 | client.get(url).query(&query).pat_auth(&self.pat) 37 | }; 38 | 39 | let resp = req.send().await?; 40 | 41 | let entry = { 42 | let body = body_or_err(resp).await?; 43 | let [entry, ..] = json::deserialize::<[NewsEntry; 1]>(&body)?; 44 | entry 45 | }; 46 | 47 | entry.wrap_ok::() 48 | }) 49 | .join_all() 50 | .await 51 | .into_iter() 52 | .collect() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/api/news/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_body; 2 | pub mod get_list; 3 | 4 | pub struct News { 5 | pat: String, 6 | } 7 | 8 | impl News { 9 | pub const fn new(pat: String) -> Self { 10 | Self { pat } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/api/post/create.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use anyhow::Result; 7 | use serde_json::{json, Value}; 8 | 9 | impl Post { 10 | pub async fn create(&self, title: &str, body: &str, publish: bool) -> Result { 11 | let client = reqwest::Client::new(); 12 | 13 | let req = { 14 | let url = blog_backend!("/posts"); 15 | let body = json!({ 16 | "postType": 1, 17 | "title": title, 18 | "postBody": body, 19 | "isPublished": publish, 20 | "displayOnHomePage": true 21 | }); 22 | client.post(url).json(&body).pat_auth(&self.pat) 23 | }; 24 | 25 | let resp = req.send().await?; 26 | 27 | let id = { 28 | let body = body_or_err(resp).await?; 29 | let json = json::deserialize::(&body)?; 30 | json["id"].as_u64().expect("as_u64 failed for `id`") as usize 31 | }; 32 | 33 | id.wrap_ok() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/api/post/del_one.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{unit_or_err, RequestBuilderExt}; 4 | use anyhow::Result; 5 | 6 | impl Post { 7 | pub async fn del_one(&self, id: usize) -> Result<()> { 8 | let client = reqwest::Client::new(); 9 | 10 | let req = { 11 | let url = blog_backend!("/posts/{}", id); 12 | client.delete(url).pat_auth(&self.pat) 13 | }; 14 | let resp = req.send().await?; 15 | 16 | unit_or_err(resp).await 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/api/post/get_comment_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::api::user::User; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use crate::openapi; 7 | use anyhow::Result; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Clone, Debug, Serialize, Deserialize)] 11 | #[serde(rename_all = "PascalCase")] 12 | pub struct PostCommentEntry { 13 | pub id: usize, 14 | #[serde(rename = "Body")] 15 | pub content: String, 16 | #[serde(rename = "Author")] 17 | pub user_name: String, 18 | #[serde(rename = "AuthorUrl")] 19 | pub user_home_url: String, 20 | #[serde(rename = "FaceUrl")] 21 | pub avatar_url: String, 22 | pub floor: usize, 23 | #[serde(rename = "DateAdded")] 24 | pub create_time: String, 25 | } 26 | 27 | impl Post { 28 | pub async fn get_comment_list(&self, post_id: usize) -> Result> { 29 | let blog_app = User::new(self.pat.to_owned()).get_info().await?.blog_app; 30 | let client = reqwest::Client::new(); 31 | 32 | let req = { 33 | let url = openapi!("/blogs/{}/posts/{}/comments", blog_app, post_id); 34 | client.get(url).pat_auth(&self.pat) 35 | }; 36 | let resp = req.send().await?; 37 | 38 | let entry_vec = { 39 | let body = body_or_err(resp).await?; 40 | json::deserialize::>(&body)? 41 | }; 42 | 43 | entry_vec.wrap_ok() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/api/post/get_count.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use anyhow::Result; 7 | use serde_json::Value; 8 | 9 | impl Post { 10 | pub async fn get_count(&self) -> Result { 11 | let client = reqwest::Client::new(); 12 | 13 | let req = { 14 | let url = blog_backend!("/posts/list"); 15 | let query = [('t', 1), ('p', 1), ('s', 1)]; 16 | client.get(url).query(&query).pat_auth(&self.pat) 17 | }; 18 | 19 | let resp = req.send().await?; 20 | 21 | let count = { 22 | let body = body_or_err(resp).await?; 23 | let json = json::deserialize::(&body)?; 24 | json["postsCount"] 25 | .as_u64() 26 | .expect("as_u64 failed for `postsCount`") as usize 27 | }; 28 | 29 | count.wrap_ok() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/api/post/get_meta_list.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::get_one::PostEntry; 2 | use crate::api::post::Post; 3 | use crate::blog_backend; 4 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 5 | use crate::infra::iter::IntoIteratorExt; 6 | use crate::infra::json; 7 | use crate::infra::result::WrapResult; 8 | use anyhow::Result; 9 | use serde_json::Value; 10 | 11 | /* 12 | Fields only available over blog_backend!("/posts/list?{}", query): 13 | aggCount: number 14 | feedBackCount: number 15 | isInSiteCandidate: boolean 16 | isInSiteHome: boolean 17 | postConfig: number 18 | viewCount: number 19 | webCount: number 20 | */ 21 | 22 | impl Post { 23 | pub async fn get_meta_list(&self, skip: usize, take: usize) -> Result<(Vec, usize)> { 24 | // WRN: 25 | // This impl has low performance but robust 26 | // Current API of blog backend is buggy 27 | // It's not worth to design a more efficient impl 28 | let client = &reqwest::Client::new(); 29 | 30 | // total_count is used for patch the buggy blog backend API 31 | // If index is greater than the max page index, API will still return the last page 32 | let total_count = self.get_count().await?; 33 | 34 | let range = (skip + 1)..=(skip + take).min(total_count); 35 | let vec = range 36 | .map(|i| async move { 37 | let req = { 38 | let url = blog_backend!("/posts/list"); 39 | let query = [('t', 1), ('p', i), ('s', 1)]; 40 | client.get(url).query(&query).pat_auth(&self.pat) 41 | }; 42 | 43 | let resp = req.send().await?; 44 | 45 | let entry = { 46 | let body = body_or_err(resp).await?; 47 | let json = json::deserialize::(&body)?["postList"].take(); 48 | 49 | let [entry, ..] = serde_json::from_value::<[PostEntry; 1]>(json)?; 50 | entry 51 | }; 52 | 53 | entry.wrap_ok() 54 | }) 55 | .join_all() 56 | .await 57 | .into_iter() 58 | .collect::>>(); 59 | 60 | (vec?, total_count).wrap_ok() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/api/post/get_one.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use anyhow::Result; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::Value; 9 | 10 | // TODO: not elegant 11 | #[derive(Serialize, Deserialize, Debug)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct PostEntry { 14 | pub id: usize, 15 | pub title: String, 16 | pub url: String, 17 | 18 | #[serde(rename = "datePublished")] 19 | pub create_time: String, 20 | #[serde(rename = "dateUpdated")] 21 | pub modify_time: String, 22 | 23 | pub is_draft: bool, 24 | pub is_pinned: bool, 25 | pub is_published: bool, 26 | 27 | // WRN: 28 | // Limited by the design of blog backend API 29 | // None implies that this filed is not fetched from server yet but DOSE NOT MEAN IT NOT EXIST 30 | #[serde(rename = "feedBackCount")] 31 | pub comment_count: Option, 32 | #[serde(rename = "postBody")] 33 | pub body: Option, 34 | pub tags: Option>, 35 | } 36 | 37 | impl Post { 38 | pub async fn get_one(&self, id: usize) -> Result { 39 | let client = reqwest::Client::new(); 40 | 41 | let req = { 42 | let url = blog_backend!("/posts/{}", id); 43 | client.get(url).pat_auth(&self.pat) 44 | }; 45 | 46 | let resp = req.send().await?; 47 | 48 | let entry = { 49 | let body = body_or_err(resp).await?; 50 | let json = json::deserialize::(&body)?["blogPost"].take(); 51 | serde_json::from_value::(json)? 52 | }; 53 | 54 | entry.wrap_ok() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api/post/get_one_raw.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::result::WrapResult; 5 | use anyhow::Result; 6 | use serde_json::Value; 7 | 8 | /* 9 | Fields only available over blog_backend!("/posts/{}", id): 10 | postBody: string 11 | categoryIds: [] 12 | collectionIds: [] 13 | inSiteCandidate: boolean 14 | inSiteHome: boolean 15 | siteCategoryId: null 16 | blogTeamIds: [] 17 | displayOnHomePage: boolean 18 | isAllowComments: boolean 19 | includeInMainSyndication: boolean 20 | isOnlyForRegisterUser: boolean 21 | isUpdateDateAdded: boolean 22 | description: string 23 | featuredImage: null 24 | tags: [] 25 | password: null 26 | autoDesc: string 27 | changePostType: boolean 28 | blogId: number 29 | author: string 30 | removeScript: boolean 31 | clientInfo: null 32 | changeCreatedTime: boolean 33 | canChangeCreatedTime: boolean 34 | isContributeToImpressiveBugActivity: boolean 35 | usingEditorId: null 36 | sourceUrl: null 37 | 38 | Fields available over blog_backend!("/posts/{}", id) and blog_backend!("/posts/list?{}", query): 39 | id: number 40 | postType: PostType 41 | accessPermission: AccessPermission 42 | title: string 43 | url: string 44 | entryName: null 45 | datePublished: string 46 | dateUpdated: string 47 | isMarkdown: boolean 48 | isDraft: boolean 49 | isPinned: boolean 50 | isPublished: boolean 51 | */ 52 | 53 | impl Post { 54 | /** 55 | Get raw json from remote 56 | 57 | Use this while it's hard to deserialize to struct 58 | **/ 59 | pub async fn get_one_raw(&self, id: usize) -> Result { 60 | let client = reqwest::Client::new(); 61 | 62 | let req = { 63 | let url = blog_backend!("/posts/{}", id); 64 | client.get(url).pat_auth(&self.pat) 65 | }; 66 | 67 | let resp = req.send().await?; 68 | 69 | let mut json = { 70 | let body = body_or_err(resp).await?; 71 | serde_json::from_str::(&body) 72 | }?; 73 | 74 | json["blogPost"].take().wrap_ok() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/api/post/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod del_one; 3 | pub mod get_comment_list; 4 | pub mod get_count; 5 | pub mod get_meta_list; 6 | pub mod get_one; 7 | pub mod get_one_raw; 8 | pub mod search_self; 9 | pub mod search_site; 10 | pub mod update; 11 | 12 | pub struct Post { 13 | pat: String, 14 | } 15 | 16 | impl Post { 17 | pub const fn new(pat: String) -> Self { 18 | Self { pat } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/post/search.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::iter::IntoIteratorExt; 5 | use crate::infra::json; 6 | use crate::infra::result::WrapResult; 7 | use anyhow::Result; 8 | use serde_json::Value; 9 | use std::collections::HashSet; 10 | use std::iter; 11 | 12 | impl Post { 13 | pub async fn search( 14 | &self, 15 | skip: usize, 16 | take: usize, 17 | keyword: &str, 18 | ) -> Result<(Vec, usize)> { 19 | let client = &reqwest::Client::new(); 20 | 21 | // total_count is used for patch the buggy blog backend API 22 | // If index is greater than the max page index, API will still return the last page 23 | let total_count = { 24 | let req = { 25 | let url = blog_backend!("/posts/list"); 26 | let query = [ 27 | ("t", 1.to_string()), 28 | ("p", 1.to_string()), 29 | ("s", 1.to_string()), 30 | ("search", keyword.to_string()), 31 | ]; 32 | client.get(url).query(&query).pat_auth(&self.pat) 33 | }; 34 | let resp = req.send().await?; 35 | 36 | // total_count 37 | { 38 | let body = body_or_err(resp).await?; 39 | let json = json::deserialize::(&body)?; 40 | json["postsCount"] 41 | .as_u64() 42 | .expect("as_u64 failed for `postsCount`") as usize 43 | } 44 | }; 45 | 46 | let range = (skip + 1)..=(skip + take).min(total_count); 47 | let id_list = range 48 | .map(|i| async move { 49 | let req = { 50 | let url = blog_backend!("/posts/list"); 51 | let query = [ 52 | ("t", 1.to_string()), 53 | ("p", i.to_string()), 54 | ("s", 1.to_string()), 55 | ("search", keyword.to_string()), 56 | ]; 57 | client.get(url).query(&query).pat_auth(&self.pat) 58 | }; 59 | let resp = req.send().await?; 60 | 61 | let id_list = { 62 | let body = body_or_err(resp).await?; 63 | let mut json = json::deserialize::(&body)?; 64 | let post_id = { 65 | let json = json["postList"].take(); 66 | let [post, ..] = serde_json::from_value::<[Value; 1]>(json)?; 67 | post["id"].as_u64().expect("as_u64 failed for `id`") as usize 68 | }; 69 | let zzk_post_id_list = { 70 | let json = json["zzkSearchResult"]["postIds"].take(); 71 | serde_json::from_value::>(json) 72 | }?; 73 | 74 | zzk_post_id_list 75 | .into_iter() 76 | .chain(iter::once(post_id)) 77 | .collect::>() 78 | }; 79 | 80 | id_list.wrap_ok::() 81 | }) 82 | .join_all() 83 | .await 84 | .into_iter() 85 | .collect::>>()? 86 | .into_iter() 87 | .flatten() 88 | .collect::>() 89 | .into_iter() 90 | .collect::>(); 91 | 92 | (id_list, total_count).wrap_ok() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/api/post/search_self.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::iter::IntoIteratorExt; 5 | use crate::infra::json; 6 | use crate::infra::result::WrapResult; 7 | use anyhow::Result; 8 | use serde_json::Value; 9 | use std::collections::HashSet; 10 | use std::iter; 11 | 12 | impl Post { 13 | pub async fn search_self( 14 | &self, 15 | skip: usize, 16 | take: usize, 17 | keyword: &str, 18 | ) -> Result<(Vec, usize)> { 19 | let client = &reqwest::Client::new(); 20 | 21 | // total_count is used for patch the buggy blog backend API 22 | // If index is greater than the max page index, API will still return the last page 23 | let total_count = { 24 | let req = { 25 | let url = blog_backend!("/posts/list"); 26 | let query = [ 27 | ("t", 1.to_string()), 28 | ("p", 1.to_string()), 29 | ("s", 1.to_string()), 30 | ("search", keyword.to_string()), 31 | ]; 32 | client.get(url).query(&query).pat_auth(&self.pat) 33 | }; 34 | let resp = req.send().await?; 35 | 36 | // total_count 37 | { 38 | let body = body_or_err(resp).await?; 39 | let json = json::deserialize::(&body)?; 40 | json["postsCount"] 41 | .as_u64() 42 | .expect("as_u64 failed for `postsCount`") as usize 43 | } 44 | }; 45 | 46 | let range = (skip + 1)..=(skip + take).min(total_count); 47 | let id_list = range 48 | .map(|i| async move { 49 | let req = { 50 | let url = blog_backend!("/posts/list"); 51 | let query = [ 52 | ("t", 1.to_string()), 53 | ("p", i.to_string()), 54 | ("s", 1.to_string()), 55 | ("search", keyword.to_string()), 56 | ]; 57 | client.get(url).query(&query).pat_auth(&self.pat) 58 | }; 59 | let resp = req.send().await?; 60 | 61 | let id_list = { 62 | let body = body_or_err(resp).await?; 63 | let mut json = json::deserialize::(&body)?; 64 | let post_id = { 65 | let json = json["postList"].take(); 66 | let [post, ..] = serde_json::from_value::<[Value; 1]>(json)?; 67 | post["id"].as_u64().expect("as_u64 failed for `id`") as usize 68 | }; 69 | let zzk_post_id_list = { 70 | let json = json["zzkSearchResult"]["postIds"].take(); 71 | serde_json::from_value::>(json) 72 | }?; 73 | 74 | zzk_post_id_list 75 | .into_iter() 76 | .chain(iter::once(post_id)) 77 | .collect::>() 78 | }; 79 | 80 | id_list.wrap_ok::() 81 | }) 82 | .join_all() 83 | .await 84 | .into_iter() 85 | .collect::>>()? 86 | .into_iter() 87 | .flatten() 88 | .collect::>() 89 | .into_iter() 90 | .collect::>(); 91 | 92 | (id_list, total_count).wrap_ok() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/api/post/search_site.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::iter::IntoIteratorExt; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use crate::openapi; 7 | use anyhow::Result; 8 | use serde::{Deserialize, Serialize}; 9 | use std::ops::Range; 10 | 11 | // HACK: 12 | // Convert skip and take to page index range while page size is 10 13 | fn get_page_index_range(skip: usize, take: usize) -> Range { 14 | let page_size = 10; 15 | let start_index = skip / page_size + 1; 16 | let end_index = if take == 0 { 17 | 0 18 | } else { 19 | ((take + skip) as f64 / page_size as f64).ceil() as usize 20 | }; 21 | start_index..end_index + 1 22 | } 23 | 24 | // HACK: 25 | // Convert skip and take to a range to slice the vec while page size is 10 26 | const fn get_slice_range(skip: usize, take: usize) -> Range { 27 | let skip_left = skip - (skip / 10) * 10; 28 | skip_left..skip_left + take 29 | } 30 | 31 | #[test] 32 | fn test_get_page_index_range_and_get_slice_range() { 33 | fn f(skip: usize, take: usize) -> (Range, Range) { 34 | ( 35 | get_page_index_range(skip, take), 36 | get_slice_range(skip, take), 37 | ) 38 | } 39 | 40 | assert_eq!(f(0, 00), (1..1, 0..00)); 41 | assert_eq!(f(0, 01), (1..2, 0..01)); 42 | assert_eq!(f(0, 09), (1..2, 0..09)); 43 | assert_eq!(f(0, 10), (1..2, 0..10)); 44 | assert_eq!(f(0, 11), (1..3, 0..11)); 45 | assert_eq!(f(0, 19), (1..3, 0..19)); 46 | assert_eq!(f(0, 20), (1..3, 0..20)); 47 | assert_eq!(f(0, 21), (1..4, 0..21)); 48 | 49 | assert_eq!(f(1, 00), (1..1, 1..01)); 50 | assert_eq!(f(1, 01), (1..2, 1..02)); 51 | assert_eq!(f(1, 09), (1..2, 1..10)); 52 | assert_eq!(f(1, 10), (1..3, 1..11)); 53 | assert_eq!(f(1, 11), (1..3, 1..12)); 54 | assert_eq!(f(1, 19), (1..3, 1..20)); 55 | assert_eq!(f(1, 20), (1..4, 1..21)); 56 | assert_eq!(f(1, 21), (1..4, 1..22)); 57 | assert_eq!(f(1, 29), (1..4, 1..30)); 58 | 59 | assert_eq!(f(9, 00), (1..1, 9..09)); 60 | assert_eq!(f(9, 01), (1..2, 9..10)); 61 | assert_eq!(f(9, 09), (1..3, 9..18)); 62 | assert_eq!(f(9, 10), (1..3, 9..19)); 63 | assert_eq!(f(9, 11), (1..3, 9..20)); 64 | assert_eq!(f(9, 19), (1..4, 9..28)); 65 | assert_eq!(f(9, 20), (1..4, 9..29)); 66 | assert_eq!(f(9, 21), (1..4, 9..30)); 67 | assert_eq!(f(9, 29), (1..5, 9..38)); 68 | 69 | assert_eq!(f(10, 00), (2..1, 0..00)); 70 | assert_eq!(f(10, 01), (2..3, 0..01)); 71 | assert_eq!(f(10, 09), (2..3, 0..09)); 72 | assert_eq!(f(10, 10), (2..3, 0..10)); 73 | assert_eq!(f(10, 11), (2..4, 0..11)); 74 | assert_eq!(f(10, 19), (2..4, 0..19)); 75 | assert_eq!(f(10, 20), (2..4, 0..20)); 76 | assert_eq!(f(10, 21), (2..5, 0..21)); 77 | } 78 | 79 | #[derive(Clone, Debug, Serialize, Deserialize)] 80 | pub struct SearchResultEntry { 81 | #[serde(rename = "Title")] 82 | pub title: String, 83 | #[serde(rename = "Content")] 84 | pub summary: String, 85 | 86 | #[serde(rename = "UserName")] 87 | pub user_name: String, 88 | 89 | #[serde(rename = "VoteTimes")] 90 | pub vote_count: usize, 91 | #[serde(rename = "ViewTimes")] 92 | pub view_count: usize, 93 | #[serde(rename = "CommentTimes")] 94 | pub comment_count: usize, 95 | 96 | #[serde(rename = "PublishTime")] 97 | pub create_time: String, 98 | #[serde(rename = "Uri")] 99 | pub url: String, 100 | } 101 | 102 | impl Post { 103 | pub async fn search_site( 104 | &self, 105 | skip: usize, 106 | take: usize, 107 | keyword: &str, 108 | ) -> Result> { 109 | let client = &reqwest::Client::new(); 110 | 111 | let slice_range = get_slice_range(skip, take); 112 | 113 | let entry_vec = { 114 | let entry_vec = get_page_index_range(skip, take) 115 | .map(|i| async move { 116 | let req = { 117 | let url = openapi!("/zzkdocuments/blog"); 118 | let query = [ 119 | ("pageIndex", i.to_string()), 120 | ("keyWords", keyword.to_string()), 121 | ]; 122 | client.get(url).query(&query).pat_auth(&self.pat) 123 | }; 124 | let resp = req.send().await?; 125 | 126 | let body = body_or_err(resp).await?; 127 | json::deserialize::>(&body) 128 | }) 129 | .join_all() 130 | .await 131 | .into_iter() 132 | .collect::>>>()? 133 | .concat(); 134 | 135 | entry_vec 136 | .into_iter() 137 | .enumerate() 138 | .filter(|(i, _)| slice_range.contains(i)) 139 | .map(|(_, entry)| entry) 140 | .collect::>() 141 | }; 142 | 143 | entry_vec.wrap_ok() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/api/post/update.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::Post; 2 | use crate::blog_backend; 3 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 4 | use crate::infra::json; 5 | use crate::infra::result::WrapResult; 6 | use anyhow::Result; 7 | use serde_json::{json, Value}; 8 | 9 | impl Post { 10 | pub async fn update( 11 | &self, 12 | id: usize, 13 | title: &Option, 14 | body: &Option, 15 | publish: &Option, 16 | ) -> Result { 17 | let client = reqwest::Client::new(); 18 | 19 | let req = { 20 | let url = blog_backend!("/posts"); 21 | 22 | let json = { 23 | let mut json = self.get_one_raw(id).await?; 24 | if let Some(title) = title { 25 | json["title"] = json!(title) 26 | } 27 | if let Some(body) = body { 28 | json["postBody"] = json!(body) 29 | } 30 | if let Some(publish) = publish { 31 | json["isPublished"] = json!(publish) 32 | } 33 | json 34 | }; 35 | 36 | client.post(url).json(&json).pat_auth(&self.pat) 37 | }; 38 | 39 | let resp = req.send().await?; 40 | 41 | let id = { 42 | let body = body_or_err(resp).await?; 43 | let json = json::deserialize::(&body)?; 44 | json["id"].as_u64().expect("as_u64 failed for `id`") as usize 45 | }; 46 | 47 | id.wrap_ok() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/api/user/info.rs: -------------------------------------------------------------------------------- 1 | use crate::api::user::User; 2 | use crate::infra::http::{body_or_err, RequestBuilderExt}; 3 | use crate::infra::json; 4 | use crate::infra::result::WrapResult; 5 | use crate::openapi; 6 | use anyhow::Result; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone)] 10 | #[serde(rename_all = "PascalCase")] 11 | pub struct UserInfo { 12 | pub user_id: String, 13 | #[serde(rename = "SpaceUserID")] 14 | pub space_user_id: usize, 15 | pub blog_id: usize, 16 | pub display_name: String, 17 | pub face: String, 18 | pub avatar: String, 19 | pub seniority: String, 20 | pub blog_app: String, 21 | pub following_count: usize, 22 | #[serde(rename = "FollowerCount")] 23 | pub followers_count: usize, 24 | pub is_vip: bool, 25 | pub joined: String, 26 | } 27 | 28 | impl User { 29 | pub async fn get_info(&self) -> Result { 30 | let client = reqwest::Client::new(); 31 | 32 | let req = { 33 | let url = openapi!("/users"); 34 | client.get(url).pat_auth(&self.pat) 35 | }; 36 | 37 | let resp = req.send().await?; 38 | 39 | let user_info = { 40 | let body = body_or_err(resp).await?; 41 | json::deserialize::(&body)? 42 | }; 43 | 44 | user_info.wrap_ok() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/api/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod info; 2 | 3 | pub struct User { 4 | pat: String, 5 | } 6 | 7 | impl User { 8 | pub const fn new(pat: String) -> Self { 9 | Self { pat } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/args/cmd/fav.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[non_exhaustive] 5 | pub struct Opt { 6 | #[arg(verbatim_doc_comment)] 7 | /// Show favorite list, order by time in DESC 8 | /// Example: cnb fav --list 9 | #[arg(long)] 10 | #[arg(short = 'l')] 11 | pub list: bool, 12 | } 13 | -------------------------------------------------------------------------------- /src/args/cmd/ing.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::IngType; 2 | use clap::{Parser, Subcommand}; 3 | 4 | #[derive(Parser, Debug)] 5 | #[non_exhaustive] 6 | pub struct Opt { 7 | #[command(subcommand)] 8 | pub cmd: Option, 9 | 10 | #[arg(verbatim_doc_comment)] 11 | /// Publish ing with specific content 12 | /// Example: cnb ing --publish 'Hello world' 13 | /// The visibility of ing is public 14 | /// * 15 | #[arg(long)] 16 | #[arg(short = 'p')] 17 | #[arg(visible_alias = "pub")] 18 | #[arg(value_name = "CONTENT")] 19 | pub publish: Option, 20 | 21 | #[arg(verbatim_doc_comment)] 22 | /// Comment ing with specific content 23 | /// Example: cnb --id 114514 ing --comment 'Hello world' 24 | /// You should also specify the id of the ing via --id 25 | #[arg(long)] 26 | #[arg(short = 'c')] 27 | #[arg(value_name = "CONTENT")] 28 | pub comment: Option, 29 | } 30 | 31 | #[derive(Debug, Subcommand)] 32 | #[non_exhaustive] 33 | pub enum Cmd { 34 | #[clap(verbatim_doc_comment)] 35 | /// Show ing list, order by time in DESC 36 | /// Example: cnb ing list 37 | /// * 38 | #[clap(visible_alias = "l")] 39 | List { 40 | #[arg(verbatim_doc_comment)] 41 | /// Ing type to show 42 | /// Example: cnb ing list --type myself 43 | /// * 44 | #[arg(long)] 45 | #[arg(value_name = "TYPE")] 46 | #[arg(default_value = "public")] 47 | r#type: Option, 48 | 49 | #[arg(verbatim_doc_comment)] 50 | /// Align ing content to user name automatically 51 | /// Example: cnb ing list --align 52 | #[arg(long)] 53 | #[arg(value_name = "BOOL")] 54 | #[arg(default_value_t = true)] 55 | align: bool, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /src/args/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fav; 2 | pub mod ing; 3 | pub mod news; 4 | pub mod post; 5 | pub mod user; 6 | 7 | use clap::Subcommand; 8 | 9 | #[derive(Debug, Subcommand)] 10 | #[non_exhaustive] 11 | pub enum Cmd { 12 | /// User operations 13 | #[clap(visible_alias = "u")] 14 | User(user::Opt), 15 | /// Ing operations 16 | #[clap(visible_alias = "i")] 17 | Ing(ing::Opt), 18 | /// Post operations 19 | #[clap(visible_alias = "p")] 20 | Post(post::Opt), 21 | /// News operations 22 | #[clap(visible_alias = "n")] 23 | News(news::Opt), 24 | /// Favorite operations 25 | #[clap(visible_alias = "f")] 26 | Fav(fav::Opt), 27 | } 28 | -------------------------------------------------------------------------------- /src/args/cmd/news.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[non_exhaustive] 5 | pub struct Opt { 6 | #[arg(verbatim_doc_comment)] 7 | /// Show news list, order by time in DESC 8 | /// Example: cnb news --list 9 | #[arg(long)] 10 | #[arg(short = 'l')] 11 | pub list: bool, 12 | } 13 | -------------------------------------------------------------------------------- /src/args/cmd/post.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[non_exhaustive] 5 | pub struct Opt { 6 | #[clap(verbatim_doc_comment)] 7 | /// Show title and content of a specific post 8 | /// Example: cnb --id 114514 post --show 9 | /// You should also specify the id of the post via --id 10 | #[arg(long)] 11 | #[arg(short = 's')] 12 | pub show: bool, 13 | 14 | #[arg(verbatim_doc_comment)] 15 | /// Show metadata of a specific post 16 | /// Example: cnb --id 114514 post --show-meta 17 | /// You should also specify the id of the post via --id 18 | /// * 19 | #[arg(long)] 20 | #[arg(visible_alias = "sm")] 21 | pub show_meta: bool, 22 | 23 | #[arg(verbatim_doc_comment)] 24 | /// Show comment list of post, order by time in DESC 25 | /// Example: cnb --id 114514 post --show-comment 26 | /// You should also specify the id of the post via --id 27 | /// * 28 | #[arg(long)] 29 | #[arg(visible_alias = "sc")] 30 | pub show_comment: bool, 31 | 32 | #[arg(verbatim_doc_comment)] 33 | /// Show post list, order by time in DESC 34 | /// Example: cnb post --list 35 | /// should in range [0,100] 36 | /// If greater than 100, it will be set to 100 37 | #[arg(long)] 38 | #[arg(short = 'l')] 39 | pub list: bool, 40 | 41 | #[arg(verbatim_doc_comment)] 42 | /// Delete post 43 | /// Example: cnb --id 114514 post --delete 44 | /// You should also specify the id of the post via --id 45 | /// * 46 | #[arg(long)] 47 | #[arg(visible_alias = "del")] 48 | pub delete: bool, 49 | 50 | #[command(subcommand)] 51 | pub cmd: Option, 52 | } 53 | 54 | #[derive(Parser, Debug)] 55 | #[non_exhaustive] 56 | pub struct CreateCmd { 57 | #[arg(verbatim_doc_comment)] 58 | /// Set post title 59 | /// Example: cnb post create --title 'Title' --body 'Body' 60 | #[arg(long)] 61 | #[arg(value_name = "TITLE")] 62 | pub title: String, 63 | 64 | #[arg(verbatim_doc_comment)] 65 | /// Set post body 66 | /// Example: cnb post create --title 'Title' --body 'Body' 67 | #[arg(long)] 68 | #[arg(value_name = "BODY")] 69 | pub body: String, 70 | 71 | #[arg(verbatim_doc_comment)] 72 | /// Set post status to publish 73 | /// Example: cnb post create --title 'Title' --body 'Body' --publish 74 | /// * 75 | #[arg(long)] 76 | #[arg(visible_alias = "pub")] 77 | pub publish: bool, 78 | } 79 | 80 | #[derive(Parser, Debug)] 81 | #[non_exhaustive] 82 | pub struct UpdateCmd { 83 | #[arg(verbatim_doc_comment)] 84 | /// Set post title 85 | /// Example: cnb --id 114514 post update --title 'Title' 86 | #[arg(long)] 87 | #[arg(value_name = "TITLE")] 88 | pub title: Option, 89 | 90 | #[arg(verbatim_doc_comment)] 91 | /// Set post body 92 | /// Example: cnb --id 114514 post update --body 'Body' 93 | #[arg(long)] 94 | #[arg(value_name = "BODY")] 95 | pub body: Option, 96 | 97 | #[arg(verbatim_doc_comment)] 98 | /// Set post publish state 99 | /// Example: cnb --id 114514 post update --publish true 100 | /// * 101 | #[arg(long)] 102 | #[arg(value_name = "BOOL")] 103 | #[arg(visible_alias = "pub")] 104 | pub publish: Option, 105 | } 106 | 107 | #[derive(Parser, Debug)] 108 | #[non_exhaustive] 109 | pub struct SearchCmd { 110 | #[arg(verbatim_doc_comment)] 111 | /// Search self post 112 | /// Example: cnb post search --self 'Keyword' 113 | #[arg(long)] 114 | #[arg(long = "self")] 115 | #[arg(value_name = "KEYWORD")] 116 | pub self_keyword: Option, 117 | 118 | #[arg(verbatim_doc_comment)] 119 | /// Search site post 120 | /// Example: cnb post search --site 'Keyword' 121 | #[arg(long)] 122 | #[arg(long = "site")] 123 | #[arg(value_name = "KEYWORD")] 124 | pub site_keyword: Option, 125 | } 126 | 127 | #[derive(Debug, Subcommand)] 128 | #[non_exhaustive] 129 | pub enum Cmd { 130 | #[clap(verbatim_doc_comment)] 131 | /// Create post 132 | /// Example: cnb post create --title 'Title' --body 'Body' 133 | /// * 134 | #[clap(visible_alias = "c")] 135 | Create(CreateCmd), 136 | 137 | #[clap(verbatim_doc_comment)] 138 | /// Update post 139 | /// Example: cnb --id 114514 post update --title 'Title' 140 | /// You should also specify the id of the post via --id 141 | /// * 142 | #[clap(visible_alias = "u")] 143 | Update(UpdateCmd), 144 | 145 | #[clap(verbatim_doc_comment)] 146 | /// Search post 147 | /// Example: cnb post search --self 'Keyword' 148 | /// * 149 | #[clap(visible_alias = "s")] 150 | Search(SearchCmd), 151 | } 152 | -------------------------------------------------------------------------------- /src/args/cmd/user.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[non_exhaustive] 5 | pub struct Opt { 6 | #[arg(verbatim_doc_comment)] 7 | /// Login with your personal access token (PAT) 8 | /// Example: cnb user --login 'FOOBARBAZ' 9 | /// PAT will be saved in ~/.cnbrc 10 | /// You can create PAT in https://account.cnblogs.com/tokens 11 | #[arg(long)] 12 | #[arg(value_name = "PAT")] 13 | pub login: Option, 14 | 15 | #[arg(verbatim_doc_comment)] 16 | /// Logout and remove ~/.cnbrc 17 | /// Example: cnb user --logout 18 | #[arg(long)] 19 | pub logout: bool, 20 | 21 | #[arg(verbatim_doc_comment)] 22 | /// Show user info 23 | /// Example: cnb user --info 24 | #[arg(long)] 25 | #[arg(short = 'i')] 26 | pub info: bool, 27 | } 28 | -------------------------------------------------------------------------------- /src/args/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod parser; 3 | 4 | use crate::args::cmd::Cmd; 5 | use clap::{Parser, ValueEnum}; 6 | 7 | #[derive(Clone, Debug, Parser, ValueEnum)] 8 | pub enum Style { 9 | Colorful, 10 | Normal, 11 | Json, 12 | } 13 | 14 | #[derive(Clone, Debug, Parser, ValueEnum)] 15 | pub enum TimeStyle { 16 | Friendly, 17 | Normal, 18 | } 19 | 20 | #[derive(Parser, Debug)] 21 | #[non_exhaustive] 22 | pub struct GlobalOpt { 23 | #[arg(verbatim_doc_comment)] 24 | /// Execute with specific PAT 25 | /// Example: cnb --with-pat 'FOOBARBAZ' post --list 26 | /// Your PAT in ~/.cnbrc will be ignored in this execution if it exists 27 | /// Please login if you don't want to input PAT everytime, try 'cnb user --help' for more details 28 | #[arg(long)] 29 | #[arg(value_name = "PAT")] 30 | pub with_pat: Option, 31 | 32 | #[arg(verbatim_doc_comment)] 33 | /// Execute in debug mode, this will print some messages for the developer 34 | /// Example: cnb --debug ing list 35 | /// THIS OPTION IS UNSTABLE FOREVER and any output from it may change in the future 36 | /// You should NEVER rely on the output while you turn this option on 37 | /// * 38 | #[arg(long)] 39 | #[clap(visible_alias = "dbg")] 40 | pub debug: bool, 41 | 42 | #[arg(verbatim_doc_comment)] 43 | /// Configure the output style 44 | /// Example: cnb --style json ing list 45 | /// * 46 | #[arg(long)] 47 | #[arg(value_enum)] 48 | #[arg(hide_possible_values = true)] 49 | #[arg(default_value_t = Style::Colorful)] 50 | #[arg(value_name = "NAME")] 51 | pub style: Style, 52 | 53 | #[arg(verbatim_doc_comment)] 54 | /// Configure the time style 55 | /// Example: cnb --style normal ing list 56 | /// This option does not affect the output of '--style json' 57 | /// * 58 | #[arg(long)] 59 | #[arg(value_enum)] 60 | #[arg(hide_possible_values = true)] 61 | #[arg(default_value_t = TimeStyle::Friendly)] 62 | #[arg(value_name = "NAME")] 63 | pub time_style: TimeStyle, 64 | 65 | #[arg(verbatim_doc_comment)] 66 | /// Fail if error occurred 67 | /// Example: cnb --fail-on-error ing list 68 | /// * 69 | #[arg(long)] 70 | #[clap(visible_alias = "foe")] 71 | #[arg(default_value_t = false)] 72 | pub fail_on_error: bool, 73 | 74 | #[arg(verbatim_doc_comment)] 75 | /// Suppress all output 76 | /// Example: cnb --quiet ing list 77 | /// * 78 | #[arg(long)] 79 | #[clap(visible_alias = "silent")] 80 | #[arg(default_value_t = false)] 81 | pub quiet: bool, 82 | } 83 | 84 | #[derive(Parser, Debug)] 85 | #[command(author, about, long_about = None, version)] 86 | #[non_exhaustive] 87 | pub struct Args { 88 | #[command(subcommand)] 89 | pub cmd: Option, 90 | #[clap(flatten)] 91 | pub global_opt: GlobalOpt, 92 | 93 | #[arg(verbatim_doc_comment)] 94 | /// Provide ID required by other options 95 | /// Example: cnb --id 114514 post --show 96 | #[arg(long)] 97 | pub id: Option, 98 | 99 | #[arg(verbatim_doc_comment)] 100 | /// Reverse list output 101 | /// Example: cnb --rev ing list 102 | #[arg(long)] 103 | pub rev: bool, 104 | 105 | #[arg(verbatim_doc_comment)] 106 | /// Skip items while request list 107 | /// Example: cnb --skip 2 ing list 108 | /// Use this option to save network I/O if some items of the list output are not needed 109 | /// If this option is required but not specified, it will be set to 0 110 | #[arg(long)] 111 | #[arg(short = 's')] 112 | #[arg(value_name = "LENGTH")] 113 | pub skip: Option, 114 | 115 | #[arg(verbatim_doc_comment)] 116 | /// Take items while request list 117 | /// Example: cnb --take 2 ing list 118 | /// Use this option to save network I/O if only a subset of the list output are required 119 | /// should be in the range [0,100] 120 | /// If is greater than 100, it will be set to 100 121 | /// If this option is required but not specified, it will be set to 8 122 | #[arg(long)] 123 | #[arg(short = 't')] 124 | #[arg(value_name = "LENGTH")] 125 | pub take: Option, 126 | } 127 | -------------------------------------------------------------------------------- /src/args/parser/fav.rs: -------------------------------------------------------------------------------- 1 | use crate::args::parser::{get_skip, get_take}; 2 | use crate::args::{cmd, Args, Cmd}; 3 | use crate::infra::option::WrapOption; 4 | 5 | pub fn list_fav(args: &Args) -> Option<(usize, usize)> { 6 | match args { 7 | Args { 8 | cmd: Some(Cmd::Fav(cmd::fav::Opt { list: true })), 9 | id: None, 10 | rev: _, 11 | skip, 12 | take, 13 | global_opt: _, 14 | } => { 15 | let skip = get_skip(skip); 16 | let take = get_take(take); 17 | (skip, take) 18 | } 19 | _ => return None, 20 | } 21 | .wrap_some() 22 | } 23 | -------------------------------------------------------------------------------- /src/args/parser/ing.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::IngType; 2 | use crate::args::parser::{get_skip, get_take}; 3 | use crate::args::{cmd, Args, Cmd}; 4 | use crate::infra::option::WrapOption; 5 | 6 | pub fn list_ing(args: &Args) -> Option<(usize, usize, IngType, bool)> { 7 | match args { 8 | Args { 9 | cmd: 10 | Some(Cmd::Ing(cmd::ing::Opt { 11 | cmd: Some(cmd::ing::Cmd::List { r#type, align }), 12 | publish: None, 13 | comment: None, 14 | })), 15 | id: None, 16 | rev: _, 17 | skip, 18 | take, 19 | global_opt: _, 20 | } => { 21 | let skip = get_skip(skip); 22 | let take = get_take(take); 23 | let r#type = r#type.clone().unwrap_or(IngType::Public); 24 | (skip, take, r#type, *align) 25 | } 26 | _ => return None, 27 | } 28 | .wrap_some() 29 | } 30 | 31 | pub fn publish_ing(args: &Args) -> Option<&String> { 32 | match args { 33 | Args { 34 | cmd: 35 | Some(Cmd::Ing(cmd::ing::Opt { 36 | cmd: None, 37 | publish: Some(content), 38 | comment: None, 39 | })), 40 | id: None, 41 | rev: false, 42 | skip: None, 43 | take: None, 44 | global_opt: _, 45 | } => content, 46 | _ => return None, 47 | } 48 | .wrap_some() 49 | } 50 | 51 | pub fn comment_ing(args: &Args) -> Option<(&String, usize)> { 52 | match args { 53 | Args { 54 | cmd: 55 | Some(Cmd::Ing(cmd::ing::Opt { 56 | cmd: None, 57 | publish: None, 58 | comment: Some(content), 59 | })), 60 | id: Some(id), 61 | rev: false, 62 | skip: None, 63 | take: None, 64 | global_opt: _, 65 | } => (content, *id), 66 | _ => return None, 67 | } 68 | .wrap_some() 69 | } 70 | -------------------------------------------------------------------------------- /src/args/parser/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fav; 2 | pub mod ing; 3 | pub mod news; 4 | pub mod post; 5 | pub mod user; 6 | 7 | use crate::args::{Args, GlobalOpt}; 8 | 9 | fn get_skip(skip: &Option) -> usize { 10 | skip.unwrap_or(0) 11 | } 12 | 13 | fn get_take(take: &Option) -> usize { 14 | take.unwrap_or(8).min(100) 15 | } 16 | 17 | pub const fn no_operation(args: &Args) -> bool { 18 | matches!( 19 | args, 20 | Args { 21 | cmd: None, 22 | id: None, 23 | rev: false, 24 | skip: None, 25 | take: None, 26 | global_opt: GlobalOpt { with_pat: None, .. } 27 | } 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/args/parser/news.rs: -------------------------------------------------------------------------------- 1 | use crate::args::parser::{get_skip, get_take}; 2 | use crate::args::{cmd, Args, Cmd}; 3 | use crate::infra::option::WrapOption; 4 | 5 | pub fn list_news(args: &Args) -> Option<(usize, usize)> { 6 | match args { 7 | Args { 8 | cmd: Some(Cmd::News(cmd::news::Opt { list: true })), 9 | id: None, 10 | rev: _, 11 | skip, 12 | take, 13 | global_opt: _, 14 | } => { 15 | let skip = get_skip(skip); 16 | let take = get_take(take); 17 | (skip, take) 18 | } 19 | _ => return None, 20 | } 21 | .wrap_some() 22 | } 23 | -------------------------------------------------------------------------------- /src/args/parser/post.rs: -------------------------------------------------------------------------------- 1 | use crate::args::cmd::post::{CreateCmd, UpdateCmd}; 2 | use crate::args::parser::{get_skip, get_take}; 3 | use crate::args::{cmd, Args, Cmd}; 4 | use crate::infra::option::WrapOption; 5 | 6 | pub fn list_post(args: &Args) -> Option<(usize, usize)> { 7 | match args { 8 | Args { 9 | cmd: 10 | Some(Cmd::Post(cmd::post::Opt { 11 | show: false, 12 | show_meta: false, 13 | show_comment: false, 14 | list: true, 15 | delete: false, 16 | cmd: None, 17 | })), 18 | id: None, 19 | rev: _, 20 | skip, 21 | take, 22 | global_opt: _, 23 | } => { 24 | let skip = get_skip(skip); 25 | let take = get_take(take); 26 | (skip, take) 27 | } 28 | _ => return None, 29 | } 30 | .wrap_some() 31 | } 32 | 33 | pub fn show_post(args: &Args) -> Option { 34 | match args { 35 | Args { 36 | cmd: 37 | Some(Cmd::Post(cmd::post::Opt { 38 | show: true, 39 | show_meta: false, 40 | show_comment: false, 41 | list: false, 42 | delete: false, 43 | cmd: None, 44 | })), 45 | id: Some(id), 46 | rev: false, 47 | skip: None, 48 | take: None, 49 | global_opt: _, 50 | } => *id, 51 | _ => return None, 52 | } 53 | .wrap_some() 54 | } 55 | 56 | pub fn show_post_meta(args: &Args) -> Option { 57 | match args { 58 | Args { 59 | cmd: 60 | Some(Cmd::Post(cmd::post::Opt { 61 | show: false, 62 | show_meta: true, 63 | show_comment: false, 64 | list: false, 65 | delete: false, 66 | cmd: None, 67 | })), 68 | id: Some(id), 69 | rev: false, 70 | skip: None, 71 | take: None, 72 | global_opt: _, 73 | } => *id, 74 | _ => return None, 75 | } 76 | .wrap_some() 77 | } 78 | 79 | pub fn show_post_comment(args: &Args) -> Option { 80 | match args { 81 | Args { 82 | cmd: 83 | Some(Cmd::Post(cmd::post::Opt { 84 | show: false, 85 | show_meta: false, 86 | show_comment: true, 87 | list: false, 88 | delete: false, 89 | cmd: None, 90 | })), 91 | id: Some(id), 92 | rev: _, 93 | skip: None, 94 | take: None, 95 | global_opt: _, 96 | } => *id, 97 | _ => return None, 98 | } 99 | .wrap_some() 100 | } 101 | 102 | pub fn search_self_post(args: &Args) -> Option<(&String, usize, usize)> { 103 | match args { 104 | Args { 105 | cmd: 106 | Some(Cmd::Post(cmd::post::Opt { 107 | show: false, 108 | show_meta: false, 109 | show_comment: false, 110 | list: false, 111 | delete: false, 112 | cmd: 113 | Some(cmd::post::Cmd::Search(cmd::post::SearchCmd { 114 | self_keyword: Some(keyword), 115 | site_keyword: None, 116 | })), 117 | })), 118 | id: None, 119 | rev: _, 120 | skip, 121 | take, 122 | global_opt: _, 123 | } => { 124 | let skip = get_skip(skip); 125 | let take = get_take(take); 126 | (keyword, skip, take) 127 | } 128 | _ => return None, 129 | } 130 | .wrap_some() 131 | } 132 | 133 | pub fn search_site_post(args: &Args) -> Option<(&String, usize, usize)> { 134 | match args { 135 | Args { 136 | cmd: 137 | Some(Cmd::Post(cmd::post::Opt { 138 | show: false, 139 | show_meta: false, 140 | show_comment: false, 141 | list: false, 142 | delete: false, 143 | cmd: 144 | Some(cmd::post::Cmd::Search(cmd::post::SearchCmd { 145 | self_keyword: None, 146 | site_keyword: Some(keyword), 147 | })), 148 | })), 149 | id: None, 150 | rev: _, 151 | skip, 152 | take, 153 | global_opt: _, 154 | } => { 155 | let skip = get_skip(skip); 156 | let take = get_take(take); 157 | (keyword, skip, take) 158 | } 159 | _ => return None, 160 | } 161 | .wrap_some() 162 | } 163 | 164 | pub fn delete_post(args: &Args) -> Option { 165 | match args { 166 | Args { 167 | cmd: 168 | Some(Cmd::Post(cmd::post::Opt { 169 | show: false, 170 | show_meta: false, 171 | show_comment: false, 172 | list: false, 173 | delete: true, 174 | cmd: None, 175 | })), 176 | id: Some(id), 177 | rev: false, 178 | skip: None, 179 | take: None, 180 | global_opt: _, 181 | } => *id, 182 | _ => return None, 183 | } 184 | .wrap_some() 185 | } 186 | 187 | pub fn create_post(args: &Args) -> Option<&CreateCmd> { 188 | match args { 189 | Args { 190 | cmd: 191 | Some(Cmd::Post(cmd::post::Opt { 192 | show: false, 193 | show_meta: false, 194 | show_comment: false, 195 | list: false, 196 | delete: false, 197 | cmd: Some(cmd::post::Cmd::Create(cmd)), 198 | })), 199 | id: None, 200 | rev: _, 201 | skip: None, 202 | take: None, 203 | global_opt: _, 204 | } => cmd, 205 | _ => return None, 206 | } 207 | .wrap_some() 208 | } 209 | 210 | pub fn update_post(args: &Args) -> Option<(usize, &UpdateCmd)> { 211 | match args { 212 | Args { 213 | cmd: 214 | Some(Cmd::Post(cmd::post::Opt { 215 | show: false, 216 | show_meta: false, 217 | show_comment: false, 218 | list: false, 219 | delete: false, 220 | cmd: Some(cmd::post::Cmd::Update(cmd)), 221 | })), 222 | id: Some(id), 223 | rev: _, 224 | skip: None, 225 | take: None, 226 | global_opt: _, 227 | } => (*id, cmd), 228 | _ => return None, 229 | } 230 | .wrap_some() 231 | } 232 | -------------------------------------------------------------------------------- /src/args/parser/user.rs: -------------------------------------------------------------------------------- 1 | use crate::args::{cmd, Args, Cmd, GlobalOpt}; 2 | use crate::infra::option::WrapOption; 3 | 4 | pub fn login(args: &Args) -> Option<&String> { 5 | match args { 6 | Args { 7 | cmd: 8 | Some(Cmd::User(cmd::user::Opt { 9 | login: Some(pat), 10 | logout: false, 11 | info: false, 12 | })), 13 | id: None, 14 | rev: false, 15 | skip: None, 16 | take: None, 17 | global_opt: GlobalOpt { with_pat: None, .. }, 18 | } => pat, 19 | _ => return None, 20 | } 21 | .wrap_some() 22 | } 23 | 24 | pub const fn logout(args: &Args) -> bool { 25 | matches!( 26 | args, 27 | Args { 28 | cmd: Some(Cmd::User(cmd::user::Opt { 29 | login: None, 30 | logout: true, 31 | info: false, 32 | })), 33 | id: None, 34 | rev: false, 35 | skip: None, 36 | take: None, 37 | global_opt: GlobalOpt { with_pat: None, .. }, 38 | } 39 | ) 40 | } 41 | 42 | pub const fn user_info(args: &Args) -> bool { 43 | matches!( 44 | args, 45 | Args { 46 | cmd: Some(Cmd::User(cmd::user::Opt { 47 | login: None, 48 | logout: false, 49 | info: true, 50 | })), 51 | id: None, 52 | rev: false, 53 | skip: None, 54 | take: None, 55 | global_opt: _, 56 | } 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/display/colorful/fav.rs: -------------------------------------------------------------------------------- 1 | use crate::api::fav::get_list::FavEntry; 2 | use crate::args::TimeStyle; 3 | use crate::display::colorful::fmt_err; 4 | use crate::infra::result::WrapResult; 5 | use crate::infra::str::StrExt; 6 | use crate::infra::terminal::get_term_width; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use colored::Colorize; 10 | use std::fmt::Write; 11 | use std::ops::Not; 12 | 13 | pub fn list_fav( 14 | time_style: &TimeStyle, 15 | fav_iter: Result>, 16 | ) -> Result { 17 | let fav_iter = match fav_iter { 18 | Ok(o) => o, 19 | Err(e) => return fmt_err(&e).wrap_ok(), 20 | }; 21 | 22 | fav_iter 23 | .map(|fav| try { 24 | let mut buf = String::new(); 25 | { 26 | let buf = &mut buf; 27 | let create_time = display_cnb_time(&fav.create_time, time_style); 28 | writeln!(buf, "{} {}", create_time.dimmed(), fav.url.dimmed())?; 29 | writeln!(buf, " {}", fav.title)?; 30 | 31 | let summary = { 32 | fav.summary.width_split(get_term_width() - 4).map_or_else( 33 | || fav.summary.clone(), 34 | |vec| { 35 | vec.into_iter() 36 | .map(|line| format!(" {}", line)) 37 | .collect::>() 38 | .join("\n") 39 | }, 40 | ) 41 | }; 42 | if summary.is_empty().not() { 43 | writeln!(buf, "{}", summary.dimmed())?; 44 | } 45 | } 46 | buf 47 | }) 48 | .try_fold(String::new(), |mut acc, buf: Result| try { 49 | writeln!(&mut acc, "{}", buf?)?; 50 | acc 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/display/colorful/ing.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::get_comment_list::IngCommentEntry; 2 | use crate::api::ing::get_list::IngEntry; 3 | use crate::api::ing::{ 4 | fmt_content, get_ing_at_user_tag_text, ing_star_tag_to_text, rm_ing_at_user_tag, IngSendFrom, 5 | }; 6 | use crate::args::TimeStyle; 7 | use crate::display::colorful::fmt_err; 8 | use crate::infra::result::WrapResult; 9 | use crate::infra::str::StrExt; 10 | use crate::infra::terminal::get_term_width; 11 | use crate::infra::time::display_cnb_time; 12 | use anyhow::Result; 13 | use colored::Colorize; 14 | use std::fmt::Write; 15 | use std::ops::Not; 16 | use unicode_width::UnicodeWidthStr; 17 | 18 | // TODO: rm unnecessary line divider 19 | pub fn list_ing( 20 | time_style: &TimeStyle, 21 | ing_with_comment_iter: Result)>>, 22 | align: bool, 23 | ) -> Result { 24 | let mut ing_with_comment_iter = match ing_with_comment_iter { 25 | Ok(o) => o, 26 | Err(e) => return fmt_err(&e).wrap_ok(), 27 | }; 28 | 29 | ing_with_comment_iter.try_fold(String::new(), |mut buf, (ing, comment_list)| try { 30 | { 31 | let buf = &mut buf; 32 | let create_time = display_cnb_time(&ing.create_time, time_style); 33 | write!(buf, "{}", create_time.dimmed())?; 34 | 35 | let send_from_mark = match ing.send_from { 36 | IngSendFrom::Cli => Some("CLI"), 37 | IngSendFrom::CellPhone => Some("Mobile"), 38 | IngSendFrom::VsCode => Some("VSCode"), 39 | IngSendFrom::Web => Some("Web"), 40 | _ => None, 41 | }; 42 | if let Some(mark) = send_from_mark { 43 | write!(buf, " {}", mark.dimmed())?; 44 | } 45 | if ing.is_lucky { 46 | let star_text = ing_star_tag_to_text(&ing.icons); 47 | write!(buf, " {}⭐", star_text.yellow())?; 48 | } 49 | writeln!(buf, " {} {}", "#".dimmed(), ing.id.to_string().dimmed())?; 50 | let content = if align { 51 | let user_name_width = ing.user_name.width_cjk(); 52 | let left_width = get_term_width().saturating_sub(user_name_width + 3); 53 | fmt_content(&ing.content) 54 | .width_split(left_width) 55 | .map_or_else( 56 | || ing.content.clone(), 57 | |lines| { 58 | if comment_list.is_empty().not() { 59 | lines.join("\n").replace( 60 | '\n', 61 | &format!("\n │{}", " ".repeat(user_name_width - 2)), 62 | ) 63 | } else { 64 | lines.join("\n").replace( 65 | '\n', 66 | &format!("\n{}", " ".repeat(user_name_width + 3)), 67 | ) 68 | } 69 | }, 70 | ) 71 | } else { 72 | fmt_content(&ing.content) 73 | }; 74 | writeln!(buf, " {} {}", ing.user_name.cyan(), content)?; 75 | 76 | let len = comment_list.len(); 77 | if len != 0 { 78 | let max_i = len - 1; 79 | let comment_list_buf: Result = comment_list.iter().enumerate().try_fold( 80 | String::new(), 81 | |mut buf, (i, entry)| try { 82 | { 83 | let buf = &mut buf; 84 | if i != max_i { 85 | write!(buf, " │ {}", entry.user_name.blue())?; 86 | } else { 87 | write!(buf, " └ {}", entry.user_name.blue())?; 88 | } 89 | let at_user = get_ing_at_user_tag_text(&entry.content); 90 | if at_user.is_empty().not() { 91 | write!(buf, " {}{}", "@".bright_black(), at_user.bright_black())?; 92 | } 93 | let content = { 94 | let content = rm_ing_at_user_tag(&entry.content); 95 | fmt_content(&content) 96 | }; 97 | writeln!(buf, " {}", content.dimmed())?; 98 | } 99 | buf 100 | }, 101 | ); 102 | write!(buf, "{}", comment_list_buf?)?; 103 | } 104 | 105 | writeln!(buf)?; 106 | }; 107 | buf 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /src/display/colorful/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fav; 2 | pub mod ing; 3 | pub mod news; 4 | pub mod post; 5 | pub mod user; 6 | 7 | use anyhow::Result; 8 | use colored::Colorize; 9 | use std::fmt::Display; 10 | 11 | #[inline] 12 | pub fn fmt_err(e: &anyhow::Error) -> String { 13 | format!("{}: {}", "Err".red(), e) 14 | } 15 | 16 | #[inline] 17 | pub fn fmt_result(result: &Result) -> String { 18 | match result { 19 | Ok(t) => format!("{}: {}", "Ok".green(), t), 20 | Err(e) => fmt_err(e), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/display/colorful/news.rs: -------------------------------------------------------------------------------- 1 | use crate::api::news::get_list::NewsEntry; 2 | use crate::args::TimeStyle; 3 | use crate::display::colorful::fmt_err; 4 | use crate::infra::result::WrapResult; 5 | use crate::infra::str::StrExt; 6 | use crate::infra::terminal::get_term_width; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use colored::Colorize; 10 | use std::fmt::Write; 11 | 12 | pub fn list_news( 13 | time_style: &TimeStyle, 14 | news_iter: Result>, 15 | ) -> Result { 16 | let news_iter = match news_iter { 17 | Ok(o) => o, 18 | Err(e) => return fmt_err(&e).wrap_ok(), 19 | }; 20 | 21 | news_iter 22 | .map(|news| try { 23 | let mut buf = String::new(); 24 | { 25 | let buf = &mut buf; 26 | let create_time = display_cnb_time(&news.create_time, time_style); 27 | let url = format!("https://news.cnblogs.com/n/{}", news.id); 28 | writeln!(buf, "{} {}", create_time.dimmed(), url.dimmed())?; 29 | writeln!(buf, " {}", news.title)?; 30 | 31 | let summary = { 32 | let summary = format!("{}...", news.summary); 33 | summary.width_split(get_term_width() - 4).map_or_else( 34 | || summary.clone(), 35 | |vec| { 36 | vec.into_iter() 37 | .map(|line| format!(" {}", line)) 38 | .collect::>() 39 | .join("\n") 40 | }, 41 | ) 42 | }; 43 | writeln!(buf, "{}", summary.dimmed())?; 44 | } 45 | buf 46 | }) 47 | .try_fold(String::new(), |mut acc, buf: Result| try { 48 | writeln!(&mut acc, "{}", buf?)?; 49 | acc 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/display/colorful/post.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::get_comment_list::PostCommentEntry; 2 | use crate::api::post::get_one::PostEntry; 3 | use crate::api::post::search_site::SearchResultEntry; 4 | use crate::args::TimeStyle; 5 | use crate::display::colorful::fmt_err; 6 | use crate::infra::result::WrapResult; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use colored::Colorize; 10 | use std::fmt::Write; 11 | 12 | pub fn list_post( 13 | result: Result<(impl ExactSizeIterator, usize)>, 14 | ) -> Result { 15 | let (mut entry_iter, total_count) = match result { 16 | Ok(o) => o, 17 | Err(e) => return fmt_err(&e).wrap_ok(), 18 | }; 19 | 20 | entry_iter.try_fold( 21 | format!("{}/{}\n", entry_iter.len(), total_count), 22 | |mut buf, entry| try { 23 | { 24 | let buf = &mut buf; 25 | write!(buf, "{} {}", "#".dimmed(), entry.id.to_string().dimmed())?; 26 | if entry.is_published { 27 | write!(buf, " {}", "Pub".green())?; 28 | } else { 29 | write!(buf, " {}", "Dft".yellow())?; 30 | } 31 | if entry.is_pinned { 32 | write!(buf, " {}", "Pin".magenta())?; 33 | } 34 | write!(buf, " {}", entry.title.cyan().bold())?; 35 | writeln!(buf)?; 36 | } 37 | buf 38 | }, 39 | ) 40 | } 41 | 42 | pub fn show_post(entry: &Result) -> Result { 43 | let entry = match entry { 44 | Ok(entry) => entry, 45 | Err(e) => return fmt_err(e).wrap_ok(), 46 | }; 47 | 48 | let mut buf = String::new(); 49 | { 50 | let buf = &mut buf; 51 | writeln!(buf, "{}\n", entry.title.cyan().bold())?; 52 | if let Some(body) = &entry.body { 53 | writeln!(buf, "{}", body)?; 54 | } 55 | } 56 | buf.wrap_ok() 57 | } 58 | 59 | pub fn show_post_meta(time_style: &TimeStyle, entry: &Result) -> Result { 60 | let entry = match entry { 61 | Ok(entry) => entry, 62 | Err(e) => return fmt_err(e).wrap_ok(), 63 | }; 64 | 65 | let mut buf = String::new(); 66 | { 67 | let buf = &mut buf; 68 | writeln!(buf, "Title {}", entry.title.cyan().bold())?; 69 | { 70 | write!(buf, "Status")?; 71 | if entry.is_published { 72 | write!(buf, " {}", "Published".green())?; 73 | } else { 74 | write!(buf, " {}", "Draft".yellow())?; 75 | } 76 | if entry.is_pinned { 77 | write!(buf, " {}", "Pinned".magenta())?; 78 | } 79 | writeln!(buf)?; 80 | }; 81 | if let Some(body) = &entry.body { 82 | let words_count = words_count::count(body).words; 83 | writeln!(buf, "Words {}", words_count)?; 84 | } 85 | if let Some(tags) = &entry.tags { 86 | if let Some(tags_text) = tags 87 | .clone() 88 | .into_iter() 89 | .reduce(|acc, tag| format!("{}, {}", acc, tag)) 90 | { 91 | writeln!(buf, "Tags {}", tags_text)?; 92 | } 93 | } 94 | let create_time = display_cnb_time(&entry.create_time, time_style); 95 | writeln!(buf, "Create {}", create_time)?; 96 | let modify_time = display_cnb_time(&entry.create_time, time_style); 97 | writeln!(buf, "Modify {}", modify_time)?; 98 | writeln!(buf, "Link https:{}", entry.url)?; 99 | } 100 | buf.wrap_ok() 101 | } 102 | 103 | pub fn show_post_comment( 104 | time_style: &TimeStyle, 105 | comment_iter: Result>, 106 | ) -> Result { 107 | let mut comment_iter = match comment_iter { 108 | Ok(entry) => entry, 109 | Err(e) => return fmt_err(&e).wrap_ok(), 110 | }; 111 | 112 | comment_iter.try_fold(String::new(), |mut buf, comment| try { 113 | { 114 | let buf = &mut buf; 115 | let create_time = display_cnb_time(&comment.create_time, time_style); 116 | let floor_text = format!("{}F", comment.floor); 117 | writeln!(buf, "{} {}", create_time.dimmed(), floor_text.dimmed())?; 118 | writeln!(buf, " {} {}", comment.user_name.cyan(), comment.content)?; 119 | } 120 | buf 121 | }) 122 | } 123 | 124 | pub fn search_self_post( 125 | result: Result<(impl ExactSizeIterator, usize)>, 126 | ) -> Result { 127 | let (mut id_iter, total_count) = match result { 128 | Ok(o) => o, 129 | Err(e) => return fmt_err(&e).wrap_ok(), 130 | }; 131 | 132 | id_iter.try_fold( 133 | format!("{}/{}\n", id_iter.len(), total_count), 134 | |mut buf, id| try { 135 | writeln!(&mut buf, "# {}", id)?; 136 | buf 137 | }, 138 | ) 139 | } 140 | 141 | pub fn search_site_post( 142 | time_style: &TimeStyle, 143 | entry_iter: Result>, 144 | ) -> Result { 145 | let entry_iter = match entry_iter { 146 | Ok(o) => o, 147 | Err(e) => return fmt_err(&e).wrap_ok(), 148 | }; 149 | 150 | entry_iter 151 | .map(|entry| try { 152 | let mut buf = String::new(); 153 | { 154 | let buf = &mut buf; 155 | let create_time = display_cnb_time(&entry.create_time, time_style); 156 | writeln!(buf, "{} {}", create_time.dimmed(), entry.url.dimmed())?; 157 | writeln!(buf, " {}", entry.title)?; 158 | let view_vote_comment_count = format!( 159 | "View {} Vote {} Comment {}", 160 | entry.view_count, entry.vote_count, entry.comment_count 161 | ); 162 | writeln!(buf, " {}", view_vote_comment_count.dimmed())?; 163 | } 164 | buf 165 | }) 166 | .try_fold(String::new(), |mut acc, buf: Result| try { 167 | writeln!(&mut acc, "{}", buf?)?; 168 | acc 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /src/display/colorful/user.rs: -------------------------------------------------------------------------------- 1 | use crate::api::user::info::UserInfo; 2 | use crate::display::colorful::fmt_err; 3 | use crate::infra::result::WrapResult; 4 | use anyhow::Result; 5 | use colored::Colorize; 6 | use std::fmt::Write; 7 | use std::path::PathBuf; 8 | 9 | pub fn login(cfg_path: &Result) -> String { 10 | match cfg_path { 11 | Ok(pb) => format!("PAT was saved in {:?}", pb), 12 | Err(e) => fmt_err(e), 13 | } 14 | } 15 | 16 | pub fn logout(cfg_path: &Result) -> String { 17 | match cfg_path { 18 | Ok(pb) => format!("{:?} was successfully removed", pb), 19 | Err(e) => fmt_err(e), 20 | } 21 | } 22 | 23 | pub fn user_info(info: &Result) -> Result { 24 | let info = match info { 25 | Ok(info) => info, 26 | Err(e) => return fmt_err(e).wrap_ok(), 27 | }; 28 | 29 | let mut buf = String::new(); 30 | { 31 | let buf = &mut buf; 32 | write!(buf, "{}", info.display_name.cyan())?; 33 | if info.is_vip { 34 | write!(buf, " {}", " VIP ".on_blue())?; 35 | } 36 | writeln!(buf)?; 37 | writeln!( 38 | buf, 39 | "{} Following {} Followers", 40 | info.following_count, info.followers_count 41 | )?; 42 | writeln!(buf, "ID {}", info.blog_id)?; 43 | writeln!(buf, "Joined {}", info.joined)?; 44 | writeln!(buf, "Blog https://www.cnblogs.com/{}", info.blog_app)?; 45 | } 46 | buf.wrap_ok() 47 | } 48 | -------------------------------------------------------------------------------- /src/display/json/fav.rs: -------------------------------------------------------------------------------- 1 | use crate::api::fav::get_list::FavEntry; 2 | use crate::display::json::{fmt_err, fmt_ok}; 3 | use anyhow::Result; 4 | 5 | pub fn list_fav(fav_iter: Result>) -> String { 6 | let fav_iter = match fav_iter { 7 | Ok(o) => o, 8 | Err(e) => return fmt_err(&e), 9 | }; 10 | 11 | let vec = fav_iter.collect::>(); 12 | 13 | fmt_ok(vec) 14 | } 15 | -------------------------------------------------------------------------------- /src/display/json/ing.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::get_comment_list::IngCommentEntry; 2 | use crate::api::ing::get_list::IngEntry; 3 | use crate::display::json::{fmt_err, fmt_ok}; 4 | use anyhow::Result; 5 | use serde_json::json; 6 | 7 | pub fn list_ing( 8 | ing_with_comment_list: Result)>>, 9 | ) -> String { 10 | let ing_with_comment_list = match ing_with_comment_list { 11 | Ok(o) => o, 12 | Err(e) => return fmt_err(&e), 13 | }; 14 | 15 | let json_vec = ing_with_comment_list 16 | .map(|(entry, comment_list)| { 17 | json!({ 18 | "entry": entry, 19 | "comment_list": comment_list 20 | }) 21 | }) 22 | .collect::>(); 23 | 24 | fmt_ok(json_vec) 25 | } 26 | -------------------------------------------------------------------------------- /src/display/json/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fav; 2 | pub mod ing; 3 | pub mod news; 4 | pub mod post; 5 | pub mod user; 6 | 7 | use anyhow::Result; 8 | use serde::Serialize; 9 | use serde_json::json; 10 | use std::fmt::Display; 11 | 12 | #[inline] 13 | pub fn fmt_ok(t: impl Serialize) -> String { 14 | let json = json!({ 15 | "is_ok": true, 16 | "msg": t 17 | }); 18 | json.to_string() 19 | } 20 | 21 | #[inline] 22 | pub fn fmt_err(e: impl ToString) -> String { 23 | let json = json!({ 24 | "is_ok": false, 25 | "msg": e.to_string() 26 | }); 27 | json.to_string() 28 | } 29 | 30 | pub fn fmt_result(result: &Result) -> String { 31 | match result { 32 | Ok(t) => fmt_ok(t), 33 | Err(e) => fmt_err(e), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/display/json/news.rs: -------------------------------------------------------------------------------- 1 | use crate::api::news::get_list::NewsEntry; 2 | use crate::display::json::{fmt_err, fmt_ok}; 3 | use anyhow::Result; 4 | 5 | pub fn list_news(news_iter: Result>) -> String { 6 | let news_iter = match news_iter { 7 | Ok(o) => o, 8 | Err(e) => return fmt_err(&e), 9 | }; 10 | 11 | let vec = news_iter.collect::>(); 12 | 13 | fmt_ok(vec) 14 | } 15 | -------------------------------------------------------------------------------- /src/display/json/post.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::get_comment_list::PostCommentEntry; 2 | use crate::api::post::get_one::PostEntry; 3 | use crate::api::post::search_site::SearchResultEntry; 4 | use crate::display::json::{fmt_err, fmt_ok, fmt_result}; 5 | use anyhow::Result; 6 | use serde_json::json; 7 | 8 | pub fn list_post(result: Result<(impl ExactSizeIterator, usize)>) -> String { 9 | let (entry_iter, total_count) = match result { 10 | Ok(o) => o, 11 | Err(e) => return fmt_err(&e), 12 | }; 13 | 14 | let vec = entry_iter.collect::>(); 15 | let json = json!({ 16 | "listed_count": vec.len(), 17 | "total_count": total_count, 18 | "entry_list": vec, 19 | }); 20 | fmt_ok(json) 21 | } 22 | 23 | pub fn show_post(entry: &Result) -> String { 24 | let json = entry.as_ref().map(|entry| { 25 | json!({ 26 | "title": entry.title, 27 | "body": entry.body 28 | }) 29 | }); 30 | fmt_result(&json) 31 | } 32 | 33 | pub fn show_post_meta(entry: &Result) -> String { 34 | fmt_result(entry) 35 | } 36 | 37 | pub fn show_post_comment( 38 | comment_iter: Result>, 39 | ) -> String { 40 | let comment_iter = match comment_iter { 41 | Ok(entry) => entry, 42 | Err(e) => return fmt_err(&e), 43 | }; 44 | 45 | let comment_vec = comment_iter.collect::>(); 46 | fmt_ok(comment_vec) 47 | } 48 | 49 | pub fn search_self_post(result: Result<(impl ExactSizeIterator, usize)>) -> String { 50 | let (id_iter, total_count) = match result { 51 | Ok(o) => o, 52 | Err(e) => return fmt_err(&e), 53 | }; 54 | 55 | let id_list = id_iter.collect::>(); 56 | let json = json!({ 57 | "listed_count": id_list.len(), 58 | "total_count": total_count, 59 | "id_list": id_list, 60 | }); 61 | fmt_ok(json) 62 | } 63 | 64 | pub fn search_site_post( 65 | entry_iter: Result>, 66 | ) -> String { 67 | let entry_iter = match entry_iter { 68 | Ok(o) => o, 69 | Err(e) => return fmt_err(&e), 70 | }; 71 | 72 | let entry_vec = entry_iter.collect::>(); 73 | fmt_ok(entry_vec) 74 | } 75 | -------------------------------------------------------------------------------- /src/display/json/user.rs: -------------------------------------------------------------------------------- 1 | use crate::api::user::info::UserInfo; 2 | use crate::display::json::fmt_result; 3 | use anyhow::Result; 4 | use serde_json::json; 5 | use std::path::PathBuf; 6 | 7 | pub fn login(cfg_path: &Result) -> String { 8 | let json = cfg_path.as_ref().map(|pb| json!({"cfg_path":pb})); 9 | fmt_result(&json) 10 | } 11 | 12 | pub fn logout(cfg_path: &Result) -> String { 13 | let json = cfg_path.as_ref().map(|pb| json!({"cfg_path":pb})); 14 | fmt_result(&json) 15 | } 16 | 17 | pub fn user_info(info: &Result) -> String { 18 | fmt_result(info) 19 | } 20 | -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::api::fav::get_list::FavEntry; 2 | use crate::api::ing::get_comment_list::IngCommentEntry; 3 | use crate::api::ing::get_list::IngEntry; 4 | use crate::api::news::get_list::NewsEntry; 5 | use crate::api::post::get_comment_list::PostCommentEntry; 6 | use crate::api::post::get_one::PostEntry; 7 | use crate::api::post::search_site::SearchResultEntry; 8 | use crate::api::user::info::UserInfo; 9 | use crate::args::{Style, TimeStyle}; 10 | use crate::infra::result::WrapResult; 11 | use anyhow::Result; 12 | use std::path::PathBuf; 13 | 14 | mod colorful; 15 | mod json; 16 | mod normal; 17 | 18 | pub fn login(style: &Style, cfg_path: &Result) -> String { 19 | match style { 20 | Style::Colorful => colorful::user::login(cfg_path), 21 | Style::Normal => normal::user::login(cfg_path), 22 | Style::Json => json::user::login(cfg_path), 23 | } 24 | } 25 | 26 | pub fn logout(style: &Style, cfg_path: &Result) -> String { 27 | match style { 28 | Style::Colorful => colorful::user::logout(cfg_path), 29 | Style::Normal => normal::user::logout(cfg_path), 30 | Style::Json => json::user::logout(cfg_path), 31 | } 32 | } 33 | 34 | pub fn user_info(style: &Style, user_info: &Result) -> Result { 35 | match style { 36 | Style::Colorful => colorful::user::user_info(user_info), 37 | Style::Normal => normal::user::user_info(user_info), 38 | Style::Json => json::user::user_info(user_info).wrap_ok(), 39 | } 40 | } 41 | 42 | pub fn list_ing( 43 | style: &Style, 44 | time_style: &TimeStyle, 45 | ing_with_comment_iter: Result)>>, 46 | align: bool, 47 | ) -> Result { 48 | match style { 49 | Style::Colorful => colorful::ing::list_ing(time_style, ing_with_comment_iter, align), 50 | Style::Normal => normal::ing::list_ing(time_style, ing_with_comment_iter, align), 51 | Style::Json => json::ing::list_ing(ing_with_comment_iter).wrap_ok(), 52 | } 53 | } 54 | 55 | pub fn publish_ing(style: &Style, result: &Result<&String>) -> String { 56 | match style { 57 | Style::Colorful => colorful::fmt_result(result), 58 | Style::Normal => normal::fmt_result(result), 59 | Style::Json => json::fmt_result(result), 60 | } 61 | } 62 | 63 | pub fn comment_ing(style: &Style, result: &Result<&String>) -> String { 64 | match style { 65 | Style::Colorful => colorful::fmt_result(result), 66 | Style::Normal => normal::fmt_result(result), 67 | Style::Json => json::fmt_result(result), 68 | } 69 | } 70 | 71 | pub fn show_post(style: &Style, entry: &Result) -> Result { 72 | match style { 73 | Style::Colorful => colorful::post::show_post(entry), 74 | Style::Normal => normal::post::show_post(entry), 75 | Style::Json => json::post::show_post(entry).wrap_ok(), 76 | } 77 | } 78 | 79 | pub fn list_post( 80 | style: &Style, 81 | result: Result<(impl ExactSizeIterator, usize)>, 82 | ) -> Result { 83 | match style { 84 | Style::Colorful => colorful::post::list_post(result), 85 | Style::Normal => normal::post::list_post(result), 86 | Style::Json => json::post::list_post(result).wrap_ok(), 87 | } 88 | } 89 | 90 | pub fn show_post_meta( 91 | style: &Style, 92 | time_style: &TimeStyle, 93 | entry: &Result, 94 | ) -> Result { 95 | match style { 96 | Style::Colorful => colorful::post::show_post_meta(time_style, entry), 97 | Style::Normal => normal::post::show_post_meta(time_style, entry), 98 | Style::Json => json::post::show_post_meta(entry).wrap_ok(), 99 | } 100 | } 101 | 102 | pub fn show_post_comment( 103 | style: &Style, 104 | time_style: &TimeStyle, 105 | comment_iter: Result>, 106 | ) -> Result { 107 | match style { 108 | Style::Colorful => colorful::post::show_post_comment(time_style, comment_iter), 109 | Style::Normal => normal::post::show_post_comment(time_style, comment_iter), 110 | Style::Json => json::post::show_post_comment(comment_iter).wrap_ok(), 111 | } 112 | } 113 | 114 | pub fn delete_post(style: &Style, result: &Result) -> String { 115 | match style { 116 | Style::Colorful => colorful::fmt_result(result), 117 | Style::Normal => normal::fmt_result(result), 118 | Style::Json => json::fmt_result(result), 119 | } 120 | } 121 | 122 | pub fn search_self_post( 123 | style: &Style, 124 | result: Result<(impl ExactSizeIterator, usize)>, 125 | ) -> Result { 126 | match style { 127 | Style::Colorful => colorful::post::search_self_post(result), 128 | Style::Normal => normal::post::search_self_post(result), 129 | Style::Json => json::post::search_self_post(result).wrap_ok(), 130 | } 131 | } 132 | 133 | pub fn search_site_post( 134 | style: &Style, 135 | time_style: &TimeStyle, 136 | entry_iter: Result>, 137 | ) -> Result { 138 | match style { 139 | Style::Colorful => colorful::post::search_site_post(time_style, entry_iter), 140 | Style::Normal => normal::post::search_site_post(time_style, entry_iter), 141 | Style::Json => json::post::search_site_post(entry_iter).wrap_ok(), 142 | } 143 | } 144 | 145 | pub fn create_post(style: &Style, result: &Result) -> String { 146 | match style { 147 | Style::Colorful => colorful::fmt_result(result), 148 | Style::Normal => normal::fmt_result(result), 149 | Style::Json => json::fmt_result(result), 150 | } 151 | } 152 | 153 | pub fn update_post(style: &Style, result: &Result) -> String { 154 | match style { 155 | Style::Colorful => colorful::fmt_result(result), 156 | Style::Normal => normal::fmt_result(result), 157 | Style::Json => json::fmt_result(result), 158 | } 159 | } 160 | 161 | pub fn list_news( 162 | style: &Style, 163 | time_style: &TimeStyle, 164 | news_iter: Result>, 165 | ) -> Result { 166 | match style { 167 | Style::Colorful => colorful::news::list_news(time_style, news_iter), 168 | Style::Normal => normal::news::list_news(time_style, news_iter), 169 | Style::Json => json::news::list_news(news_iter).wrap_ok(), 170 | } 171 | } 172 | 173 | pub fn list_fav( 174 | style: &Style, 175 | time_style: &TimeStyle, 176 | fav_iter: Result>, 177 | ) -> Result { 178 | match style { 179 | Style::Colorful => colorful::fav::list_fav(time_style, fav_iter), 180 | Style::Normal => normal::fav::list_fav(time_style, fav_iter), 181 | Style::Json => json::fav::list_fav(fav_iter).wrap_ok(), 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/display/normal/fav.rs: -------------------------------------------------------------------------------- 1 | use crate::api::fav::get_list::FavEntry; 2 | use crate::args::TimeStyle; 3 | use crate::display::normal::fmt_err; 4 | use crate::infra::result::WrapResult; 5 | use crate::infra::str::StrExt; 6 | use crate::infra::terminal::get_term_width; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use std::fmt::Write; 10 | use std::ops::Not; 11 | 12 | pub fn list_fav( 13 | time_style: &TimeStyle, 14 | fav_iter: Result>, 15 | ) -> Result { 16 | let fav_iter = match fav_iter { 17 | Ok(o) => o, 18 | Err(e) => return fmt_err(&e).wrap_ok(), 19 | }; 20 | 21 | fav_iter 22 | .map(|fav| try { 23 | let mut buf = String::new(); 24 | { 25 | let buf = &mut buf; 26 | let create_time = display_cnb_time(&fav.create_time, time_style); 27 | writeln!(buf, "{} {}", create_time, fav.url)?; 28 | writeln!(buf, " {}", fav.title)?; 29 | 30 | let summary = { 31 | fav.summary.width_split(get_term_width() - 4).map_or_else( 32 | || fav.summary.clone(), 33 | |vec| { 34 | vec.into_iter() 35 | .map(|line| format!(" {}", line)) 36 | .collect::>() 37 | .join("\n") 38 | }, 39 | ) 40 | }; 41 | if summary.is_empty().not() { 42 | writeln!(buf, "{}", summary)?; 43 | } 44 | } 45 | buf 46 | }) 47 | .try_fold(String::new(), |mut acc, buf: Result| try { 48 | writeln!(&mut acc, "{}", buf?)?; 49 | acc 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/display/normal/ing.rs: -------------------------------------------------------------------------------- 1 | use crate::api::ing::get_comment_list::IngCommentEntry; 2 | use crate::api::ing::get_list::IngEntry; 3 | use crate::api::ing::{ 4 | fmt_content, get_ing_at_user_tag_text, ing_star_tag_to_text, rm_ing_at_user_tag, IngSendFrom, 5 | }; 6 | use crate::args::TimeStyle; 7 | use crate::display::normal::fmt_err; 8 | use crate::infra::result::WrapResult; 9 | use crate::infra::str::StrExt; 10 | use crate::infra::terminal::get_term_width; 11 | use crate::infra::time::display_cnb_time; 12 | use anyhow::Result; 13 | use std::fmt::Write; 14 | use std::ops::Not; 15 | use unicode_width::UnicodeWidthStr; 16 | 17 | // TODO: rm unnecessary line divider 18 | pub fn list_ing( 19 | time_style: &TimeStyle, 20 | ing_with_comment_list: Result)>>, 21 | align: bool, 22 | ) -> Result { 23 | let mut ing_with_comment_list = match ing_with_comment_list { 24 | Ok(o) => o, 25 | Err(e) => return fmt_err(&e).wrap_ok(), 26 | }; 27 | 28 | ing_with_comment_list.try_fold(String::new(), |mut buf, (ing, comment_list)| try { 29 | { 30 | let buf = &mut buf; 31 | let create_time = display_cnb_time(&ing.create_time, time_style); 32 | write!(buf, "{}", create_time)?; 33 | 34 | let send_from_mark = match ing.send_from { 35 | IngSendFrom::Cli => Some("CLI"), 36 | IngSendFrom::CellPhone => Some("Mobile"), 37 | IngSendFrom::VsCode => Some("VSCode"), 38 | IngSendFrom::Web => Some("Web"), 39 | _ => None, 40 | }; 41 | if let Some(mark) = send_from_mark { 42 | write!(buf, " {}", mark)?; 43 | } 44 | if ing.is_lucky { 45 | let star_text = ing_star_tag_to_text(&ing.icons); 46 | write!(buf, " {}★", star_text)?; 47 | } 48 | writeln!(buf, " # {}", ing.id)?; 49 | let content = if align { 50 | let user_name_width = ing.user_name.width_cjk(); 51 | let left_width = get_term_width().saturating_sub(user_name_width + 3); 52 | fmt_content(&ing.content) 53 | .width_split(left_width) 54 | .map_or_else( 55 | || ing.content.clone(), 56 | |lines| { 57 | if comment_list.is_empty().not() { 58 | lines.join("\n").replace( 59 | '\n', 60 | &format!("\n │{}", " ".repeat(user_name_width - 2)), 61 | ) 62 | } else { 63 | lines.join("\n").replace( 64 | '\n', 65 | &format!("\n{}", " ".repeat(user_name_width + 3)), 66 | ) 67 | } 68 | }, 69 | ) 70 | } else { 71 | fmt_content(&ing.content) 72 | }; 73 | writeln!(buf, " {}: {}", ing.user_name, content)?; 74 | 75 | let len = comment_list.len(); 76 | if len != 0 { 77 | let max_i = len - 1; 78 | let comment_list_buf: Result = comment_list.iter().enumerate().try_fold( 79 | String::new(), 80 | |mut buf, (i, entry)| try { 81 | { 82 | let buf = &mut buf; 83 | if i != max_i { 84 | write!(buf, " │ {}", entry.user_name)?; 85 | } else { 86 | write!(buf, " └ {}", entry.user_name)?; 87 | } 88 | let at_user = get_ing_at_user_tag_text(&entry.content); 89 | if at_user.is_empty().not() { 90 | write!(buf, " @{}", at_user)?; 91 | } 92 | let content = { 93 | let content = rm_ing_at_user_tag(&entry.content); 94 | fmt_content(&content) 95 | }; 96 | writeln!(buf, ": {}", content)?; 97 | } 98 | buf 99 | }, 100 | ); 101 | write!(buf, "{}", comment_list_buf?)?; 102 | } 103 | 104 | writeln!(buf)?; 105 | }; 106 | buf 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /src/display/normal/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fav; 2 | pub mod ing; 3 | pub mod news; 4 | pub mod post; 5 | pub mod user; 6 | 7 | use anyhow::Result; 8 | use std::fmt::Display; 9 | 10 | #[inline] 11 | pub fn fmt_err(e: &anyhow::Error) -> String { 12 | format!("Err: {}", e) 13 | } 14 | 15 | #[inline] 16 | pub fn fmt_result(result: &Result) -> String { 17 | match result { 18 | Ok(t) => format!("Ok: {}", t), 19 | Err(e) => fmt_err(e), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/display/normal/news.rs: -------------------------------------------------------------------------------- 1 | use crate::api::news::get_list::NewsEntry; 2 | use crate::args::TimeStyle; 3 | use crate::display::normal::fmt_err; 4 | use crate::infra::result::WrapResult; 5 | use crate::infra::str::StrExt; 6 | use crate::infra::terminal::get_term_width; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use std::fmt::Write; 10 | 11 | pub fn list_news( 12 | time_style: &TimeStyle, 13 | news_iter: Result>, 14 | ) -> Result { 15 | let news_iter = match news_iter { 16 | Ok(o) => o, 17 | Err(e) => return fmt_err(&e).wrap_ok(), 18 | }; 19 | 20 | news_iter 21 | .map(|news| try { 22 | let mut buf = String::new(); 23 | { 24 | let buf = &mut buf; 25 | let create_time = display_cnb_time(&news.create_time, time_style); 26 | let url = format!("https://news.cnblogs.com/n/{}", news.id); 27 | writeln!(buf, "{} {}", create_time, url)?; 28 | writeln!(buf, " {}", news.title)?; 29 | 30 | let summary = { 31 | let summary = format!("{}...", news.summary); 32 | summary.width_split(get_term_width() - 4).map_or_else( 33 | || summary.clone(), 34 | |vec| { 35 | vec.into_iter() 36 | .map(|line| format!(" {}", line)) 37 | .collect::>() 38 | .join("\n") 39 | }, 40 | ) 41 | }; 42 | writeln!(buf, "{}", summary)?; 43 | } 44 | buf 45 | }) 46 | .try_fold(String::new(), |mut acc, buf: Result| try { 47 | writeln!(&mut acc, "{}", buf?)?; 48 | acc 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/display/normal/post.rs: -------------------------------------------------------------------------------- 1 | use crate::api::post::get_comment_list::PostCommentEntry; 2 | use crate::api::post::get_one::PostEntry; 3 | use crate::api::post::search_site::SearchResultEntry; 4 | use crate::args::TimeStyle; 5 | use crate::display::normal::fmt_err; 6 | use crate::infra::result::WrapResult; 7 | use crate::infra::time::display_cnb_time; 8 | use anyhow::Result; 9 | use std::fmt::Write; 10 | 11 | pub fn list_post( 12 | result: Result<(impl ExactSizeIterator, usize)>, 13 | ) -> Result { 14 | let (mut entry_iter, total_count) = match result { 15 | Ok(o) => o, 16 | Err(e) => return fmt_err(&e).wrap_ok(), 17 | }; 18 | 19 | entry_iter.try_fold( 20 | format!("{}/{}\n", entry_iter.len(), total_count), 21 | |mut buf, entry| try { 22 | { 23 | let buf = &mut buf; 24 | write!(buf, "# {}", entry.id)?; 25 | if entry.is_published { 26 | write!(buf, " Pub")?; 27 | } else { 28 | write!(buf, " Dft")?; 29 | } 30 | if entry.is_pinned { 31 | write!(buf, " Pin")?; 32 | } 33 | write!(buf, " {}", entry.title)?; 34 | writeln!(buf)?; 35 | } 36 | buf 37 | }, 38 | ) 39 | } 40 | 41 | pub fn show_post(entry: &Result) -> Result { 42 | let entry = match entry { 43 | Ok(entry) => entry, 44 | Err(e) => return fmt_err(e).wrap_ok(), 45 | }; 46 | 47 | let mut buf = String::new(); 48 | { 49 | let buf = &mut buf; 50 | writeln!(buf, "{}\n", entry.title)?; 51 | if let Some(body) = &entry.body { 52 | writeln!(buf, "{}", body)?; 53 | } 54 | } 55 | buf.wrap_ok() 56 | } 57 | 58 | pub fn show_post_meta(time_style: &TimeStyle, entry: &Result) -> Result { 59 | let entry = match entry { 60 | Ok(entry) => entry, 61 | Err(e) => return fmt_err(e).wrap_ok(), 62 | }; 63 | 64 | let mut buf = String::new(); 65 | { 66 | let buf = &mut buf; 67 | writeln!(buf, "Title {}", entry.title)?; 68 | { 69 | write!(buf, "Status")?; 70 | if entry.is_published { 71 | write!(buf, " Published")?; 72 | } else { 73 | write!(buf, " Draft")?; 74 | } 75 | if entry.is_pinned { 76 | write!(buf, " Pinned")?; 77 | } 78 | writeln!(buf)?; 79 | }; 80 | if let Some(body) = &entry.body { 81 | let words_count = words_count::count(body).words; 82 | writeln!(buf, "Words {}", words_count)?; 83 | } 84 | if let Some(tags) = &entry.tags { 85 | if let Some(tags_text) = tags 86 | .clone() 87 | .into_iter() 88 | .reduce(|acc, tag| format!("{}, {}", acc, tag)) 89 | { 90 | writeln!(buf, "Tags {}", tags_text)?; 91 | } 92 | } 93 | let create_time = display_cnb_time(&entry.create_time, time_style); 94 | writeln!(buf, "Create {}", create_time)?; 95 | let modify_time = display_cnb_time(&entry.create_time, time_style); 96 | writeln!(buf, "Modify {}", modify_time)?; 97 | writeln!(buf, "Link https:{}", entry.url)?; 98 | } 99 | buf.wrap_ok() 100 | } 101 | 102 | pub fn show_post_comment( 103 | time_style: &TimeStyle, 104 | comment_iter: Result>, 105 | ) -> Result { 106 | let mut comment_iter = match comment_iter { 107 | Ok(entry) => entry, 108 | Err(e) => return fmt_err(&e).wrap_ok(), 109 | }; 110 | 111 | comment_iter.try_fold(String::new(), |mut buf, comment| try { 112 | { 113 | let buf = &mut buf; 114 | let create_time = display_cnb_time(&comment.create_time, time_style); 115 | writeln!(buf, "{} {}F", create_time, comment.floor)?; 116 | writeln!(buf, " {} {}", comment.user_name, comment.content)?; 117 | } 118 | buf 119 | }) 120 | } 121 | 122 | pub fn search_self_post( 123 | result: Result<(impl ExactSizeIterator, usize)>, 124 | ) -> Result { 125 | let (mut id_iter, total_count) = match result { 126 | Ok(o) => o, 127 | Err(e) => return fmt_err(&e).wrap_ok(), 128 | }; 129 | 130 | id_iter.try_fold( 131 | format!("{}/{}\n", id_iter.len(), total_count), 132 | |mut buf, id| try { 133 | writeln!(&mut buf, "# {}", id)?; 134 | buf 135 | }, 136 | ) 137 | } 138 | 139 | pub fn search_site_post( 140 | time_style: &TimeStyle, 141 | entry_iter: Result>, 142 | ) -> Result { 143 | let entry_iter = match entry_iter { 144 | Ok(o) => o, 145 | Err(e) => return fmt_err(&e).wrap_ok(), 146 | }; 147 | 148 | entry_iter 149 | .map(|entry| try { 150 | let mut buf = String::new(); 151 | { 152 | let buf = &mut buf; 153 | let create_time = display_cnb_time(&entry.create_time, time_style); 154 | writeln!(buf, "{} {}", create_time, entry.url)?; 155 | writeln!(buf, " {}", entry.title)?; 156 | let view_vote_comment_count = format!( 157 | "View {} Vote {} Comment {}", 158 | entry.view_count, entry.vote_count, entry.comment_count 159 | ); 160 | writeln!(buf, " {}", view_vote_comment_count)?; 161 | } 162 | buf 163 | }) 164 | .try_fold(String::new(), |mut acc, buf: Result| try { 165 | writeln!(&mut acc, "{}", buf?)?; 166 | acc 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /src/display/normal/user.rs: -------------------------------------------------------------------------------- 1 | use crate::api::user::info::UserInfo; 2 | use crate::display::normal::fmt_err; 3 | use crate::infra::result::WrapResult; 4 | use anyhow::Result; 5 | use std::fmt::Write; 6 | use std::path::PathBuf; 7 | 8 | pub fn login(cfg_path: &Result) -> String { 9 | match cfg_path { 10 | Ok(pb) => format!("PAT was saved in {:?}", pb), 11 | Err(e) => fmt_err(e), 12 | } 13 | } 14 | 15 | pub fn logout(cfg_path: &Result) -> String { 16 | match cfg_path { 17 | Ok(pb) => format!("{:?} was successfully removed", pb), 18 | Err(e) => fmt_err(e), 19 | } 20 | } 21 | 22 | pub fn user_info(info: &Result) -> Result { 23 | let info = match info { 24 | Ok(info) => info, 25 | Err(e) => return fmt_err(e).wrap_ok(), 26 | }; 27 | 28 | let mut buf = String::new(); 29 | { 30 | let buf = &mut buf; 31 | write!(buf, "{}", info.display_name)?; 32 | if info.is_vip { 33 | write!(buf, " VIP")?; 34 | } 35 | writeln!(buf)?; 36 | writeln!( 37 | buf, 38 | "{} Following {} Followers", 39 | info.following_count, info.followers_count 40 | )?; 41 | writeln!(buf, "ID {}", info.blog_id)?; 42 | writeln!(buf, "Joined {}", info.joined)?; 43 | writeln!(buf, "Blog https://www.cnblogs.com/{}", info.blog_app)?; 44 | } 45 | buf.wrap_ok() 46 | } 47 | -------------------------------------------------------------------------------- /src/infra/fp.rs: -------------------------------------------------------------------------------- 1 | pub mod currying { 2 | #[inline] 3 | pub fn id(x: X) -> impl Fn(X) -> X 4 | where 5 | X: Clone, 6 | { 7 | move |_| x.clone() 8 | } 9 | 10 | #[inline] 11 | pub fn eq(a: T) -> impl Fn(T) -> bool 12 | where 13 | T: PartialEq, 14 | { 15 | move |b| a == b 16 | } 17 | 18 | #[inline] 19 | pub fn lt(a: T) -> impl Fn(T) -> bool 20 | where 21 | T: PartialOrd, 22 | { 23 | move |b| a < b 24 | } 25 | 26 | #[inline] 27 | pub fn gt(a: T) -> impl Fn(T) -> bool 28 | where 29 | T: PartialOrd, 30 | { 31 | move |b| a > b 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infra/http.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::result::WrapResult; 2 | use anyhow::bail; 3 | use anyhow::Result; 4 | use reqwest::header::AUTHORIZATION; 5 | use reqwest::{RequestBuilder, Response}; 6 | use std::ops::Not; 7 | 8 | pub const AUTHORIZATION_TYPE: &str = "Authorization-Type"; 9 | pub const PAT: &str = "pat"; 10 | 11 | #[macro_export] 12 | macro_rules! bearer { 13 | ($token:expr) => {{ 14 | format!("Bearer {}", $token) 15 | }}; 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! basic { 20 | ($token:expr) => {{ 21 | format!("Basic {}", $token) 22 | }}; 23 | } 24 | 25 | pub trait RequestBuilderExt { 26 | fn pat_auth(self, pat: &str) -> RequestBuilder; 27 | } 28 | 29 | impl RequestBuilderExt for RequestBuilder { 30 | fn pat_auth(self, pat: &str) -> RequestBuilder { 31 | let builder = self.header(AUTHORIZATION, bearer!(pat)); 32 | builder.header(AUTHORIZATION_TYPE, PAT) 33 | } 34 | } 35 | 36 | pub trait VecExt { 37 | fn into_query_string(self) -> String; 38 | } 39 | 40 | impl VecExt for Vec<(K, V)> { 41 | fn into_query_string(self) -> String { 42 | self.into_iter() 43 | .map(|(k, v)| { 44 | let s_k = k.to_string(); 45 | let s_v = v.to_string(); 46 | format!("{}={}", s_k, s_v) 47 | }) 48 | .fold(String::new(), |acc, q| format!("{acc}&{q}")) 49 | } 50 | } 51 | 52 | pub async fn unit_or_err(resp: Response) -> Result<()> { 53 | let code = resp.status(); 54 | let body = resp.text().await?; 55 | 56 | if code.is_success().not() { 57 | bail!("{}: {}", code, body); 58 | } 59 | 60 | ().wrap_ok() 61 | } 62 | 63 | pub async fn body_or_err(resp: Response) -> Result { 64 | let code = resp.status(); 65 | let body = resp.text().await?; 66 | 67 | if code.is_success() { 68 | body.wrap_ok() 69 | } else { 70 | bail!("{}: {}", code, body) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/infra/infer.rs: -------------------------------------------------------------------------------- 1 | /// Use this to infer type for val, as same as type ascription 2 | pub const fn infer(val: T) -> T { 3 | val 4 | } 5 | -------------------------------------------------------------------------------- /src/infra/iter.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{join_all, JoinAll}; 2 | use std::future::Future; 3 | 4 | pub trait IteratorExt: Iterator { 5 | #[inline] 6 | fn dyn_rev<'t>(self, rev: bool) -> Box + 't> 7 | where 8 | Self: DoubleEndedIterator + Sized + 't, 9 | { 10 | if rev { 11 | Box::new(self.rev()) 12 | } else { 13 | Box::new(self) 14 | } 15 | } 16 | } 17 | 18 | impl IteratorExt for I where I: Iterator {} 19 | 20 | pub trait ExactSizeIteratorExt: ExactSizeIterator { 21 | #[inline] 22 | fn dyn_rev<'t>(self, rev: bool) -> Box + 't> 23 | where 24 | Self: DoubleEndedIterator + Sized + 't, 25 | { 26 | if rev { 27 | Box::new(self.rev()) 28 | } else { 29 | Box::new(self) 30 | } 31 | } 32 | } 33 | 34 | impl ExactSizeIteratorExt for I where I: ExactSizeIterator {} 35 | 36 | pub trait IntoIteratorExt: IntoIterator { 37 | #[inline] 38 | fn join_all(self) -> JoinAll 39 | where 40 | Self::Item: Future, 41 | Self: Sized, 42 | { 43 | join_all(self) 44 | } 45 | } 46 | 47 | impl IntoIteratorExt for I where I: IntoIterator {} 48 | -------------------------------------------------------------------------------- /src/infra/json.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::de::DeserializeOwned; 3 | use serde::Serialize; 4 | use serde_json::Value; 5 | 6 | pub fn serialize(val: T) -> Result 7 | where 8 | T: Serialize, 9 | { 10 | serde_json::to_value::(val) 11 | .map_err(|e| anyhow!(e)) 12 | .map(|v| v.to_string()) 13 | } 14 | 15 | pub fn deserialize(json: &str) -> Result 16 | where 17 | T: DeserializeOwned, 18 | { 19 | let val: Value = serde_json::from_str(json)?; 20 | serde_json::from_value::(val).map_err(|e| anyhow!(e)) 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fp; 2 | pub mod http; 3 | pub mod infer; 4 | pub mod iter; 5 | pub mod json; 6 | pub mod option; 7 | pub mod result; 8 | pub mod str; 9 | pub mod terminal; 10 | pub mod time; 11 | pub mod vec; 12 | -------------------------------------------------------------------------------- /src/infra/option.rs: -------------------------------------------------------------------------------- 1 | pub trait WrapOption 2 | where 3 | Self: Sized, 4 | { 5 | #[inline] 6 | fn wrap_some(self) -> Option { 7 | Some(self) 8 | } 9 | } 10 | 11 | impl WrapOption for T {} 12 | 13 | pub trait OptionExt { 14 | fn or_eval_result(self, f: F) -> Result 15 | where 16 | F: FnOnce() -> Result; 17 | } 18 | 19 | impl OptionExt for Option { 20 | #[inline] 21 | fn or_eval_result(self, f: F) -> Result 22 | where 23 | F: FnOnce() -> Result, 24 | { 25 | self.map_or_else(f, |val| Ok(val)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/infra/result.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub trait WrapResult 4 | where 5 | Self: Sized, 6 | { 7 | #[inline] 8 | fn wrap_ok(self) -> Result { 9 | Ok(self) 10 | } 11 | #[inline] 12 | fn wrap_err(self) -> Result { 13 | Err(self) 14 | } 15 | } 16 | 17 | impl WrapResult for T {} 18 | 19 | pub type HomoResult = Result; 20 | 21 | pub trait ResultExt { 22 | fn err_to_string(self) -> Result 23 | where 24 | E: ToString; 25 | 26 | fn homo_string(self) -> HomoResult 27 | where 28 | O: ToString, 29 | E: ToString; 30 | } 31 | 32 | impl ResultExt for Result { 33 | #[inline] 34 | fn err_to_string(self) -> Result 35 | where 36 | E: ToString, 37 | { 38 | self.map_err(|e| e.to_string()) 39 | } 40 | 41 | #[inline] 42 | fn homo_string(self) -> HomoResult 43 | where 44 | O: ToString, 45 | E: ToString, 46 | { 47 | match self { 48 | Ok(o) => Ok(o.to_string()), 49 | Err(e) => Err(e.to_string()), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/infra/str.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow; 2 | use unicode_width::UnicodeWidthChar; 3 | 4 | pub trait StrExt { 5 | fn width_split_head(&self, head_width: usize) -> (&str, &str); 6 | fn width_split(&self, width: usize) -> Option>; 7 | } 8 | 9 | impl StrExt for str { 10 | fn width_split_head(&self, head_width: usize) -> (&str, &str) { 11 | let mut left_take = head_width; 12 | let mut take_bytes = 0; 13 | self.chars().try_for_each(|c| { 14 | let current_width = c.width_cjk().unwrap_or(0); 15 | if left_take > 0 { 16 | if left_take >= current_width { 17 | left_take -= current_width; 18 | take_bytes += c.len_utf8(); 19 | ControlFlow::Continue(()) 20 | } else { 21 | left_take = 0; 22 | ControlFlow::Break(()) 23 | } 24 | } else { 25 | ControlFlow::Break(()) 26 | } 27 | }); 28 | self.split_at(take_bytes) 29 | } 30 | 31 | fn width_split(&self, width: usize) -> Option> { 32 | let mut vec = vec![]; 33 | let mut str = self; 34 | loop { 35 | let (head, tail) = str.width_split_head(width); 36 | // No split strategy exist, return None 37 | if head.is_empty() { 38 | return None; 39 | } 40 | vec.push(head); 41 | if tail.is_empty() { 42 | break; 43 | } 44 | str = tail; 45 | } 46 | Some(vec) 47 | } 48 | } 49 | 50 | #[test] 51 | fn test_width_split_head() { 52 | let text = "测试test⭐"; 53 | assert_eq!(text.width_split_head(0), ("", "测试test⭐")); 54 | assert_eq!(text.width_split_head(1), ("", "测试test⭐")); 55 | assert_eq!(text.width_split_head(2), ("测", "试test⭐")); 56 | assert_eq!(text.width_split_head(3), ("测", "试test⭐")); 57 | assert_eq!(text.width_split_head(4), ("测试", "test⭐")); 58 | assert_eq!(text.width_split_head(5), ("测试t", "est⭐")); 59 | assert_eq!(text.width_split_head(9), ("测试test", "⭐")); 60 | assert_eq!(text.width_split_head(10), ("测试test⭐", "")); 61 | assert_eq!(text.width_split_head(11), ("测试test⭐", "")); 62 | } 63 | 64 | #[test] 65 | fn test_width_split() { 66 | use crate::infra::option::WrapOption; 67 | let text = "测试test⭐测试test⭐"; 68 | assert_eq!(text.width_split(0), None); 69 | assert_eq!(text.width_split(1), None); 70 | assert_eq!( 71 | text.width_split(2), 72 | vec!["测", "试", "te", "st", "⭐", "测", "试", "te", "st", "⭐"].wrap_some() 73 | ); 74 | assert_eq!( 75 | text.width_split(3), 76 | vec!["测", "试t", "est", "⭐", "测", "试t", "est", "⭐"].wrap_some() 77 | ); 78 | assert_eq!( 79 | text.width_split(4), 80 | vec!["测试", "test", "⭐测", "试te", "st⭐"].wrap_some() 81 | ); 82 | assert_eq!( 83 | text.width_split(19), 84 | vec!["测试test⭐测试test", "⭐"].wrap_some() 85 | ); 86 | assert_eq!( 87 | text.width_split(20), 88 | vec!["测试test⭐测试test⭐"].wrap_some() 89 | ); 90 | assert_eq!( 91 | text.width_split(21), 92 | vec!["测试test⭐测试test⭐"].wrap_some() 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/infra/terminal.rs: -------------------------------------------------------------------------------- 1 | use terminal_size::{terminal_size, Width}; 2 | 3 | pub fn get_term_width() -> usize { 4 | let (Width(width), _) = terminal_size().expect("Can not get terminal size"); 5 | width as usize 6 | } 7 | -------------------------------------------------------------------------------- /src/infra/time.rs: -------------------------------------------------------------------------------- 1 | use crate::args::TimeStyle; 2 | use chrono::{DateTime, Datelike, Local, TimeZone, Timelike, Utc}; 3 | use std::fmt::Display; 4 | 5 | pub fn display_cnb_time(time_str: &str, time_style: &TimeStyle) -> String { 6 | let rfc3339 = patch_rfc3339(time_str); 7 | let dt = DateTime::parse_from_rfc3339(&rfc3339) 8 | .unwrap_or_else(|_| panic!("Invalid RFC3339: {}", rfc3339)) 9 | .with_timezone(&Utc); 10 | 11 | match time_style { 12 | TimeStyle::Friendly => fmt_time_to_string_friendly(dt.into(), Local::now()), 13 | TimeStyle::Normal => dt.format("%y-%-m-%-d %-H:%M").to_string(), 14 | } 15 | } 16 | 17 | // HACK: 18 | // Sometimes cnblogs' web API returns time string like: "2023-09-12T14:07:00" or "2019-02-06T08:45:53.94" 19 | // This will patch it to standard RFC3339 format 20 | fn patch_rfc3339(time_str: &str) -> String { 21 | if time_str.len() != 25 { 22 | let u8vec: Vec<_> = time_str.bytes().take(19).collect(); 23 | format!( 24 | "{}+08:00", 25 | String::from_utf8(u8vec) 26 | .unwrap_or_else(|_| panic!("Can not patch time string: {}", time_str)) 27 | ) 28 | } else { 29 | time_str.to_owned() 30 | } 31 | } 32 | 33 | fn fmt_time_to_string_friendly(time_to_fmt: DateTime, current_time: DateTime) -> String 34 | where 35 | T: TimeZone, 36 | ::Offset: Display, 37 | { 38 | let diff = current_time.clone() - time_to_fmt.clone(); 39 | match diff { 40 | // In the future 41 | _ if diff.num_milliseconds() < 0 => time_to_fmt.format("%y-%-m-%-d %-H:%M").to_string(), 42 | // Same year... 43 | _ if time_to_fmt.year() != current_time.year() => { 44 | time_to_fmt.format("%Y-%m-%d").to_string() 45 | } 46 | _ if time_to_fmt.month() != current_time.month() => { 47 | time_to_fmt.format("%m-%d %H:%M").to_string() 48 | } 49 | _ if time_to_fmt.day() != current_time.day() => { 50 | let postfix = match time_to_fmt.day() { 51 | 1 => "st", 52 | 2 => "nd", 53 | 3 => "rd", 54 | _ => "th", 55 | }; 56 | time_to_fmt 57 | .format(&format!("%d{} %H:%M", postfix)) 58 | .to_string() 59 | } 60 | _ if time_to_fmt.hour() != current_time.hour() => time_to_fmt.format("%H:%M").to_string(), 61 | // Within an hour 62 | _ if diff.num_seconds() < 30 => "Now".to_owned(), 63 | _ if diff.num_minutes() < 3 => "Recently".to_owned(), 64 | _ if diff.num_minutes() < 30 => format!("{}m", diff.num_minutes()), 65 | _ => time_to_fmt.format("%H:%M").to_string(), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/infra/vec.rs: -------------------------------------------------------------------------------- 1 | pub trait VecExt { 2 | fn chain_push(self, item: T) -> Vec; 3 | } 4 | 5 | impl VecExt for Vec { 6 | #[inline] 7 | fn chain_push(mut self, item: T) -> Self { 8 | self.push(item); 9 | self 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(try_blocks)] 2 | #![feature(if_let_guard)] 3 | #![feature(let_chains)] 4 | #![feature(type_name_of_val)] 5 | #![feature(iterator_try_collect)] 6 | #![feature(iterator_try_reduce)] 7 | #![warn(clippy::all, clippy::nursery, clippy::cargo_common_metadata)] 8 | 9 | use crate::api::auth::session; 10 | use crate::api::fav::Fav; 11 | use crate::api::ing::Ing; 12 | use crate::api::news::News; 13 | use crate::api::post::Post; 14 | use crate::api::user::User; 15 | use crate::args::cmd::post::{CreateCmd, UpdateCmd}; 16 | use crate::args::parser::no_operation; 17 | use crate::args::{parser, Args}; 18 | use crate::infra::fp::currying::eq; 19 | use crate::infra::infer::infer; 20 | use crate::infra::iter::{ExactSizeIteratorExt, IntoIteratorExt}; 21 | use crate::infra::option::OptionExt; 22 | use crate::infra::result::WrapResult; 23 | use anyhow::Result; 24 | use clap::Parser; 25 | use clap::{Command, CommandFactory}; 26 | use colored::Colorize; 27 | use std::env; 28 | 29 | pub mod api; 30 | pub mod args; 31 | pub mod display; 32 | pub mod infra; 33 | 34 | fn show_non_printable_chars(text: String) -> String { 35 | #[inline] 36 | fn make_red(str: &str) -> String { 37 | format!("{}", str.red()) 38 | } 39 | 40 | text.replace(' ', &make_red("·")) 41 | .replace('\0', &make_red("␀\0")) 42 | .replace('\t', &make_red("␉\t")) 43 | .replace('\n', &make_red("␊\n")) 44 | .replace('\r', &make_red("␍\r")) 45 | .replace("\r\n", &make_red("␍␊\r\n")) 46 | } 47 | 48 | fn panic_if_err(result: &Result) { 49 | if let Err(e) = result { 50 | panic!("{}", e) 51 | } 52 | } 53 | 54 | #[tokio::main(flavor = "multi_thread")] 55 | async fn main() -> Result<()> { 56 | let args_vec = env::args().collect::>(); 57 | if args_vec.iter().any(eq(&"--debug".to_owned())) { 58 | dbg!(args_vec); 59 | } 60 | 61 | let args: Args = Args::parse(); 62 | let global_opt = &args.global_opt; 63 | if global_opt.debug { 64 | dbg!(&args); 65 | } 66 | 67 | let pat = global_opt.with_pat.clone().or_eval_result(session::get_pat); 68 | let style = &global_opt.style; 69 | let time_style = &global_opt.time_style; 70 | let rev = args.rev; 71 | let foe = global_opt.fail_on_error; 72 | 73 | let output = match args { 74 | _ if let Some(pat) = parser::user::login(&args) => { 75 | let cfg_path = session::login(pat); 76 | foe.then(|| panic_if_err(&cfg_path)); 77 | display::login(style, &cfg_path) 78 | } 79 | _ if parser::user::logout(&args) => { 80 | let cfg_path = session::logout(); 81 | foe.then(|| panic_if_err(&cfg_path)); 82 | display::logout(style, &cfg_path) 83 | } 84 | _ if parser::user::user_info(&args) => { 85 | let user_info = User::new(pat?).get_info().await; 86 | foe.then(|| panic_if_err(&user_info)); 87 | display::user_info(style, &user_info)? 88 | } 89 | _ if let Some((skip, take, r#type, align)) = parser::ing::list_ing(&args) => { 90 | let ing_with_comment_iter = infer::>(try { 91 | let ing_api = Ing::new(pat?); 92 | let ing_vec = ing_api.get_list(skip, take, &r#type).await?; 93 | ing_vec.into_iter() 94 | .map(|ing| async { 95 | let result = ing_api.get_comment_list(ing.id).await; 96 | result.map(|comment_vec| (ing, comment_vec)) 97 | }) 98 | .join_all() 99 | .await 100 | .into_iter() 101 | .collect::>>()? 102 | }).map(|vec| vec.into_iter().dyn_rev(rev)); 103 | foe.then(|| panic_if_err(&ing_with_comment_iter)); 104 | display::list_ing(style, time_style, ing_with_comment_iter, align)? 105 | } 106 | _ if let Some(content) = parser::ing::publish_ing(&args) => { 107 | let content = try { 108 | Ing::new(pat?).publish(content).await?; 109 | content 110 | }; 111 | foe.then(|| panic_if_err(&content)); 112 | display::publish_ing(style, &content) 113 | } 114 | _ if let Some((content, id)) = parser::ing::comment_ing(&args) => { 115 | let content = try { 116 | Ing::new(pat?).comment(id, content.clone(), None, None).await?; 117 | content 118 | }; 119 | foe.then(|| panic_if_err(&content)); 120 | display::comment_ing(style, &content) 121 | } 122 | _ if let Some(id) = parser::post::show_post(&args) => { 123 | let entry = Post::new(pat?).get_one(id).await; 124 | foe.then(|| panic_if_err(&entry)); 125 | display::show_post(style, &entry)? 126 | } 127 | _ if let Some(id) = parser::post::show_post_meta(&args) => { 128 | let entry = Post::new(pat?).get_one(id).await; 129 | foe.then(|| panic_if_err(&entry)); 130 | display::show_post_meta(style, time_style, &entry)? 131 | } 132 | _ if let Some(id) = parser::post::show_post_comment(&args) => { 133 | let comment_iter = Post::new(pat?) 134 | .get_comment_list(id).await 135 | .map(|vec| vec.into_iter().dyn_rev(rev)); 136 | foe.then(|| panic_if_err(&comment_iter)); 137 | display::show_post_comment(style, time_style, comment_iter)? 138 | } 139 | _ if let Some((skip, take)) = parser::post::list_post(&args) => { 140 | let meta_iter = Post::new(pat?) 141 | .get_meta_list(skip, take) 142 | .await 143 | .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); 144 | foe.then(|| panic_if_err(&meta_iter)); 145 | display::list_post(style, meta_iter)? 146 | } 147 | _ if let Some(id) = parser::post::delete_post(&args) => { 148 | let id = try { 149 | Post::new(pat?).del_one(id).await?; 150 | id 151 | }; 152 | foe.then(|| panic_if_err(&id)); 153 | display::delete_post(style, &id) 154 | } 155 | _ if let Some((kw, skip, take)) = parser::post::search_self_post(&args) => { 156 | let result = Post::new(pat?) 157 | .search_self(skip, take, kw) 158 | .await 159 | .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); 160 | foe.then(|| panic_if_err(&result)); 161 | display::search_self_post(style, result)? 162 | } 163 | _ if let Some((kw, skip, take)) = parser::post::search_site_post(&args) => { 164 | let result = Post::new(pat?) 165 | .search_site(skip, take, kw) 166 | .await 167 | .map(|vec | vec.into_iter().dyn_rev(rev)); 168 | foe.then(|| panic_if_err(&result)); 169 | display::search_site_post(style, time_style, result)? 170 | } 171 | _ if let Some(create_cmd) = parser::post::create_post(&args) => { 172 | let CreateCmd { title, body, publish } = create_cmd; 173 | let id = Post::new(pat?).create(title, body, *publish).await; 174 | foe.then(|| panic_if_err(&id)); 175 | display::create_post(style, &id) 176 | } 177 | _ if let Some((id, update_cmd)) = parser::post::update_post(&args) => { 178 | let UpdateCmd { title, body, publish } = update_cmd; 179 | let id = Post::new(pat?).update(id, title, body, publish).await; 180 | foe.then(|| panic_if_err(&id)); 181 | display::update_post(style, &id) 182 | } 183 | _ if let Some((skip, take)) = parser::news::list_news(&args) => { 184 | let news_iter = News::new(pat?) 185 | .get_list(skip, take) 186 | .await 187 | .map(|vec| vec.into_iter().dyn_rev(rev)); 188 | foe.then(|| panic_if_err(&news_iter)); 189 | display::list_news(style, time_style, news_iter)? 190 | } 191 | _ if let Some((skip, take)) = parser::fav::list_fav(&args) => { 192 | let fav_iter = Fav::new(pat?) 193 | .get_list(skip, take) 194 | .await 195 | .map(|vec| vec.into_iter().dyn_rev(rev)); 196 | foe.then(|| panic_if_err(&fav_iter)); 197 | display::list_fav(style, time_style, fav_iter)? 198 | } 199 | 200 | _ if no_operation(&args) => 201 | infer::(Args::command()).render_help().to_string(), 202 | _ => "Invalid usage, follow '--help' for more information".to_owned() 203 | }; 204 | 205 | if global_opt.quiet { 206 | return ().wrap_ok(); 207 | } 208 | 209 | let output = { 210 | let output = if output.ends_with("\n\n") { 211 | output[..output.len() - 1].to_owned() 212 | } else if output.ends_with('\n') { 213 | output 214 | } else { 215 | format!("{}\n", output) 216 | }; 217 | if global_opt.debug { 218 | show_non_printable_chars(output) 219 | } else { 220 | output 221 | } 222 | }; 223 | 224 | print!("{}", output); 225 | 226 | ().wrap_ok() 227 | } 228 | --------------------------------------------------------------------------------