diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 08a7b37..91c3279 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -18,4 +18,4 @@ runs: - name: Build release if: ${{ inputs.release == 'true' }} shell: bash - run: cargo build --target ${{ inputs.target }} -r + run: cargo build --release --target ${{ inputs.target }} -r diff --git a/.github/actions/check/action.yml b/.github/actions/check/action.yml index 6a593e5..9d6610d 100644 --- a/.github/actions/check/action.yml +++ b/.github/actions/check/action.yml @@ -6,7 +6,9 @@ runs: - name: Audit shell: bash run: | - cargo install cargo-audit + if ! command -v cargo-audit &> /dev/null; then + cargo install cargo-audit + fi cargo audit - name: Fmt diff --git a/.github/actions/make-archive/action.yml b/.github/actions/make-archive/action.yml index 299d3ea..198fa96 100644 --- a/.github/actions/make-archive/action.yml +++ b/.github/actions/make-archive/action.yml @@ -20,4 +20,4 @@ runs: if: runner.os == 'Windows' shell: pwsh run: | - Compress-Archive ${{ inputs.files }} ${{ inputs.out }} + Compress-Archive -Path "${{ inputs.files }}" -DestinationPath "${{ inputs.out }}" diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 2695a65..86354f5 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -5,8 +5,4 @@ runs: steps: - name: Test dev shell: bash - run: cargo test --verbose - - - name: Test release - shell: bash - run: cargo test --verbose --release + run: cargo test diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml deleted file mode 100644 index a517808..0000000 --- a/.github/workflows/build-dev.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Build / Development - -on: - push: - branches: - - '*' - pull_request_target: - types: - - edited - - opened - - reopened - - synchronize - -jobs: - build-dev: - name: ${{ matrix.targets.alias }} - runs-on: ${{ matrix.targets.os }} - - strategy: - fail-fast: false - matrix: - targets: - # aarch64 - - { os: macos-11 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-11 } - - { os: macos-12 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-12 } - - { os: macos-13 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-13 } - # amd64 - - { os: macos-12 , target: x86_64-apple-darwin , alias: amd64-darwin-macos-unknown } - - { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-20.04 } - - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-22.04 } - - { os: ubuntu-22.04, target: x86_64-unknown-linux-musl, alias: amd64-musl-linux-unknown } - - { os: windows-2019, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2019 } - - { os: windows-2022, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2022 } - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Setup Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - toolchain: nightly-2023-09-06 - components: rustfmt, clippy - - - name: Show Rust toolchain version - shell: bash - run: | - cargo -V - cargo clippy -V - cargo fmt -- -V - rustc -V - - - name: Setup musl-tools - if: matrix.targets.target == 'x86_64-unknown-linux-musl' - shell: bash - run: sudo apt -y install musl-tools - - - name: Add target - uses: ./.github/actions/add-target - with: - target: ${{ matrix.targets.target }} - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.targets.alias }} - - - name: Generate version - id: gen-version - shell: bash - run: echo 'VERSION=0.0.0-${{ github.sha }}' >> $GITHUB_OUTPUT - - - name: Replace version - uses: ./.github/actions/replace-version - with: - version: ${{ steps.gen-version.outputs.VERSION }} - - - name: Run check - uses: ./.github/actions/check - - - name: Run test - uses: ./.github/actions/test - - - name: Run build - uses: ./.github/actions/build - with: - target: ${{ matrix.targets.target }} - release: false - - - name: Generate artifacts name - id: gen-name - shell: bash - run: echo 'NAME=cnb-dev-${{ matrix.targets.alias }}' >> $GITHUB_OUTPUT - - - name: Generate binary extension - id: gen-ext - if: runner.os == 'Windows' - shell: bash - run: echo 'EXT=.exe' >> $GITHUB_OUTPUT - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ steps.gen-name.outputs.NAME }} - path: ./target/${{ matrix.targets.target }}/debug/cnb${{ steps.gen-ext.outputs.EXT }} - if-no-files-found: error diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 976d5ef..0000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Build / Release - -on: - push: - tags: - - v*.*.* - -jobs: - build-release: - name: ${{ matrix.targets.alias }} - runs-on: ${{ matrix.targets.os }} - - strategy: - fail-fast: false - matrix: - targets: - # aarch64 - - { os: macos-11 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-11 } - - { os: macos-12 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-12 } - - { os: macos-13 , target: aarch64-apple-darwin , alias: aarch64-darwin-macos-13 } - # amd64 - - { os: macos-12 , target: x86_64-apple-darwin , alias: amd64-darwin-macos-unknown } - - { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-20.04 } - - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu , alias: amd64-gnu-ubuntu-22.04 } - - { os: ubuntu-22.04, target: x86_64-unknown-linux-musl, alias: amd64-musl-linux-unknown } - - { os: windows-2019, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2019 } - - { os: windows-2022, target: x86_64-pc-windows-msvc , alias: amd64-msvc-windows-2022 } - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Setup Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - toolchain: nightly-2023-09-06 - components: rustfmt, clippy - - - name: Setup musl-tools - if: matrix.targets.target == 'x86_64-unknown-linux-musl' - shell: bash - run: sudo apt -y install musl-tools - - - name: Add target - uses: ./.github/actions/add-target - with: - target: ${{ matrix.targets.target }} - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.targets.alias }} - - - name: Generate version - id: gen-version - shell: bash - run: echo 'VERSION=${{ github.ref_name }}' | sed 's/v//' >> $GITHUB_OUTPUT - - - name: Replace version - uses: ./.github/actions/replace-version - with: - version: ${{ steps.gen-version.outputs.VERSION }} - - - name: Run build - uses: ./.github/actions/build - with: - target: ${{ matrix.targets.target }} - release: true - - - name: Generate artifacts name - id: gen-name - shell: bash - run: echo 'NAME=cnb-${{ steps.gen-version.outputs.VERSION }}-${{ matrix.targets.alias }}' >> $GITHUB_OUTPUT - - - name: Generate binary extension - id: gen-ext - if: runner.os == 'Windows' - shell: bash - run: echo 'EXT=.exe' >> $GITHUB_OUTPUT - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ steps.gen-name.outputs.NAME }} - path: ./target/${{ matrix.targets.target }}/release/cnb${{ steps.gen-ext.outputs.EXT }} - if-no-files-found: error - - - name: Archive binary - uses: ./.github/actions/make-archive - with: - files: ./target/${{ matrix.targets.target }}/release/cnb${{ steps.gen-ext.outputs.EXT }} - out: ${{ steps.gen-name.outputs.NAME }}.zip - - - name: Create GitHub release - uses: softprops/action-gh-release@v1 - with: - files: ${{ steps.gen-name.outputs.NAME }}.zip - diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..e661be2 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,47 @@ +name: Build / Development + +on: + pull_request: + types: + - opened + - reopened + - synchronize + +jobs: + commit-check: + name: Commit Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # 必须,才能拿到完整 commit history + + - name: Lint commits + uses: wagoid/commitlint-github-action@v6 + + pr-check: + needs: commit-check + name: Pull Request Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # 浅克隆以加快速度 + persist-credentials: false + + - name: Setup Rust toolchain + uses: dsherret/rust-toolchain-file@v1 + + - name: Fmt + run: cargo fmt --check + + - name: Clippy + run: cargo clippy -- -D warnings + + - name: Test + run: cargo test + + - name: Build Dev + run: cargo build \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..066964b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Build / Release + +on: + push: + branches: + - main + +permissions: + contents: write + + +jobs: + + prepare: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + should_release: ${{ steps.check.outputs.should_release }} + + steps: + - uses: actions/checkout@v4 + + - name: Read version from Cargo.toml + id: version + run: | + VERSION=$(grep '^version' Cargo.toml | sed 's/.*= "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Check if release exists + id: check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="v${{ steps.version.outputs.version }}" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "should_release=false" >> $GITHUB_OUTPUT + else + echo "should_release=true" >> $GITHUB_OUTPUT + fi + + - name: Create and push tag + if: steps.check.outputs.should_release == 'true' + run: | + TAG="v${{ steps.version.outputs.version }}" + git tag "$TAG" + git push origin "$TAG" + + build-release: + needs: prepare + if: needs.prepare.outputs.should_release == 'true' + name: ${{ matrix.targets.alias }} + runs-on: ${{ matrix.targets.os }} + + strategy: + fail-fast: false + matrix: + targets: + - { os: macos-latest, target: aarch64-apple-darwin, alias: aarch64-apple-darwin } + - { os: macos-latest, target: x86_64-apple-darwin, alias: x86_64-apple-darwin } + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, alias: x86_64-unknown-linux-gnu } + - { os: ubuntu-latest, target: x86_64-unknown-linux-musl,alias: x86_64-unknown-linux-musl} + - { os: windows-latest, target: x86_64-pc-windows-msvc, alias: x86_64-pc-windows-msvc } + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: dsherret/rust-toolchain-file@v1 + + - name: Setup musl-tools + if: matrix.targets.target == 'x86_64-unknown-linux-musl' + shell: bash + run: sudo apt -y install musl-tools + + - name: Add target + uses: ./.github/actions/add-target + with: + target: ${{ matrix.targets.target }} + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.targets.alias }} + + - name: Run build + uses: ./.github/actions/build + with: + target: ${{ matrix.targets.target }} + release: true + + - name: Archive binary + uses: ./.github/actions/make-archive + with: + files: ./target/${{ matrix.targets.target }}/release/cnb${{ matrix.targets.target == 'x86_64-pc-windows-msvc' && '.exe' || '' }} + out: cnb-${{ needs.prepare.outputs.version }}-${{ matrix.targets.target }}.zip + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.prepare.outputs.version }} + files: cnb-${{ needs.prepare.outputs.version }}-${{ matrix.targets.target }}.zip diff --git a/.gitignore b/.gitignore index ee44a96..f288b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea +.vscode +.zed + +**/.DS_Store + target diff --git a/Cargo.lock b/Cargo.lock index 7fc3726..bf35e16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,37 +1,16 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +version = 4 [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,153 +22,167 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] -name = "autocfg" -version = "1.1.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "backtrace" -version = "0.3.68" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base64" -version = "0.21.4" +name = "aws-lc-rs" +version = "1.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +dependencies = [ + "aws-lc-sys", + "zeroize", +] [[package]] -name = "base64url" -version = "0.1.0" +name = "aws-lc-sys" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33de68096bac8e252e45589f42afd364c1dd28fbb3466ed726a941d5b9727d2c" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" dependencies = [ - "base64", + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.4.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.0.82" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ + "find-msvc-tools", + "jobserver", "libc", + "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] name = "clap" -version = "4.4.3" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -197,9 +190,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -210,9 +203,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -222,141 +215,308 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "cmake" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] [[package]] -name = "cnb" +name = "cnblogs_lib" version = "0.0.0-dev" dependencies = [ + "anstream", + "anstyle", "anyhow", - "base64", - "base64url", "chrono", "clap", - "colored", - "futures", - "getrandom", + "const_format", "home", + "html2md", "lazy_static", - "mime", - "rand", + "owo-colors", "regex", "reqwest", "serde", "serde_json", - "serde_qs", - "serde_repr", - "serde_with", - "terminal_size", + "termimad", "tokio", - "unicode-width", - "words-count", ] [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "colored" -version = "2.0.4" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "is-terminal", - "lazy_static", - "windows-sys", + "bytes", + "memchr", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crokey" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn", +] + +[[package]] +name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] [[package]] -name = "darling" -version = "0.20.3" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "darling_core", - "darling_macro", + "crossbeam-utils", ] [[package]] -name = "darling_core" -version = "0.20.3" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "fnv", - "ident_case", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", "proc-macro2", "quote", - "strsim", + "rustc_version", "syn", ] [[package]] -name = "darling_macro" -version = "0.20.3" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "darling_core", + "proc-macro2", "quote", "syn", ] [[package]] -name = "deranged" -version = "0.3.7" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "serde", + "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "find-msvc-tools" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fnv" @@ -366,97 +526,64 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] -name = "futures" -version = "0.3.28" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "futures-channel" -version = "0.3.28" +name = "futf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ - "futures-core", - "futures-sink", + "mac", + "new_debug_unreachable", ] [[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" +name = "futures-channel" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-task", - "futures-util", ] [[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -464,9 +591,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -476,24 +603,32 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.27.3" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] [[package]] name = "h2" -version = "0.3.20" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -502,127 +637,169 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hermit-abi" -version = "0.3.2" +name = "home" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] -name = "hex" -version = "0.4.3" +name = "html2md" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "8cff9891f2e0d9048927fbdfc28b11bf378f6a93c7ba70b23d0fbee9af6071b4" +dependencies = [ + "html5ever", + "jni 0.19.0", + "lazy_static", + "markup5ever_rcdom", + "percent-encoding", + "regex", +] [[package]] -name = "home" -version = "0.5.5" +name = "html5ever" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ - "windows-sys", + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "http" -version = "0.2.9" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] name = "http-body" -version = "0.4.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", - "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.8.0" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "0.14.27" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "pin-utils", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", + "hyper-util", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -635,131 +812,312 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] name = "idna" -version = "0.4.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.0.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.14.0", - "serde", + "hashbrown", ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "memchr", + "serde", ] [[package]] -name = "ipnet" -version = "2.8.0" +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] -name = "is-terminal" -version = "0.4.9" +name = "jni" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ - "hermit-abi", - "rustix 0.38.8", - "windows-sys", + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", ] [[package]] -name = "itoa" -version = "1.0.9" +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "lazy-regex" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c13b6857ade4c8ee05c3c3dc97d2ab5415d691213825b90d3211c425c1f907" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a95c68db5d41694cea563c86a4ba4dc02141c16ef64814108cb23def4d5438" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] -name = "linux-raw-sys" -version = "0.4.5" +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.20" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -768,64 +1126,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "miniz_oxide" -version = "0.7.1" +name = "minimad" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "df8b688969b16915f3ecadc7829d5b7779dee4977e503f767f34136803d5c06f" dependencies = [ - "adler", + "once_cell", ] [[package]] name = "mio" -version = "0.8.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "object" -version = "0.31.1" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" -dependencies = [ - "memchr", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "once_cell" -version = "1.18.0" +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "owo-colors" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -833,28 +1197,66 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -862,49 +1264,137 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" -version = "1.0.32" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -912,24 +1402,30 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -939,9 +1435,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -950,203 +1446,276 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", - "futures-util", "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-rustls", - "ipnet", + "hyper-util", "js-sys", "log", "mime", - "once_cell", "percent-encoding", "pin-project-lite", + "quinn", "rustls", - "rustls-pemfile", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "tokio", "tokio-rustls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", - "winreg", ] [[package]] name = "ring" -version = "0.16.20" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", + "cfg-if", + "getrandom 0.2.17", "libc", - "once_cell", - "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "rustc-demangle" -version = "0.1.23" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustix" -version = "0.37.23" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys", + "semver", ] [[package]] name = "rustix" -version = "0.38.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.4.0", + "bitflags", "errno", "libc", - "linux-raw-sys 0.4.5", - "windows-sys", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.21.7" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ - "log", - "ring", + "aws-lc-rs", + "once_cell", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] -name = "rustls-pemfile" -version = "1.0.3" +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "base64", + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] -name = "rustls-webpki" -version = "0.101.4" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "ring", - "untrusted", + "web-time", + "zeroize", ] [[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "scopeguard" -version = "1.2.0" +name = "rustls-platform-verifier" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.21.1", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] [[package]] -name = "sct" -version = "0.7.0" +name = "rustls-platform-verifier-android" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] [[package]] -name = "serde" -version = "1.0.188" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "serde_derive", + "winapi-util", ] [[package]] -name = "serde_derive" -version = "1.0.188" +name = "schannel" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-sys 0.61.2", ] [[package]] -name = "serde_json" -version = "1.0.107" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "itoa", - "ryu", - "serde", + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "serde_qs" -version = "0.12.0" +name = "security-framework-sys" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ - "percent-encoding", - "serde", - "thiserror", + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", ] [[package]] -name = "serde_repr" -version = "0.1.16" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1160,125 +1729,231 @@ dependencies = [ ] [[package]] -name = "serde_with" -version = "3.3.0" +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.0.0", - "serde", - "serde_json", - "serde_with_macros", - "time", + "libc", + "signal-hook-registry", ] [[package]] -name = "serde_with_macros" -version = "3.3.0" +name = "signal-hook-mio" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", + "libc", + "mio", + "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.11.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.4.9" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "winapi", + "windows-sys 0.60.2", ] [[package]] -name = "socket2" -version = "0.5.3" +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + +[[package]] +name = "string_cache" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ - "libc", - "windows-sys", + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", ] [[package]] -name = "spin" -version = "0.5.2" +name = "string_cache_codegen" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.28" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termimad" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889a9370996b74cf46016ce35b96c248a9ac36d69aab1d112b3e09bc33affa49" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 2.0.18", + "unicode-width", +] + [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "rustix 0.37.23", - "windows-sys", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "1.0.45" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedd246497092a89beedfe2c9f176d44c1b672ea6090edc20544ade01fbb7ea0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", ] [[package]] name = "thiserror-impl" -version = "1.0.45" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b1fadccbbc7e19ea64708629f9d8dccd007c260d66485f20a6d41bc1cf4b3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1286,38 +1961,31 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.25" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "deranged", - "itoa", - "serde", - "time-core", - "time-macros", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.11" +name = "tinystr" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ - "time-core", + "displaydoc", + "zerovec", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1330,28 +1998,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -1360,9 +2026,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -1370,105 +2036,156 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "unicode-blocks" -version = "0.1.8" +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84398c527c802fbf222e5145f220382d60f1878e0e6cb4d22a3080949a8ddcd" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] -name = "unicode-ident" -version = "1.0.11" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-normalization" -version = "0.1.22" +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] -name = "unicode-width" -version = "0.1.10" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.0" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] name = "want" @@ -1481,52 +2198,51 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.87" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" +name = "wasm-bindgen" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1534,38 +2250,54 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webpki-roots" -version = "0.25.2" +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "winapi" @@ -1583,6 +2315,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1590,95 +2331,425 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "windows-targets", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows-targets", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" -version = "0.48.2" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.2" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.2" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.48.2" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.48.2" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.48.2" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.2" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.48.2" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "winreg" -version = "0.50.0" +name = "xml5ever" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ - "cfg-if", - "windows-sys", + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", ] [[package]] -name = "words-count" +name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28653ddaede5475c44a03e4014ae19f35aa9b231c423228b28963cb873e4869" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ - "unicode-blocks", + "proc-macro2", + "quote", + "syn", ] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index edffc32..fe5c0cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,49 +1,40 @@ [package] -name = "cnb" +name = "cnblogs_lib" # WRN: Version will be updated by CI while create a tag, NERVER change this. version = "0.0.0-dev" -edition = "2021" +edition = "2024" description = "Cnblogs' command line tool" license = "MIT" repository = "https://github.com/cnblogs/cli" keywords = ["cli", "cnblogs", "blog"] categories = ["command-line-utilities"] +default-run = "cnb" -[profile.dev] -lto = true -strip = true +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "cnb" +path = "src/bin/cnb.rs" + +[dependencies] +anstream = "0.6.21" +anstyle = "1.0.13" +anyhow = "1.0.100" +chrono = { version = "0.4.42", features = ["serde"] } +clap = { version = "4.5.54", features = ["derive", "wrap_help"] } +const_format = "0.2.35" +home = "0.5.12" +html2md = "0.2.15" +lazy_static = "1.5.0" +owo-colors = "4.2.3" +regex = "1.12.2" +reqwest = { version = "0.13.1", features = ["json", "query", "form"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +termimad = "0.34.1" +tokio = { version = "1.49.0", features = ["full"] } [profile.release] lto = true strip = true codegen-units = 1 - -[dependencies] -anyhow = "1.0.75" -lazy_static = "1.4.0" -base64 = "0.21.4" -base64url = "0.1.0" -getrandom = { version = "0.2.10", features = ["js"] } -rand = { version = "0.8.5" } -regex = "1.9.5" -words-count = "0.1.6" -unicode-width = "0.1.10" - -serde = { version = "1.0.188", features = ["derive"] } -serde_qs = "0.12.0" -serde_json = "1.0.107" -serde_with = "3.3.0" -serde_repr = "0.1.16" - -home = "0.5.5" -chrono = "0.4.30" -mime = "0.3.17" -reqwest = { version = "0.11.20", default-features = false, features = ["json", "rustls-tls"] } -tokio = { version = "1.32.0", features = ["full"] } -futures = "0.3.28" - -clap = { version = "4.4.3", features = ["derive", "wrap_help"] } -colored = "2.0.4" -terminal_size = "0.2.6" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 520c86e..ee3c420 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,191 @@ -# Cnblogs' command line tool +**Languages:** [English](README.md) | [简体中文](README.zh-CN.md) +# Cnblogs' command line tool + [![Build / Release](https://github.com/cnblogs/cli/actions/workflows/build-release.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-release.yml) [![Build / Development](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml) -Access cnblogs form CLI. +Access and manage your cnblogs content directly form the command line. -## Usage +## Features -To use `cnb` directly, add it to your environment variables is required. +- 📝 Manage Content: Create, view, and interact with posts, moments, and comments +- 🔐 Secure Authentication: Personal Access Token (PAT) based authentication +- ⚡ Fast & Lightweight: Built with Rust for optimal performance +- 🔧 Cross-Platform: Available for Windows, macOS, and Linux +- 📚 Intuitive Interface: Familiar CLI patterns and comprehensive help system -### Login +## Installation -You need to get your PAT from [https://account.cnblogs.com/settings/tokens](https://account.cnblogs.com/settings/tokens) to use this tool. +### Download Pre-built Binaries (Recommended) -Then run `cnb user --login 'YOUR_PAT_HERE'`. This will save your PAT to `~/.cnbrc`. +Download the latest release for your platform from the [Releases page](https://github.com/cnblogs/cli/releases). -If you want to logout, run `cnb user --logout` or just remove `~/.cnbrc`. +#### Quick Install (macOS/Linux) -### Examples +```sh +# Download and install cnb +curl -fSL -O https://github.com/cnblogs/cli/releases/download/v$version/cnb-$version-$arch-$os.zip +unzip -d . cnb-$version-$arch-$os.zip +mv ./cnb ~/.local/bin/ +``` -It's time to enjoy cnblogs. +### Build from Source -Here are some simple examples: +Versions prior to 0.2.1 (inclusive) require a Rust nightly toolchain(channel=2026-01-10+), and versions after 0.2.1 switch to the stable toolchain (1.92+) -```shell -# Check your post list -cnb post --list -# Check your post -cnb --id 114514 post --show -# Create and publish post -cnb post create --title 'Hello' --body 'world!' --publish -# Change your post body -cnb --id 114514 post update --body 'niconiconiconi' +If you have a Rust compilation environment locally, you can install or build it using Cargo. -# Show ing list -cnb ing list -# Publish ing -cnb ing --publish 'Hello world!' -# Comment to ing -cnb --id 114514 ing --comment 'Awesome!' +Cargo Install + +```sh +# from repo main +cargo install --git https://github.com/cnblogs/cli.git + +# Or local install +# Clone repository +git clone --depth 1 https://github.com/cnblogs/cli.git -# Check your user infomation -cnb user --info +cargo install --path ./cli --bin cnb ``` -For more information, try `cnb --help`. +Build from Source -## Installation +```sh +# Clone repository +git clone --depth 1 https://github.com/cnblogs/cli.git +cd cli -### From releases +# Build release version +cargo build --release --bin cnb -[Releases](https://github.com/cnblogs/cli/releases) +# The binary will be available at ./target/release/cnb +``` -### Build locally +## Quick Start -This tool requires nightly toolchains(1.74.0+) to build. +### 1. Get Your Personal Access Token -```shell -git clone --depth 1 https://github.com/cnblogs/cli.git -cd cli -cargo build -r +1. Visit [https://account.cnblogs.com/settings/tokens](https://account.cnblogs.com/settings/tokens) +2. Click "Generate New Token" +3. Copy the generated token (you won't be able to see it again) + +### 2. Login + +```sh +# login (recommended) +cnb user login YOUR_PAT_TOKEN + +# Verify login status +cnb user status ``` -Or get binaries from [CI](https://github.com/cnblogs/cli/actions) artifacts. +This will save your PAT to `~/.cnblogs/token` + +If you want to log out, run `cnb user logout` or just remove `~/.cnblogs`. + +## Command Reference + +### Command Usage + +```sh +cnb [option] [arg] +``` + +### Available Commands + +| Command | Description | Available Subcommands | +|---------|------------------|---------------------------------------------| +| `user` | User module | `login`, `logout`, `status` | +| `ing` | Moments module | `create`, `delete`, `list`, `show`, `reply` | +| `post` | posts module | `list`, `show`, `reply` | +| `news` | news module | `list` | +| `fav` | bookmarks module | `list` | + +### Usage Examples + +It's time to enjoy cnblogs. + +Here are some simple examples: + +```sh +# Check your post list +cnb post list +# Check your post +cnb post show 114514 + +# Show ing list +cnb ing list +cnb ing list my --page-index 1 --page-size 10 + +# Publish ing +cnb ing create 'Hello world!' +cnb ing create 'Hello world!' --tag lucky + +# Comment to ing +cnb ing replay 'Awesome!' --id 114514 +``` + +For more information, try `cnb --help` or `cnb help`. + +## Project Structure + +```text +cli/ +├── Cargo.lock +├── Cargo.toml +├── LICENSE +├── README.md +├── README.zh-CN.md +├── rust-fmt.toml +├── rust-toolchain.toml +├── shell.nix +└── src + ├── api # CNBlogs API interaction module + │ ├── fav.rs # Favorites/bookmarks API endpoints + │ ├── ing.rs # Moments/statuses API endpoints + │ ├── mod.rs # Module exports for API layer + │ ├── news.rs # News API endpoints + │ ├── post.rs # Blog posts API endpoints + │ ├── urls.rs # URL generator for API request construction + │ └── user.rs # User API endpoints (login, profile, etc.) + ├── bin # Binary entry point + │ └── cnb.rs # CLI main executable (argument parsing and routing) + ├── commands # CLI command implementations + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + ├── context # Context management (configuration, state, output) + │ ├── config.rs # Configuration file reading/writing and management + │ ├── mod.rs # Context module exports + │ └── output.rs # Output formatting control (JSON, table, text, etc.) + ├── display # Data display and formatting module + │ ├── ing.rs # Moment data display formatting + │ └── mod.rs # Display module exports + ├── lib.rs # Library crate root, exports public interfaces + ├── logic # Business logic layer (orchestrates operations) + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + ├── models # Data model definitions and formats + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + └── tools # Utility functions and extensions + ├── http.rs # Extensions to reqwest. + ├── mod.rs + ├── strings.rs # Extensions to String. + └── timer.rs # Extensions to chrono. +``` ## License diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..4eaf220 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,199 @@ +# Cnblogs 命令行工具 + +[![Build / Release](https://github.com/cnblogs/cli/actions/workflows/build-release.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-release.yml) +[![Build / Development](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml) + +直接从命令行访问和管理您的博客园内容。 + +## 功能特性 + +- 📝 内容管理: 创建、查看和互动博客文章、闪存和评论 +- 🔐 安全认证: 基于个人访问令牌 (PAT) 的身份验证 +- ⚡ 快速轻量: 使用 Rust 构建,性能优异 +- 🔧 跨平台: 支持 Windows、macOS 和 Linux +- 📚 直观界面: 熟悉的 CLI 模式和全面的帮助系统 + +## 安装方法 + +### 下载预编译二进制文件(推荐) + +从[发布页面](https://github.com/cnblogs/cli/releases)下载适用于您平台的最新版本。 + +#### 快速安装(macOS/Linux) + +```sh +# 下载并安装 cnb +curl -fSL -O https://github.com/cnblogs/cli/releases/download/v$version/cnb-$version-$arch-$os.zip +unzip -d . cnb-$version-$arch-$os.zip +mv ./cnb ~/.local/bin/ +``` + +Windows PowerShell + +```powershell +# 下载并解压 +Invoke-WebRequest -Uri "https://github.com/cnblogs/cli/releases/latest/download/cnb-x86_64-pc-windows-msvc.zip" -OutFile "cnb.zip" +Expand-Archive -Path "cnb.zip" -DestinationPath "." +# 将 cnb.exe 添加到 PATH 环境变量 +``` + +### 从源码构建 + +`v0.2.1`之前的版本需要nightly版本,`channel`推荐`2026-01-10`以上。最新版本已切换至`stable`版本(1.95+)。 + +Cargo安装 + +```sh +# from repo main +cargo install --git https://github.com/cnblogs/cli.git + +# Or local install +# Clone repository +git clone --depth 1 https://github.com/cnblogs/cli.git + +cargo install --path ./cli --bin cnb +``` + +源码编译 + +```sh +# 克隆仓库 +git clone --depth 1 https://github.com/cnblogs/cli.git +cd cli + +# 构建发布版本 +cargo build --release --bin cnb + +# 二进制文件位于 ./target/release/cnb(或 Windows 上的 cnb.exe) +``` + +## 快速开始 + +### 1. 获取个人访问令牌 + +1. 访问[https://account.cnblogs.com/settings/tokens](https://account.cnblogs.com/settings/tokens) +2. 点击"生成新令牌" +3. 复制生成的令牌(以后将无法再次查看) + +### 2. 登录 + +```bash +# 登录(推荐) +cnb user login YOUR_PAT_TOKEN + +# 验证登录状态 +cnb user status +``` + +您的令牌安全地存储在 `~/.cnblogs/token`(Windows:`%USERPROFILE%\.cnblogs\token`)。 + +## 命令参考 + +### 命令格式 + +```bash +cnb <命令> <子命令> [选项] [参数] +``` + +### 命令参考 + +| 命令 | 描述 | 可用子命令 | +|--------|------|---------------------------------------------| +| `user` | 用户模块 | `login`, `logout`, `status` | +| `ing` | 闪存管理 | `create`, `delete`, `list`, `show`, `reply` | +| `post` | 博客文章 | `list`, `show`, `reply` | +| `news` | 新闻 | `list` | +| `fav` | 书签 | `list` | + +### 使用示例 + +以下级几个简单的示例: + +```sh +# 查看你的随笔 +cnb post list +# 查看随笔内容 +cnb post show 114514 + +# 闪存相关操作 +cnb ing list +cnb ing list my --page-index 1 --page-size 10 + +# 发布闪存 +cnb ing create 'Hello world!' +cnb ing create 'Hello world!' --tag lucky + +# 发布评论 +cnb ing replay 'Awesome!' --id 114514 +``` + +更多使用信息请通过`cnb --help`或者`cnb help`查询 + +## 项目结构 + +```text +cli/ +├── Cargo.lock +├── Cargo.toml +├── LICENSE +├── README.md +├── README.zh-CN.md +├── rust-fmt.toml +├── rust-toolchain.toml +├── shell.nix +└── src + ├── api # 博客园API交互模块 + │ ├── fav.rs # 书签API + │ ├── ing.rs # 闪存API + │ ├── mod.rs # Module exports for API layer + │ ├── news.rs # 新闻API + │ ├── post.rs # 随笔API + │ ├── urls.rs # 生成API的url + │ └── user.rs # 用户API + ├── bin # 二进制目录 + │ └── cnb.rs # cli可执行文件 + ├── commands # CLI 命令实现 + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + ├── context # 上下文管理 + │ ├── config.rs # 配置文件 + │ ├── mod.rs # 模块管理,Context实现 + │ └── output.rs # 输出管理 + ├── display # 显示相关 + │ ├── ing.rs # 闪存相关`trait`的定义和实现 + │ └── mod.rs + ├── lib.rs # lib + ├── logic # 实现逻辑 + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + ├── models # 模型定义和格式化输出, + │ ├── fav.rs + │ ├── ing.rs + │ ├── mod.rs + │ ├── news.rs + │ ├── post.rs + │ └── user.rs + └── tools # 工具模块,定义一些拓展和函数 + ├── http.rs # reqwest的拓展 + ├── mod.rs + ├── strings.rs # String的拓展 + └── timer.rs # chrono的拓展 +``` + +## 许可证 + +[MIT](https://raw.githubusercontent.com/cnblogs/cli/main/LICENSE) + +## 反馈 + +反馈我们十分期待你对本项目的看法,欢迎随时留言交流! + +[Issues](https://github.com/cnblogs/cli/issues) diff --git a/rust-fmt.toml b/rust-fmt.toml new file mode 100644 index 0000000..24e0279 --- /dev/null +++ b/rust-fmt.toml @@ -0,0 +1,5 @@ +max_width = 79 # 设置最大行宽为 80 个字符 +tab_spaces = 4 # 设置缩进宽度为 4 个空格 +edition = "2024" # 设置 Rust 版本(根据实际项目版本进行调整) +use_small_heuristics = "Max" # 设置换行策略 +newline_style = "Auto" # 设置换行符风格,根据平台自动选择 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index fac5529..85bd069 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] -profile = "default" -channel = "nightly-2023-12-27" +profile = "minimal" +channel = "stable" +components = [ "rustfmt", "clippy" ] diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs deleted file mode 100644 index f52f1c4..0000000 --- a/src/api/auth/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod session; diff --git a/src/api/auth/session.rs b/src/api/auth/session.rs deleted file mode 100644 index 60aa621..0000000 --- a/src/api/auth/session.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::infra::result::WrapResult; -use anyhow::{anyhow, Result}; -use home::home_dir; -use std::fs; -use std::fs::{metadata, remove_file, File}; -use std::io::Write; -use std::path::{Path, PathBuf}; - -fn remove_pat(path: &Path) -> Result<()> { - if metadata(path).is_ok() { - remove_file(path)?; - } - ().wrap_ok() -} - -fn save_pat(pat: &str, path: &Path) -> Result<()> { - let mut file = File::create(path)?; - file.write_all(pat.as_bytes())?; - ().wrap_ok() -} - -fn get_cfg_path() -> Result { - let home = home_dir().ok_or_else(|| anyhow!("Can not get home dir"))?; - home.join(".cnbrc").wrap_ok() -} - -pub fn login(pat: &str) -> Result { - let cfg_path = get_cfg_path()?; - let cfg_path = cfg_path.as_path(); - - remove_pat(cfg_path)?; - save_pat(pat, cfg_path)?; - - cfg_path.to_owned().wrap_ok() -} - -pub fn logout() -> Result { - let cfg_path = get_cfg_path()?; - let cfg_path = cfg_path.as_path(); - - remove_pat(cfg_path)?; - - cfg_path.to_owned().wrap_ok() -} - -pub fn get_pat() -> Result { - let cfg_path = get_cfg_path()?; - let cfg_path = cfg_path.as_path(); - - fs::read_to_string(cfg_path) - .map_err(|e| anyhow!("Can not read {:?}, please login first ({})", cfg_path, e)) -} diff --git a/src/api/fav.rs b/src/api/fav.rs new file mode 100644 index 0000000..5fd1919 --- /dev/null +++ b/src/api/fav.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use reqwest::{Client, Response}; +use serde::Serialize; + +use crate::{api::urls::OPENAPI, models::fav::FavInfo, tools::IntoAnyhowResult}; + +pub async fn list_bookmarks( + c: &Client, + page: impl Serialize + Send + Sync, +) -> Result> { + raw_list_bookmarks(c, page) + .await? + .json() + .await + .into_anyhow_result() +} + +pub async fn raw_list_bookmarks( + c: &Client, + page: impl Serialize + Send + Sync, +) -> Result { + let url = format!("{}/{}", OPENAPI, "Bookmarks"); + c.get(url).query(&page).send().await.into_anyhow_result() +} diff --git a/src/api/fav/get_list.rs b/src/api/fav/get_list.rs deleted file mode 100644 index 82f2120..0000000 --- a/src/api/fav/get_list.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::api::fav::Fav; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::infra::vec::VecExt; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::ops::ControlFlow; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct FavEntry { - pub title: String, - #[serde(rename = "LinkUrl")] - pub url: String, - pub summary: String, - pub tags: Vec, - #[serde(rename = "DateAdded")] - pub create_time: String, -} - -impl Fav { - pub async fn get_list(&self, skip: usize, take: usize) -> Result> { - let client = &reqwest::Client::new(); - - let range = (skip + 1)..=(skip + take); - let cf = range - .map(|i| async move { - let req = { - let url = openapi!("/bookmarks"); - let query = [("pageIndex", i), ("pageSize", 1)]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let body = body_or_err(resp).await?; - - json::deserialize::>(&body)? - .pop() - .wrap_ok::() - }) - .join_all() - .await - .into_iter() - .try_fold(vec![], |acc, it| match it { - Ok(maybe) => match maybe { - Some(entry) => ControlFlow::Continue(acc.chain_push(entry)), - None => ControlFlow::Break(Ok(acc)), - }, - Err(e) => ControlFlow::Break(Err(e)), - }); - - match cf { - ControlFlow::Continue(vec) => Ok(vec), - ControlFlow::Break(result) => result, - } - } -} diff --git a/src/api/fav/mod.rs b/src/api/fav/mod.rs deleted file mode 100644 index a1e5984..0000000 --- a/src/api/fav/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod get_list; - -// Aka cnblogs wz -pub struct Fav { - pat: String, -} - -impl Fav { - pub const fn new(pat: String) -> Self { - Self { pat } - } -} diff --git a/src/api/ing.rs b/src/api/ing.rs new file mode 100644 index 0000000..24cae8f --- /dev/null +++ b/src/api/ing.rs @@ -0,0 +1,109 @@ +//! +//! 闪存API +//! +//! raw_*类型直接返回anyhow::Result,提供原始的reponse,供自定义处理逻辑。 +//! + +use anyhow::{Ok, Result}; +use reqwest::{Client, Response}; +use serde::Serialize; +use serde_json::json; + +use crate::{ + api::urls::{COMMENTS_PATH, STATUS}, + models::ing::{IngComment, IngDetail, IngInfo}, + tools::IntoAnyhowResult, +}; + +/// 创建闪存 +/// +/// content: +pub async fn create_statuses( + c: &Client, + content: impl Serialize + Send + Sync, +) -> Result { + raw_create_status(c, content).await +} + +pub async fn get_status_with_comment(c: &Client, status: IngInfo) -> Result { + if status.comment_count < 1 { + return Ok(IngDetail { + status, + comments: vec![], + }); + } + let comments = list_comments(c, status.id).await?; + Ok(IngDetail { status, comments }) +} + +pub async fn get_status(c: &Client, id: u64) -> Result { + raw_get_status(c, id) + .await? + .json() + .await + .into_anyhow_result() +} + +pub async fn list_comments(c: &Client, id: u64) -> Result> { + raw_list_comments(c, id) + .await? + .json() + .await + .into_anyhow_result() +} + +pub async fn list_statuses( + c: &Client, + path: &str, + params: impl Serialize + Send + Sync, +) -> Result> { + let resp = raw_list_statuses(c, path, params).await?; + resp.error_for_status()?.json().await.into_anyhow_result() +} + +pub async fn raw_list_comments(c: &Client, id: u64) -> Result { + let url = format!("{}{}/{}", STATUS, id, COMMENTS_PATH); + c.get(url).send().await.into_anyhow_result() +} + +pub async fn raw_list_statuses( + c: &Client, + path: &str, + params: impl Serialize + Send + Sync, +) -> Result { + let url = format!("{}@{}", STATUS, path); + c.get(url).query(¶ms).send().await.into_anyhow_result() +} + +pub async fn raw_create_status( + c: &Client, + content: impl Serialize + Send + Sync, +) -> Result { + let url = STATUS.to_string(); + c.post(url).json(&content).send().await.into_anyhow_result() +} + +pub async fn raw_create_comment(c: &Client, id: u64, content: String) -> Result { + let url = format!("{}{}/{}", STATUS, id, COMMENTS_PATH); + let res = json!({"content": content}); + c.post(url).json(&res).send().await.into_anyhow_result() +} + +pub async fn raw_delete_status(c: &Client, id: u64) -> Result { + let url = format!("{}{}", STATUS, id); + c.delete(url).send().await.into_anyhow_result() +} + +pub async fn raw_delete_status_comment( + c: &Client, + status_id: u64, + comment_id: u64, +) -> Result { + let url = format!("{}{}/{}/{}", STATUS, status_id, COMMENTS_PATH, comment_id); + c.delete(url).send().await.into_anyhow_result() +} + +pub async fn raw_get_status(c: &Client, id: u64) -> Result { + let url = format!("{}{}", STATUS, id); + c.get(url).send().await.into_anyhow_result() +} diff --git a/src/api/ing/comment.rs b/src/api/ing/comment.rs deleted file mode 100644 index 53f8395..0000000 --- a/src/api/ing/comment.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::api::ing::Ing; -use crate::infra::http::{unit_or_err, RequestBuilderExt}; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -impl Ing { - pub async fn comment( - &self, - ing_id: usize, - content: String, - reply_to: Option, - parent_comment_id: Option, - ) -> Result<()> { - let client = reqwest::Client::new(); - - let req = { - let url = openapi!("/statuses/{}/comments", ing_id); - let body = { - #[serde_with::skip_serializing_none] - #[derive(Clone, Debug, Serialize, Deserialize)] - struct Body { - #[serde(rename(serialize = "replyTo"))] - reply_to: Option, - #[serde(rename(serialize = "parentCommentId"))] - parent_comment_id: Option, - content: String, - } - Body { - reply_to, - parent_comment_id, - content, - } - }; - client.post(url).json(&body).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - unit_or_err(resp).await - } -} diff --git a/src/api/ing/get_comment_list.rs b/src/api/ing/get_comment_list.rs deleted file mode 100644 index 59ac871..0000000 --- a/src/api/ing/get_comment_list.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::api::ing::Ing; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct IngCommentEntry { - pub id: usize, - pub content: String, - #[serde(rename = "DateAdded")] - pub create_time: String, - pub status_id: usize, - pub user_alias: String, - #[serde(rename = "UserDisplayName")] - pub user_name: String, - pub user_icon_url: String, - pub user_id: usize, - pub user_guid: String, -} - -impl Ing { - pub async fn get_comment_list(&self, ing_id: usize) -> Result> { - let client = reqwest::Client::new(); - - let req = { - let url = openapi!("/statuses/{}/comments", ing_id); - client.get(url).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - let entry_vec = { - let body = body_or_err(resp).await?; - json::deserialize::>(&body)? - }; - - entry_vec.wrap_ok() - } -} diff --git a/src/api/ing/get_list.rs b/src/api/ing/get_list.rs deleted file mode 100644 index 5eca27a..0000000 --- a/src/api/ing/get_list.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::api::ing::{Ing, IngSendFrom, IngType}; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::infra::vec::VecExt; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::ops::ControlFlow; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct IngEntry { - pub id: usize, - pub content: String, - pub is_private: bool, - pub is_lucky: bool, - pub comment_count: usize, - #[serde(rename = "DateAdded")] - pub create_time: String, - pub user_alias: String, - #[serde(rename = "UserDisplayName")] - pub user_name: String, - pub user_icon_url: String, - pub user_id: usize, - pub user_guid: String, - pub send_from: IngSendFrom, - #[serde(rename = "Icons")] - pub icons: String, -} - -impl Ing { - pub async fn get_list( - &self, - skip: usize, - take: usize, - ing_type: &IngType, - ) -> Result> { - let client = &reqwest::Client::new(); - - let range = (skip + 1)..=(skip + take); - let cf = range - .map(|i| async move { - let req = { - let url = openapi!("/statuses/@{}", ing_type.clone() as usize); - let query = [("pageIndex", i), ("pageSize", 1)]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let body = body_or_err(resp).await?; - - json::deserialize::>(&body)?.pop().wrap_ok() - }) - .join_all() - .await - .into_iter() - .try_fold(vec![], |acc, it| match it { - Ok(maybe) => match maybe { - Some(entry) => ControlFlow::Continue(acc.chain_push(entry)), - None => ControlFlow::Break(Ok(acc)), - }, - Err(e) => ControlFlow::Break(Err(e)), - }); - - match cf { - ControlFlow::Continue(vec) => Ok(vec), - ControlFlow::Break(result) => result, - } - } -} diff --git a/src/api/ing/mod.rs b/src/api/ing/mod.rs deleted file mode 100644 index 56343b3..0000000 --- a/src/api/ing/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -pub mod comment; -pub mod publish; - -use clap::{Parser, ValueEnum}; -use lazy_static::lazy_static; -use regex::Regex; -use serde_repr::{Deserialize_repr, Serialize_repr}; - -pub mod get_comment_list; -pub mod get_list; - -pub struct Ing { - pat: String, -} - -impl Ing { - pub const fn new(pat: String) -> Self { - Self { pat } - } -} - -#[derive(Clone, Debug, Parser, ValueEnum)] -pub enum IngType { - Follow = 1, - Myself = 4, - Public = 5, - //RecentComment = 6, - MyComment = 7, - //Tag = 10, - //Comment = 13, - //Mention = 14, -} - -#[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum IngSendFrom { - None = 0, - Ms = 1, - GTalk = 2, - Qq = 3, - Sms = 5, - CellPhone = 6, - Web = 8, - VsCode = 9, - Cli = 13, -} - -pub fn ing_star_tag_to_text(tag: &str) -> String { - lazy_static! { - static ref REGEX: Regex = - Regex::new(r#""#).expect("Invalid regexp"); - } - let caps = REGEX - .captures(tag) - .unwrap_or_else(|| panic!("No captures for: {}", tag)); - let text = caps.get(1).expect("No capture at index 1").as_str(); - text.to_string() -} - -pub fn fmt_content(content: &str) -> String { - lazy_static! { - static ref REGEX: Regex = - Regex::new(r#"(@.*?)"#) - .expect("Invalid regexp"); - } - REGEX.captures(content).map_or_else( - || content.to_owned(), - |caps| { - let at_user = caps.get(1).expect("No capture at index 1").as_str(); - REGEX.replace(content, at_user).to_string() - }, - ) -} - -pub fn rm_ing_at_user_tag(text: &str) -> String { - lazy_static! { - static ref REGEX: Regex = - Regex::new(r#"(@.*?):"#) - .expect("Invalid regexp"); - } - REGEX.replace(text, "").to_string() -} - -pub fn get_ing_at_user_tag_text(text: &str) -> String { - lazy_static! { - static ref REGEX: Regex = - Regex::new(r#"@(.*?):"#) - .expect("Invalid regexp"); - } - REGEX.captures(text).map_or_else(String::new, |caps| { - caps.get(1) - .expect("No capture at index 1") - .as_str() - .to_string() - }) -} diff --git a/src/api/ing/publish.rs b/src/api/ing/publish.rs deleted file mode 100644 index d3efdd0..0000000 --- a/src/api/ing/publish.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::api::ing::{Ing, IngSendFrom}; -use crate::infra::http::{unit_or_err, RequestBuilderExt}; -use crate::openapi; -use anyhow::Result; -use serde_json::json; - -impl Ing { - pub async fn publish(&self, content: &str) -> Result<()> { - let client = reqwest::Client::new(); - - let req = { - let url = openapi!("/statuses"); - let body = json!({ - "content": content, - "isPrivate": false, - "clientType": IngSendFrom::Cli, - }); - - client.post(url).json(&body).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - unit_or_err(resp).await - } -} diff --git a/src/api/mod.rs b/src/api/mod.rs index 94018dd..1f9d008 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,33 +1,14 @@ -pub mod auth; +//! +//! API +//! +//! 负责HTTP的调用,返回对应的response +//! + pub mod fav; pub mod ing; pub mod news; pub mod post; +pub mod urls; pub mod user; -pub const BLOG_BACKEND: &str = "https://i.cnblogs.com/api"; -#[macro_export] -macro_rules! blog_backend { - ($($arg:tt)*) => {{ - use $crate::api::BLOG_BACKEND; - format!("{}{}", BLOG_BACKEND, format_args!($($arg)*)) - }}; -} - -pub const OPENAPI: &str = "https://api.cnblogs.com/api"; -#[macro_export] -macro_rules! openapi { - ($($arg:tt)*) => {{ - use $crate::api::OPENAPI; - format!("{}{}", OPENAPI, format_args!($($arg)*)) - }}; -} - -pub const OAUTH: &str = "https://oauth.cnblogs.com"; -#[macro_export] -macro_rules! oauth { - ($($arg:tt)*) => {{ - use $crate::api::OAUTH; - format!("{}{}", OAUTH, format_args!($($arg)*)) - }}; -} +pub struct Client {} diff --git a/src/api/news.rs b/src/api/news.rs new file mode 100644 index 0000000..543d898 --- /dev/null +++ b/src/api/news.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use reqwest::{Client, Response}; +use serde::Serialize; + +use crate::{api::urls::OPENAPI, models::news::NewsInfo, tools::IntoAnyhowResult}; + +pub async fn list_news(c: &Client, page: impl Serialize + Send + Sync) -> Result> { + raw_list_news(c, page) + .await? + .json() + .await + .into_anyhow_result() +} + +pub async fn raw_list_news(c: &Client, page: impl Serialize + Send + Sync) -> Result { + let url = format!("{}/{}", OPENAPI, "NewsItems"); + c.get(url).query(&page).send().await.into_anyhow_result() +} diff --git a/src/api/news/get_body.rs b/src/api/news/get_body.rs deleted file mode 100644 index 82a2f35..0000000 --- a/src/api/news/get_body.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::api::news::News; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use anyhow::Result; - -impl News { - pub async fn get_body(&self, id: usize) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("newsitems/{}/body", id); - client.get(url).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - body_or_err(resp).await - } -} diff --git a/src/api/news/get_list.rs b/src/api/news/get_list.rs deleted file mode 100644 index 23ff0b3..0000000 --- a/src/api/news/get_list.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::api::news::News; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct NewsEntry { - pub id: usize, - pub title: String, - pub summary: String, - pub topic_id: usize, - #[serde(rename = "TopicIcon")] - pub topic_icon_url: Option, - pub view_count: usize, - pub comment_count: usize, - pub digg_count: usize, - #[serde(rename = "DateAdded")] - pub create_time: String, -} - -impl News { - pub async fn get_list(&self, skip: usize, take: usize) -> Result> { - let client = &reqwest::Client::new(); - - let range = (skip + 1)..=(skip + take); - range - .map(|i| async move { - let req = { - let url = openapi!("/newsitems"); - let query = [("pageIndex", i), ("pageSize", 1)]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let entry = { - let body = body_or_err(resp).await?; - let [entry, ..] = json::deserialize::<[NewsEntry; 1]>(&body)?; - entry - }; - - entry.wrap_ok::() - }) - .join_all() - .await - .into_iter() - .collect() - } -} diff --git a/src/api/news/mod.rs b/src/api/news/mod.rs deleted file mode 100644 index 24c04b2..0000000 --- a/src/api/news/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod get_body; -pub mod get_list; - -pub struct News { - pat: String, -} - -impl News { - pub const fn new(pat: String) -> Self { - Self { pat } - } -} diff --git a/src/api/post.rs b/src/api/post.rs new file mode 100644 index 0000000..7b4e4ec --- /dev/null +++ b/src/api/post.rs @@ -0,0 +1,97 @@ +use anyhow::Result; +use reqwest::{Client, Response}; +use serde::Serialize; + +use crate::{ + api::urls::{BLOG_POST_PREFIX, POST_PREFIX}, + models::post::PostInfo, + tools::IntoAnyhowResult, +}; + +pub async fn list_someone_post( + c: &Client, + blog_app: &String, + params: impl Serialize + Send + Sync, +) -> Result> { + // let a = raw_list_someone_post(c, blog_app).await?; + // a.json().await.into_anyhow_result() + raw_list_someone_post(c, blog_app, params) + .await? + .json() + .await + .into_anyhow_result() +} + +pub async fn show_post_detail() {} + +/// 获取指定用户的随笔列表(支持分页) +/// +/// ## 参数 +/// +/// - `c`: 已初始化的HTTP客户端实例 +/// - `blog_app`: 博客用户的唯一标识(博客园中的博客别名) +/// - `params`: 查询参数,pageIndex和pageIndex两个参数,需要能实现serilize。 +/// +/// ## 查询参数要求 +/// +/// 参数应实现 `Serialize` trait,通常包含: +/// - `pageIndex`: u64 - 页码(从0或1开始,取决于API规范) +/// - `pageSize`: u64 - 每页记录数 +pub async fn raw_list_someone_post( + c: &Client, + blog_app: &String, + params: impl Serialize + Send + Sync, +) -> Result { + let url = format!("{}/{}/{}", POST_PREFIX, blog_app, "posts"); + c.get(url).query(¶ms).send().await.into_anyhow_result() +} + +pub async fn raw_show_post(c: &Client, id: u64) -> Result { + let url = format!("{}/{}/body", BLOG_POST_PREFIX, id); + c.get(url).send().await.into_anyhow_result() +} + +/// 获取指定id的随笔评论 +/// +/// ## 参数 +/// +/// - `c`: 已初始化的HTTP客户端实例 +/// - `blog_app`: 博客园的博客名称 +/// - `id`: 随笔ID +pub async fn raw_list_comments( + c: &Client, + blog_app: &String, + id: u64, + params: impl Serialize + Send + Sync, +) -> Result { + c.get(gen_comments_url(blog_app, id)) + .query(¶ms) + .send() + .await + .into_anyhow_result() +} + +pub async fn raw_create_comment( + c: &Client, + blog_app: &String, + id: u64, + params: impl Serialize + Send + Sync, +) -> Result { + c.post(gen_comments_url(blog_app, id)) + .json(¶ms) + .send() + .await + .into_anyhow_result() +} + +// pub async fn raw_post_detail(c: &Client, id: u64) -> Result { +// let url = format!("{}/{}", BLOG_BACKEND_POST, id); +// c.get(url).send().await.into_anyhow_result() +// } + +fn gen_comments_url(blog_app: &String, id: u64) -> String { + format!( + "{}/{}/{}/{}/{}", + POST_PREFIX, blog_app, "posts", id, "comments" + ) +} diff --git a/src/api/post/create.rs b/src/api/post/create.rs deleted file mode 100644 index 437dd34..0000000 --- a/src/api/post/create.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::{json, Value}; - -impl Post { - pub async fn create(&self, title: &str, body: &str, publish: bool) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts"); - let body = json!({ - "postType": 1, - "title": title, - "postBody": body, - "isPublished": publish, - "displayOnHomePage": true - }); - client.post(url).json(&body).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let id = { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?; - json["id"].as_u64().expect("as_u64 failed for `id`") as usize - }; - - id.wrap_ok() - } -} diff --git a/src/api/post/del_one.rs b/src/api/post/del_one.rs deleted file mode 100644 index 02b03c6..0000000 --- a/src/api/post/del_one.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{unit_or_err, RequestBuilderExt}; -use anyhow::Result; - -impl Post { - pub async fn del_one(&self, id: usize) -> Result<()> { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts/{}", id); - client.delete(url).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - unit_or_err(resp).await - } -} diff --git a/src/api/post/get_comment_list.rs b/src/api/post/get_comment_list.rs deleted file mode 100644 index e14162f..0000000 --- a/src/api/post/get_comment_list.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::api::post::Post; -use crate::api::user::User; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct PostCommentEntry { - pub id: usize, - #[serde(rename = "Body")] - pub content: String, - #[serde(rename = "Author")] - pub user_name: String, - #[serde(rename = "AuthorUrl")] - pub user_home_url: String, - #[serde(rename = "FaceUrl")] - pub avatar_url: String, - pub floor: usize, - #[serde(rename = "DateAdded")] - pub create_time: String, -} - -impl Post { - pub async fn get_comment_list(&self, post_id: usize) -> Result> { - let blog_app = User::new(self.pat.to_owned()).get_info().await?.blog_app; - let client = reqwest::Client::new(); - - let req = { - let url = openapi!("/blogs/{}/posts/{}/comments", blog_app, post_id); - client.get(url).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - let entry_vec = { - let body = body_or_err(resp).await?; - json::deserialize::>(&body)? - }; - - entry_vec.wrap_ok() - } -} diff --git a/src/api/post/get_count.rs b/src/api/post/get_count.rs deleted file mode 100644 index 3c0e4c2..0000000 --- a/src/api/post/get_count.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::Value; - -impl Post { - pub async fn get_count(&self) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts/list"); - let query = [('t', 1), ('p', 1), ('s', 1)]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let count = { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?; - json["postsCount"] - .as_u64() - .expect("as_u64 failed for `postsCount`") as usize - }; - - count.wrap_ok() - } -} diff --git a/src/api/post/get_meta_list.rs b/src/api/post/get_meta_list.rs deleted file mode 100644 index befa829..0000000 --- a/src/api/post/get_meta_list.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::api::post::get_one::PostEntry; -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::Value; - -/* -Fields only available over blog_backend!("/posts/list?{}", query): - aggCount: number - feedBackCount: number - isInSiteCandidate: boolean - isInSiteHome: boolean - postConfig: number - viewCount: number - webCount: number -*/ - -impl Post { - pub async fn get_meta_list(&self, skip: usize, take: usize) -> Result<(Vec, usize)> { - // WRN: - // This impl has low performance but robust - // Current API of blog backend is buggy - // It's not worth to design a more efficient impl - let client = &reqwest::Client::new(); - - // total_count is used for patch the buggy blog backend API - // If index is greater than the max page index, API will still return the last page - let total_count = self.get_count().await?; - - let range = (skip + 1)..=(skip + take).min(total_count); - let vec = range - .map(|i| async move { - let req = { - let url = blog_backend!("/posts/list"); - let query = [('t', 1), ('p', i), ('s', 1)]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let entry = { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?["postList"].take(); - - let [entry, ..] = serde_json::from_value::<[PostEntry; 1]>(json)?; - entry - }; - - entry.wrap_ok() - }) - .join_all() - .await - .into_iter() - .collect::>>(); - - (vec?, total_count).wrap_ok() - } -} diff --git a/src/api/post/get_one.rs b/src/api/post/get_one.rs deleted file mode 100644 index 4fedee6..0000000 --- a/src/api/post/get_one.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -// TODO: not elegant -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct PostEntry { - pub id: usize, - pub title: String, - pub url: String, - - #[serde(rename = "datePublished")] - pub create_time: String, - #[serde(rename = "dateUpdated")] - pub modify_time: String, - - pub is_draft: bool, - pub is_pinned: bool, - pub is_published: bool, - - // WRN: - // Limited by the design of blog backend API - // None implies that this filed is not fetched from server yet but DOSE NOT MEAN IT NOT EXIST - #[serde(rename = "feedBackCount")] - pub comment_count: Option, - #[serde(rename = "postBody")] - pub body: Option, - pub tags: Option>, -} - -impl Post { - pub async fn get_one(&self, id: usize) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts/{}", id); - client.get(url).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let entry = { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?["blogPost"].take(); - serde_json::from_value::(json)? - }; - - entry.wrap_ok() - } -} diff --git a/src/api/post/get_one_raw.rs b/src/api/post/get_one_raw.rs deleted file mode 100644 index ebf24e2..0000000 --- a/src/api/post/get_one_raw.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::Value; - -/* -Fields only available over blog_backend!("/posts/{}", id): - postBody: string - categoryIds: [] - collectionIds: [] - inSiteCandidate: boolean - inSiteHome: boolean - siteCategoryId: null - blogTeamIds: [] - displayOnHomePage: boolean - isAllowComments: boolean - includeInMainSyndication: boolean - isOnlyForRegisterUser: boolean - isUpdateDateAdded: boolean - description: string - featuredImage: null - tags: [] - password: null - autoDesc: string - changePostType: boolean - blogId: number - author: string - removeScript: boolean - clientInfo: null - changeCreatedTime: boolean - canChangeCreatedTime: boolean - isContributeToImpressiveBugActivity: boolean - usingEditorId: null - sourceUrl: null - -Fields available over blog_backend!("/posts/{}", id) and blog_backend!("/posts/list?{}", query): - id: number - postType: PostType - accessPermission: AccessPermission - title: string - url: string - entryName: null - datePublished: string - dateUpdated: string - isMarkdown: boolean - isDraft: boolean - isPinned: boolean - isPublished: boolean -*/ - -impl Post { - /** - Get raw json from remote - - Use this while it's hard to deserialize to struct - **/ - pub async fn get_one_raw(&self, id: usize) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts/{}", id); - client.get(url).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let mut json = { - let body = body_or_err(resp).await?; - serde_json::from_str::(&body) - }?; - - json["blogPost"].take().wrap_ok() - } -} diff --git a/src/api/post/mod.rs b/src/api/post/mod.rs deleted file mode 100644 index e5cf72a..0000000 --- a/src/api/post/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod create; -pub mod del_one; -pub mod get_comment_list; -pub mod get_count; -pub mod get_meta_list; -pub mod get_one; -pub mod get_one_raw; -pub mod search_self; -pub mod search_site; -pub mod update; - -pub struct Post { - pat: String, -} - -impl Post { - pub const fn new(pat: String) -> Self { - Self { pat } - } -} diff --git a/src/api/post/search.rs b/src/api/post/search.rs deleted file mode 100644 index baff137..0000000 --- a/src/api/post/search.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::Value; -use std::collections::HashSet; -use std::iter; - -impl Post { - pub async fn search( - &self, - skip: usize, - take: usize, - keyword: &str, - ) -> Result<(Vec, usize)> { - let client = &reqwest::Client::new(); - - // total_count is used for patch the buggy blog backend API - // If index is greater than the max page index, API will still return the last page - let total_count = { - let req = { - let url = blog_backend!("/posts/list"); - let query = [ - ("t", 1.to_string()), - ("p", 1.to_string()), - ("s", 1.to_string()), - ("search", keyword.to_string()), - ]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - // total_count - { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?; - json["postsCount"] - .as_u64() - .expect("as_u64 failed for `postsCount`") as usize - } - }; - - let range = (skip + 1)..=(skip + take).min(total_count); - let id_list = range - .map(|i| async move { - let req = { - let url = blog_backend!("/posts/list"); - let query = [ - ("t", 1.to_string()), - ("p", i.to_string()), - ("s", 1.to_string()), - ("search", keyword.to_string()), - ]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - let id_list = { - let body = body_or_err(resp).await?; - let mut json = json::deserialize::(&body)?; - let post_id = { - let json = json["postList"].take(); - let [post, ..] = serde_json::from_value::<[Value; 1]>(json)?; - post["id"].as_u64().expect("as_u64 failed for `id`") as usize - }; - let zzk_post_id_list = { - let json = json["zzkSearchResult"]["postIds"].take(); - serde_json::from_value::>(json) - }?; - - zzk_post_id_list - .into_iter() - .chain(iter::once(post_id)) - .collect::>() - }; - - id_list.wrap_ok::() - }) - .join_all() - .await - .into_iter() - .collect::>>()? - .into_iter() - .flatten() - .collect::>() - .into_iter() - .collect::>(); - - (id_list, total_count).wrap_ok() - } -} diff --git a/src/api/post/search_self.rs b/src/api/post/search_self.rs deleted file mode 100644 index b48bb59..0000000 --- a/src/api/post/search_self.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::Value; -use std::collections::HashSet; -use std::iter; - -impl Post { - pub async fn search_self( - &self, - skip: usize, - take: usize, - keyword: &str, - ) -> Result<(Vec, usize)> { - let client = &reqwest::Client::new(); - - // total_count is used for patch the buggy blog backend API - // If index is greater than the max page index, API will still return the last page - let total_count = { - let req = { - let url = blog_backend!("/posts/list"); - let query = [ - ("t", 1.to_string()), - ("p", 1.to_string()), - ("s", 1.to_string()), - ("search", keyword.to_string()), - ]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - // total_count - { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?; - json["postsCount"] - .as_u64() - .expect("as_u64 failed for `postsCount`") as usize - } - }; - - let range = (skip + 1)..=(skip + take).min(total_count); - let id_list = range - .map(|i| async move { - let req = { - let url = blog_backend!("/posts/list"); - let query = [ - ("t", 1.to_string()), - ("p", i.to_string()), - ("s", 1.to_string()), - ("search", keyword.to_string()), - ]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - let id_list = { - let body = body_or_err(resp).await?; - let mut json = json::deserialize::(&body)?; - let post_id = { - let json = json["postList"].take(); - let [post, ..] = serde_json::from_value::<[Value; 1]>(json)?; - post["id"].as_u64().expect("as_u64 failed for `id`") as usize - }; - let zzk_post_id_list = { - let json = json["zzkSearchResult"]["postIds"].take(); - serde_json::from_value::>(json) - }?; - - zzk_post_id_list - .into_iter() - .chain(iter::once(post_id)) - .collect::>() - }; - - id_list.wrap_ok::() - }) - .join_all() - .await - .into_iter() - .collect::>>()? - .into_iter() - .flatten() - .collect::>() - .into_iter() - .collect::>(); - - (id_list, total_count).wrap_ok() - } -} diff --git a/src/api/post/search_site.rs b/src/api/post/search_site.rs deleted file mode 100644 index fe02b48..0000000 --- a/src/api/post/search_site.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::api::post::Post; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::iter::IntoIteratorExt; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::ops::Range; - -// HACK: -// Convert skip and take to page index range while page size is 10 -fn get_page_index_range(skip: usize, take: usize) -> Range { - let page_size = 10; - let start_index = skip / page_size + 1; - let end_index = if take == 0 { - 0 - } else { - ((take + skip) as f64 / page_size as f64).ceil() as usize - }; - start_index..end_index + 1 -} - -// HACK: -// Convert skip and take to a range to slice the vec while page size is 10 -const fn get_slice_range(skip: usize, take: usize) -> Range { - let skip_left = skip - (skip / 10) * 10; - skip_left..skip_left + take -} - -#[test] -fn test_get_page_index_range_and_get_slice_range() { - fn f(skip: usize, take: usize) -> (Range, Range) { - ( - get_page_index_range(skip, take), - get_slice_range(skip, take), - ) - } - - assert_eq!(f(0, 00), (1..1, 0..00)); - assert_eq!(f(0, 01), (1..2, 0..01)); - assert_eq!(f(0, 09), (1..2, 0..09)); - assert_eq!(f(0, 10), (1..2, 0..10)); - assert_eq!(f(0, 11), (1..3, 0..11)); - assert_eq!(f(0, 19), (1..3, 0..19)); - assert_eq!(f(0, 20), (1..3, 0..20)); - assert_eq!(f(0, 21), (1..4, 0..21)); - - assert_eq!(f(1, 00), (1..1, 1..01)); - assert_eq!(f(1, 01), (1..2, 1..02)); - assert_eq!(f(1, 09), (1..2, 1..10)); - assert_eq!(f(1, 10), (1..3, 1..11)); - assert_eq!(f(1, 11), (1..3, 1..12)); - assert_eq!(f(1, 19), (1..3, 1..20)); - assert_eq!(f(1, 20), (1..4, 1..21)); - assert_eq!(f(1, 21), (1..4, 1..22)); - assert_eq!(f(1, 29), (1..4, 1..30)); - - assert_eq!(f(9, 00), (1..1, 9..09)); - assert_eq!(f(9, 01), (1..2, 9..10)); - assert_eq!(f(9, 09), (1..3, 9..18)); - assert_eq!(f(9, 10), (1..3, 9..19)); - assert_eq!(f(9, 11), (1..3, 9..20)); - assert_eq!(f(9, 19), (1..4, 9..28)); - assert_eq!(f(9, 20), (1..4, 9..29)); - assert_eq!(f(9, 21), (1..4, 9..30)); - assert_eq!(f(9, 29), (1..5, 9..38)); - - assert_eq!(f(10, 00), (2..1, 0..00)); - assert_eq!(f(10, 01), (2..3, 0..01)); - assert_eq!(f(10, 09), (2..3, 0..09)); - assert_eq!(f(10, 10), (2..3, 0..10)); - assert_eq!(f(10, 11), (2..4, 0..11)); - assert_eq!(f(10, 19), (2..4, 0..19)); - assert_eq!(f(10, 20), (2..4, 0..20)); - assert_eq!(f(10, 21), (2..5, 0..21)); -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SearchResultEntry { - #[serde(rename = "Title")] - pub title: String, - #[serde(rename = "Content")] - pub summary: String, - - #[serde(rename = "UserName")] - pub user_name: String, - - #[serde(rename = "VoteTimes")] - pub vote_count: usize, - #[serde(rename = "ViewTimes")] - pub view_count: usize, - #[serde(rename = "CommentTimes")] - pub comment_count: usize, - - #[serde(rename = "PublishTime")] - pub create_time: String, - #[serde(rename = "Uri")] - pub url: String, -} - -impl Post { - pub async fn search_site( - &self, - skip: usize, - take: usize, - keyword: &str, - ) -> Result> { - let client = &reqwest::Client::new(); - - let slice_range = get_slice_range(skip, take); - - let entry_vec = { - let entry_vec = get_page_index_range(skip, take) - .map(|i| async move { - let req = { - let url = openapi!("/zzkdocuments/blog"); - let query = [ - ("pageIndex", i.to_string()), - ("keyWords", keyword.to_string()), - ]; - client.get(url).query(&query).pat_auth(&self.pat) - }; - let resp = req.send().await?; - - let body = body_or_err(resp).await?; - json::deserialize::>(&body) - }) - .join_all() - .await - .into_iter() - .collect::>>>()? - .concat(); - - entry_vec - .into_iter() - .enumerate() - .filter(|(i, _)| slice_range.contains(i)) - .map(|(_, entry)| entry) - .collect::>() - }; - - entry_vec.wrap_ok() - } -} diff --git a/src/api/post/update.rs b/src/api/post/update.rs deleted file mode 100644 index 79e689d..0000000 --- a/src/api/post/update.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::api::post::Post; -use crate::blog_backend; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use anyhow::Result; -use serde_json::{json, Value}; - -impl Post { - pub async fn update( - &self, - id: usize, - title: &Option, - body: &Option, - publish: &Option, - ) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = blog_backend!("/posts"); - - let json = { - let mut json = self.get_one_raw(id).await?; - if let Some(title) = title { - json["title"] = json!(title) - } - if let Some(body) = body { - json["postBody"] = json!(body) - } - if let Some(publish) = publish { - json["isPublished"] = json!(publish) - } - json - }; - - client.post(url).json(&json).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let id = { - let body = body_or_err(resp).await?; - let json = json::deserialize::(&body)?; - json["id"].as_u64().expect("as_u64 failed for `id`") as usize - }; - - id.wrap_ok() - } -} diff --git a/src/api/urls.rs b/src/api/urls.rs new file mode 100644 index 0000000..da7b75f --- /dev/null +++ b/src/api/urls.rs @@ -0,0 +1,13 @@ +use const_format::formatcp; + +pub const BLOG_BACKEND: &str = "https://i.cnblogs.com/api"; +pub const OPENAPI: &str = "https://api.cnblogs.com/api"; +pub const OAUTH: &str = "https://oauth.cnblogs.com"; + +pub const USER: &str = formatcp!("{}/users", OPENAPI); +pub const STATUS: &str = formatcp!("{}/statuses/", OPENAPI); +pub const COMMENTS_PATH: &str = "comments"; + +pub const POST_PREFIX: &str = formatcp!("{}/blogs", OPENAPI); +pub const BLOG_POST_PREFIX: &str = formatcp!("{}/blogposts", OPENAPI); +pub const BLOG_BACKEND_POST: &str = formatcp!("{}/potst", BLOG_BACKEND); diff --git a/src/api/user.rs b/src/api/user.rs new file mode 100644 index 0000000..a0732ba --- /dev/null +++ b/src/api/user.rs @@ -0,0 +1,12 @@ +use anyhow::Result; +use reqwest::{Client, Response}; + +use crate::{api::urls, models::user::UserInfo, tools::IntoAnyhowResult}; + +pub async fn raw_user_info(c: &Client) -> Result { + c.get(urls::USER).send().await.into_anyhow_result() +} + +pub async fn user_info(c: &Client) -> Result { + raw_user_info(c).await?.json().await.into_anyhow_result() +} diff --git a/src/api/user/info.rs b/src/api/user/info.rs deleted file mode 100644 index 19df873..0000000 --- a/src/api/user/info.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::api::user::User; -use crate::infra::http::{body_or_err, RequestBuilderExt}; -use crate::infra::json; -use crate::infra::result::WrapResult; -use crate::openapi; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "PascalCase")] -pub struct UserInfo { - pub user_id: String, - #[serde(rename = "SpaceUserID")] - pub space_user_id: usize, - pub blog_id: usize, - pub display_name: String, - pub face: String, - pub avatar: String, - pub seniority: String, - pub blog_app: String, - pub following_count: usize, - #[serde(rename = "FollowerCount")] - pub followers_count: usize, - pub is_vip: bool, - pub joined: String, -} - -impl User { - pub async fn get_info(&self) -> Result { - let client = reqwest::Client::new(); - - let req = { - let url = openapi!("/users"); - client.get(url).pat_auth(&self.pat) - }; - - let resp = req.send().await?; - - let user_info = { - let body = body_or_err(resp).await?; - json::deserialize::(&body)? - }; - - user_info.wrap_ok() - } -} diff --git a/src/api/user/mod.rs b/src/api/user/mod.rs deleted file mode 100644 index 97b22a5..0000000 --- a/src/api/user/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod info; - -pub struct User { - pat: String, -} - -impl User { - pub const fn new(pat: String) -> Self { - Self { pat } - } -} diff --git a/src/args/cmd/fav.rs b/src/args/cmd/fav.rs deleted file mode 100644 index 420755b..0000000 --- a/src/args/cmd/fav.rs +++ /dev/null @@ -1,12 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct Opt { - #[arg(verbatim_doc_comment)] - /// Show favorite list, order by time in DESC - /// Example: cnb fav --list - #[arg(long)] - #[arg(short = 'l')] - pub list: bool, -} diff --git a/src/args/cmd/ing.rs b/src/args/cmd/ing.rs deleted file mode 100644 index 974c6bc..0000000 --- a/src/args/cmd/ing.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::api::ing::IngType; -use clap::{Parser, Subcommand}; - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct Opt { - #[command(subcommand)] - pub cmd: Option, - - #[arg(verbatim_doc_comment)] - /// Publish ing with specific content - /// Example: cnb ing --publish 'Hello world' - /// The visibility of ing is public - /// * - #[arg(long)] - #[arg(short = 'p')] - #[arg(visible_alias = "pub")] - #[arg(value_name = "CONTENT")] - pub publish: Option, - - #[arg(verbatim_doc_comment)] - /// Comment ing with specific content - /// Example: cnb --id 114514 ing --comment 'Hello world' - /// You should also specify the id of the ing via --id - #[arg(long)] - #[arg(short = 'c')] - #[arg(value_name = "CONTENT")] - pub comment: Option, -} - -#[derive(Debug, Subcommand)] -#[non_exhaustive] -pub enum Cmd { - #[clap(verbatim_doc_comment)] - /// Show ing list, order by time in DESC - /// Example: cnb ing list - /// * - #[clap(visible_alias = "l")] - List { - #[arg(verbatim_doc_comment)] - /// Ing type to show - /// Example: cnb ing list --type myself - /// * - #[arg(long)] - #[arg(value_name = "TYPE")] - #[arg(default_value = "public")] - r#type: Option, - - #[arg(verbatim_doc_comment)] - /// Align ing content to user name automatically - /// Example: cnb ing list --align - #[arg(long)] - #[arg(value_name = "BOOL")] - #[arg(default_value_t = true)] - align: bool, - }, -} diff --git a/src/args/cmd/mod.rs b/src/args/cmd/mod.rs deleted file mode 100644 index e112102..0000000 --- a/src/args/cmd/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub mod fav; -pub mod ing; -pub mod news; -pub mod post; -pub mod user; - -use clap::Subcommand; - -#[derive(Debug, Subcommand)] -#[non_exhaustive] -pub enum Cmd { - /// User operations - #[clap(visible_alias = "u")] - User(user::Opt), - /// Ing operations - #[clap(visible_alias = "i")] - Ing(ing::Opt), - /// Post operations - #[clap(visible_alias = "p")] - Post(post::Opt), - /// News operations - #[clap(visible_alias = "n")] - News(news::Opt), - /// Favorite operations - #[clap(visible_alias = "f")] - Fav(fav::Opt), -} diff --git a/src/args/cmd/news.rs b/src/args/cmd/news.rs deleted file mode 100644 index f6aa52d..0000000 --- a/src/args/cmd/news.rs +++ /dev/null @@ -1,12 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct Opt { - #[arg(verbatim_doc_comment)] - /// Show news list, order by time in DESC - /// Example: cnb news --list - #[arg(long)] - #[arg(short = 'l')] - pub list: bool, -} diff --git a/src/args/cmd/post.rs b/src/args/cmd/post.rs deleted file mode 100644 index a0ee3b9..0000000 --- a/src/args/cmd/post.rs +++ /dev/null @@ -1,151 +0,0 @@ -use clap::{Parser, Subcommand}; - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct Opt { - #[clap(verbatim_doc_comment)] - /// Show title and content of a specific post - /// Example: cnb --id 114514 post --show - /// You should also specify the id of the post via --id - #[arg(long)] - #[arg(short = 's')] - pub show: bool, - - #[arg(verbatim_doc_comment)] - /// Show metadata of a specific post - /// Example: cnb --id 114514 post --show-meta - /// You should also specify the id of the post via --id - /// * - #[arg(long)] - #[arg(visible_alias = "sm")] - pub show_meta: bool, - - #[arg(verbatim_doc_comment)] - /// Show comment list of post, order by time in DESC - /// Example: cnb --id 114514 post --show-comment - /// You should also specify the id of the post via --id - /// * - #[arg(long)] - #[arg(visible_alias = "sc")] - pub show_comment: bool, - - #[arg(verbatim_doc_comment)] - /// Show post list, order by time in DESC - /// Example: cnb post --list - /// should in range [0,100] - /// If greater than 100, it will be set to 100 - #[arg(long)] - #[arg(short = 'l')] - pub list: bool, - - #[arg(verbatim_doc_comment)] - /// Delete post - /// Example: cnb --id 114514 post --delete - /// You should also specify the id of the post via --id - /// * - #[arg(long)] - #[arg(visible_alias = "del")] - pub delete: bool, - - #[command(subcommand)] - pub cmd: Option, -} - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct CreateCmd { - #[arg(verbatim_doc_comment)] - /// Set post title - /// Example: cnb post create --title 'Title' --body 'Body' - #[arg(long)] - #[arg(value_name = "TITLE")] - pub title: String, - - #[arg(verbatim_doc_comment)] - /// Set post body - /// Example: cnb post create --title 'Title' --body 'Body' - #[arg(long)] - #[arg(value_name = "BODY")] - pub body: String, - - #[arg(verbatim_doc_comment)] - /// Set post status to publish - /// Example: cnb post create --title 'Title' --body 'Body' --publish - /// * - #[arg(long)] - #[arg(visible_alias = "pub")] - pub publish: bool, -} - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct UpdateCmd { - #[arg(verbatim_doc_comment)] - /// Set post title - /// Example: cnb --id 114514 post update --title 'Title' - #[arg(long)] - #[arg(value_name = "TITLE")] - pub title: Option, - - #[arg(verbatim_doc_comment)] - /// Set post body - /// Example: cnb --id 114514 post update --body 'Body' - #[arg(long)] - #[arg(value_name = "BODY")] - pub body: Option, - - #[arg(verbatim_doc_comment)] - /// Set post publish state - /// Example: cnb --id 114514 post update --publish true - /// * - #[arg(long)] - #[arg(value_name = "BOOL")] - #[arg(visible_alias = "pub")] - pub publish: Option, -} - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct SearchCmd { - #[arg(verbatim_doc_comment)] - /// Search self post - /// Example: cnb post search --self 'Keyword' - #[arg(long)] - #[arg(long = "self")] - #[arg(value_name = "KEYWORD")] - pub self_keyword: Option, - - #[arg(verbatim_doc_comment)] - /// Search site post - /// Example: cnb post search --site 'Keyword' - #[arg(long)] - #[arg(long = "site")] - #[arg(value_name = "KEYWORD")] - pub site_keyword: Option, -} - -#[derive(Debug, Subcommand)] -#[non_exhaustive] -pub enum Cmd { - #[clap(verbatim_doc_comment)] - /// Create post - /// Example: cnb post create --title 'Title' --body 'Body' - /// * - #[clap(visible_alias = "c")] - Create(CreateCmd), - - #[clap(verbatim_doc_comment)] - /// Update post - /// Example: cnb --id 114514 post update --title 'Title' - /// You should also specify the id of the post via --id - /// * - #[clap(visible_alias = "u")] - Update(UpdateCmd), - - #[clap(verbatim_doc_comment)] - /// Search post - /// Example: cnb post search --self 'Keyword' - /// * - #[clap(visible_alias = "s")] - Search(SearchCmd), -} diff --git a/src/args/cmd/user.rs b/src/args/cmd/user.rs deleted file mode 100644 index a38dff4..0000000 --- a/src/args/cmd/user.rs +++ /dev/null @@ -1,27 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct Opt { - #[arg(verbatim_doc_comment)] - /// Login with your personal access token (PAT) - /// Example: cnb user --login 'FOOBARBAZ' - /// PAT will be saved in ~/.cnbrc - /// You can create PAT in https://account.cnblogs.com/tokens - #[arg(long)] - #[arg(value_name = "PAT")] - pub login: Option, - - #[arg(verbatim_doc_comment)] - /// Logout and remove ~/.cnbrc - /// Example: cnb user --logout - #[arg(long)] - pub logout: bool, - - #[arg(verbatim_doc_comment)] - /// Show user info - /// Example: cnb user --info - #[arg(long)] - #[arg(short = 'i')] - pub info: bool, -} diff --git a/src/args/mod.rs b/src/args/mod.rs deleted file mode 100644 index 57855c5..0000000 --- a/src/args/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -pub mod cmd; -pub mod parser; - -use crate::args::cmd::Cmd; -use clap::{Parser, ValueEnum}; - -#[derive(Clone, Debug, Parser, ValueEnum)] -pub enum Style { - Colorful, - Normal, - Json, -} - -#[derive(Clone, Debug, Parser, ValueEnum)] -pub enum TimeStyle { - Friendly, - Normal, -} - -#[derive(Parser, Debug)] -#[non_exhaustive] -pub struct GlobalOpt { - #[arg(verbatim_doc_comment)] - /// Execute with specific PAT - /// Example: cnb --with-pat 'FOOBARBAZ' post --list - /// Your PAT in ~/.cnbrc will be ignored in this execution if it exists - /// Please login if you don't want to input PAT everytime, try 'cnb user --help' for more details - #[arg(long)] - #[arg(value_name = "PAT")] - pub with_pat: Option, - - #[arg(verbatim_doc_comment)] - /// Execute in debug mode, this will print some messages for the developer - /// Example: cnb --debug ing list - /// THIS OPTION IS UNSTABLE FOREVER and any output from it may change in the future - /// You should NEVER rely on the output while you turn this option on - /// * - #[arg(long)] - #[clap(visible_alias = "dbg")] - pub debug: bool, - - #[arg(verbatim_doc_comment)] - /// Configure the output style - /// Example: cnb --style json ing list - /// * - #[arg(long)] - #[arg(value_enum)] - #[arg(hide_possible_values = true)] - #[arg(default_value_t = Style::Colorful)] - #[arg(value_name = "NAME")] - pub style: Style, - - #[arg(verbatim_doc_comment)] - /// Configure the time style - /// Example: cnb --style normal ing list - /// This option does not affect the output of '--style json' - /// * - #[arg(long)] - #[arg(value_enum)] - #[arg(hide_possible_values = true)] - #[arg(default_value_t = TimeStyle::Friendly)] - #[arg(value_name = "NAME")] - pub time_style: TimeStyle, - - #[arg(verbatim_doc_comment)] - /// Fail if error occurred - /// Example: cnb --fail-on-error ing list - /// * - #[arg(long)] - #[clap(visible_alias = "foe")] - #[arg(default_value_t = false)] - pub fail_on_error: bool, - - #[arg(verbatim_doc_comment)] - /// Suppress all output - /// Example: cnb --quiet ing list - /// * - #[arg(long)] - #[clap(visible_alias = "silent")] - #[arg(default_value_t = false)] - pub quiet: bool, -} - -#[derive(Parser, Debug)] -#[command(author, about, long_about = None, version)] -#[non_exhaustive] -pub struct Args { - #[command(subcommand)] - pub cmd: Option, - #[clap(flatten)] - pub global_opt: GlobalOpt, - - #[arg(verbatim_doc_comment)] - /// Provide ID required by other options - /// Example: cnb --id 114514 post --show - #[arg(long)] - pub id: Option, - - #[arg(verbatim_doc_comment)] - /// Reverse list output - /// Example: cnb --rev ing list - #[arg(long)] - pub rev: bool, - - #[arg(verbatim_doc_comment)] - /// Skip items while request list - /// Example: cnb --skip 2 ing list - /// Use this option to save network I/O if some items of the list output are not needed - /// If this option is required but not specified, it will be set to 0 - #[arg(long)] - #[arg(short = 's')] - #[arg(value_name = "LENGTH")] - pub skip: Option, - - #[arg(verbatim_doc_comment)] - /// Take items while request list - /// Example: cnb --take 2 ing list - /// Use this option to save network I/O if only a subset of the list output are required - /// should be in the range [0,100] - /// If is greater than 100, it will be set to 100 - /// If this option is required but not specified, it will be set to 8 - #[arg(long)] - #[arg(short = 't')] - #[arg(value_name = "LENGTH")] - pub take: Option, -} diff --git a/src/args/parser/fav.rs b/src/args/parser/fav.rs deleted file mode 100644 index 71fc9af..0000000 --- a/src/args/parser/fav.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::args::parser::{get_skip, get_take}; -use crate::args::{cmd, Args, Cmd}; -use crate::infra::option::WrapOption; - -pub fn list_fav(args: &Args) -> Option<(usize, usize)> { - match args { - Args { - cmd: Some(Cmd::Fav(cmd::fav::Opt { list: true })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - (skip, take) - } - _ => return None, - } - .wrap_some() -} diff --git a/src/args/parser/ing.rs b/src/args/parser/ing.rs deleted file mode 100644 index 62ed32b..0000000 --- a/src/args/parser/ing.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::api::ing::IngType; -use crate::args::parser::{get_skip, get_take}; -use crate::args::{cmd, Args, Cmd}; -use crate::infra::option::WrapOption; - -pub fn list_ing(args: &Args) -> Option<(usize, usize, IngType, bool)> { - match args { - Args { - cmd: - Some(Cmd::Ing(cmd::ing::Opt { - cmd: Some(cmd::ing::Cmd::List { r#type, align }), - publish: None, - comment: None, - })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - let r#type = r#type.clone().unwrap_or(IngType::Public); - (skip, take, r#type, *align) - } - _ => return None, - } - .wrap_some() -} - -pub fn publish_ing(args: &Args) -> Option<&String> { - match args { - Args { - cmd: - Some(Cmd::Ing(cmd::ing::Opt { - cmd: None, - publish: Some(content), - comment: None, - })), - id: None, - rev: false, - skip: None, - take: None, - global_opt: _, - } => content, - _ => return None, - } - .wrap_some() -} - -pub fn comment_ing(args: &Args) -> Option<(&String, usize)> { - match args { - Args { - cmd: - Some(Cmd::Ing(cmd::ing::Opt { - cmd: None, - publish: None, - comment: Some(content), - })), - id: Some(id), - rev: false, - skip: None, - take: None, - global_opt: _, - } => (content, *id), - _ => return None, - } - .wrap_some() -} diff --git a/src/args/parser/mod.rs b/src/args/parser/mod.rs deleted file mode 100644 index fe6ba72..0000000 --- a/src/args/parser/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub mod fav; -pub mod ing; -pub mod news; -pub mod post; -pub mod user; - -use crate::args::{Args, GlobalOpt}; - -fn get_skip(skip: &Option) -> usize { - skip.unwrap_or(0) -} - -fn get_take(take: &Option) -> usize { - take.unwrap_or(8).min(100) -} - -pub const fn no_operation(args: &Args) -> bool { - matches!( - args, - Args { - cmd: None, - id: None, - rev: false, - skip: None, - take: None, - global_opt: GlobalOpt { with_pat: None, .. } - } - ) -} diff --git a/src/args/parser/news.rs b/src/args/parser/news.rs deleted file mode 100644 index 83d72f8..0000000 --- a/src/args/parser/news.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::args::parser::{get_skip, get_take}; -use crate::args::{cmd, Args, Cmd}; -use crate::infra::option::WrapOption; - -pub fn list_news(args: &Args) -> Option<(usize, usize)> { - match args { - Args { - cmd: Some(Cmd::News(cmd::news::Opt { list: true })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - (skip, take) - } - _ => return None, - } - .wrap_some() -} diff --git a/src/args/parser/post.rs b/src/args/parser/post.rs deleted file mode 100644 index c668d93..0000000 --- a/src/args/parser/post.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::args::cmd::post::{CreateCmd, UpdateCmd}; -use crate::args::parser::{get_skip, get_take}; -use crate::args::{cmd, Args, Cmd}; -use crate::infra::option::WrapOption; - -pub fn list_post(args: &Args) -> Option<(usize, usize)> { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: true, - delete: false, - cmd: None, - })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - (skip, take) - } - _ => return None, - } - .wrap_some() -} - -pub fn show_post(args: &Args) -> Option { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: true, - show_meta: false, - show_comment: false, - list: false, - delete: false, - cmd: None, - })), - id: Some(id), - rev: false, - skip: None, - take: None, - global_opt: _, - } => *id, - _ => return None, - } - .wrap_some() -} - -pub fn show_post_meta(args: &Args) -> Option { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: true, - show_comment: false, - list: false, - delete: false, - cmd: None, - })), - id: Some(id), - rev: false, - skip: None, - take: None, - global_opt: _, - } => *id, - _ => return None, - } - .wrap_some() -} - -pub fn show_post_comment(args: &Args) -> Option { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: true, - list: false, - delete: false, - cmd: None, - })), - id: Some(id), - rev: _, - skip: None, - take: None, - global_opt: _, - } => *id, - _ => return None, - } - .wrap_some() -} - -pub fn search_self_post(args: &Args) -> Option<(&String, usize, usize)> { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: false, - delete: false, - cmd: - Some(cmd::post::Cmd::Search(cmd::post::SearchCmd { - self_keyword: Some(keyword), - site_keyword: None, - })), - })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - (keyword, skip, take) - } - _ => return None, - } - .wrap_some() -} - -pub fn search_site_post(args: &Args) -> Option<(&String, usize, usize)> { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: false, - delete: false, - cmd: - Some(cmd::post::Cmd::Search(cmd::post::SearchCmd { - self_keyword: None, - site_keyword: Some(keyword), - })), - })), - id: None, - rev: _, - skip, - take, - global_opt: _, - } => { - let skip = get_skip(skip); - let take = get_take(take); - (keyword, skip, take) - } - _ => return None, - } - .wrap_some() -} - -pub fn delete_post(args: &Args) -> Option { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: false, - delete: true, - cmd: None, - })), - id: Some(id), - rev: false, - skip: None, - take: None, - global_opt: _, - } => *id, - _ => return None, - } - .wrap_some() -} - -pub fn create_post(args: &Args) -> Option<&CreateCmd> { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: false, - delete: false, - cmd: Some(cmd::post::Cmd::Create(cmd)), - })), - id: None, - rev: _, - skip: None, - take: None, - global_opt: _, - } => cmd, - _ => return None, - } - .wrap_some() -} - -pub fn update_post(args: &Args) -> Option<(usize, &UpdateCmd)> { - match args { - Args { - cmd: - Some(Cmd::Post(cmd::post::Opt { - show: false, - show_meta: false, - show_comment: false, - list: false, - delete: false, - cmd: Some(cmd::post::Cmd::Update(cmd)), - })), - id: Some(id), - rev: _, - skip: None, - take: None, - global_opt: _, - } => (*id, cmd), - _ => return None, - } - .wrap_some() -} diff --git a/src/args/parser/user.rs b/src/args/parser/user.rs deleted file mode 100644 index a44ad76..0000000 --- a/src/args/parser/user.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::args::{cmd, Args, Cmd, GlobalOpt}; -use crate::infra::option::WrapOption; - -pub fn login(args: &Args) -> Option<&String> { - match args { - Args { - cmd: - Some(Cmd::User(cmd::user::Opt { - login: Some(pat), - logout: false, - info: false, - })), - id: None, - rev: false, - skip: None, - take: None, - global_opt: GlobalOpt { with_pat: None, .. }, - } => pat, - _ => return None, - } - .wrap_some() -} - -pub const fn logout(args: &Args) -> bool { - matches!( - args, - Args { - cmd: Some(Cmd::User(cmd::user::Opt { - login: None, - logout: true, - info: false, - })), - id: None, - rev: false, - skip: None, - take: None, - global_opt: GlobalOpt { with_pat: None, .. }, - } - ) -} - -pub const fn user_info(args: &Args) -> bool { - matches!( - args, - Args { - cmd: Some(Cmd::User(cmd::user::Opt { - login: None, - logout: false, - info: true, - })), - id: None, - rev: false, - skip: None, - take: None, - global_opt: _, - } - ) -} diff --git a/src/bin/cnb.rs b/src/bin/cnb.rs new file mode 100644 index 0000000..3cc27a8 --- /dev/null +++ b/src/bin/cnb.rs @@ -0,0 +1,13 @@ +use clap::Parser; +use cnblogs_lib::context::Context; +use cnblogs_lib::{commands::Cli, logic}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let mut ctx = Context::new()?; + ctx.json = cli.json; + logic::run(cli, &mut ctx).await?; + Ok(()) +} diff --git a/src/commands/fav.rs b/src/commands/fav.rs new file mode 100644 index 0000000..4e25fbc --- /dev/null +++ b/src/commands/fav.rs @@ -0,0 +1,25 @@ +use clap::{Args, Subcommand}; +use serde::Serialize; + +#[derive(Debug, Args)] +pub struct FaverateCommand { + #[clap(subcommand)] + pub commands: FaverateAction, +} + +#[derive(Debug, Subcommand)] +pub enum FaverateAction { + List(ListArgs), +} + +#[derive(Debug, Args, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListArgs { + /// 分页页码(从1开始) + #[arg(long = "page-index", default_value_t = 1)] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg(long = "page-size", default_value_t = 10)] + pub page_size: u64, +} diff --git a/src/commands/ing.rs b/src/commands/ing.rs new file mode 100644 index 0000000..ffb5a0b --- /dev/null +++ b/src/commands/ing.rs @@ -0,0 +1,191 @@ +use anyhow::anyhow; +use clap::{Args, Subcommand, ValueEnum, builder::NonEmptyStringValueParser}; +use serde::Serialize; +use serde_json::json; + +use crate::commands::validate_non_zero_id; + +#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)] +pub enum IngType { + #[clap(name = "all", help = "查询所有闪存内容")] + All, + /// following 简写查询关注的人的闪存 + #[clap(name = "fo")] + Following, + /// 查询@我的闪存 + // #[clap(name = "mention")] + // Mention, + /// 查询我的闪存 + // #[clap(name = "my")] + My, + /// 查询回复我的闪存 + // #[clap(name = "comment")] + // Comment, + /// recent comment 简写 查询最近的评论闪存 + #[clap(name = "rc")] + RecentComment, + /// recent comment 简写 查询我发布的评论闪存 + #[clap(name = "myc")] + MyComment, + /// 按标签查询闪存(需配合 --tag 参数) + #[clap(name = "tag")] + Tag, +} + +impl IngType { + pub const fn as_str(&self) -> &str { + match self { + Self::All => "all", + Self::Following => "following", + // Self::Mention => "mention", + Self::My => "my", + // Self::Comment => "comment", + Self::RecentComment => "recentcomment", + Self::MyComment => "mycomment", + Self::Tag => "tag", + } + } +} + +/// 闪存的发布,查询,删除,评论等操作 +#[derive(Debug, Subcommand)] +pub enum IngAction { + Create(IngContent), + Delete(IngDelete), + List(IngListArg), + Replay(IngReplayContent), + Show(IngShowDetail), +} + +/// 浏览闪存,可结合type和option进一步筛选。 +#[derive(Debug, Args, Serialize)] +pub struct IngListArg { + /// 闪存查询类型(可选值:all/following/mention/my/comments/recent-comments/my-comments/tag) + #[clap(value_enum, default_value_t = IngType::All)] + #[serde(skip)] + pub r#type: IngType, + + /// 分页页码(从1开始) + #[arg(long = "page-index", default_value_t = 1)] + #[serde(rename = "pageIndex")] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg(long = "page-size", default_value_t = 20)] + #[serde(rename = "pageSize")] + pub page_size: u64, + + /// 按照标签名查询,注意 仅当type=tag时,标签名必须。其余可选。 + #[arg( + long, + required_if_eq("type", "tag"), + value_parser = NonEmptyStringValueParser::new(), + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, + + /// no commnet,关闭评论,只显示闪存。 + #[arg( + default_value = "false", + long = "no", + // help = "no-comment,用于控制是否显示评论。" + )] + #[serde(skip)] + pub no_comment: bool, +} + +/// 发布闪存,操作示例: +/// +/// cnb ing create "Hello World" +/// +/// cnb ing create --tag "lucky" +/// +/// cnb ing create "Hello World" --tag lucky --tag cnb +#[derive(Debug, Args)] +pub struct IngContent { + /// 闪存正文,闪存内容空时,tag为必须 + #[clap(value_parser = NonEmptyStringValueParser::new())] + pub content: Option, + + /// content是否公开,默认公开 + #[arg(long = "is-private", short = 'p')] + pub is_private: bool, + + /// tag标签,可选,如lucky,python + #[arg(short = 't', + long = "tag", + // required_if_eq("content", "None"), + requires_if("false", "content"), + value_parser = NonEmptyStringValueParser::new())] + pub tag: Vec, +} + +impl IngContent { + pub fn to_json(self) -> impl Serialize { + let mut content = String::new(); + self.tag.iter().for_each(|x| { + if x.contains("[") && x.contains("]") { + content.push_str(x); + } else { + content.push_str(&format!("[{}]", x)); + } + }); + + if let Some(cc) = self.content { + content.push_str(&cc); + } + + json!({ + "content": content, + "isPrivate": false, + "clientType": 13, + }) + } + + pub fn validate(&self) -> anyhow::Result<()> { + if self.content.is_none() && self.tag.is_empty() { + return Err(anyhow!("当 content 为空时,tag 必须提供")); + } + Ok(()) + } +} + +/// 根据提供的闪存id删除对应闪存 +/// 也可以同时提供闪存id和commentid删除对应的评论 +#[derive(Debug, Args)] +pub struct IngDelete { + #[arg( + required = true, + // value_delimiter = ',', + value_parser = validate_non_zero_id, + help = "要删除的ID列表(用逗号分隔),不能包含0" + )] + pub id: u64, + + #[arg(long = "cid", help = "根据闪存id和commentid删除评论。", value_parser = validate_non_zero_id)] + pub comment_id: Option, +} + +/// 评论回复,to实现@功能, +#[derive(Debug, Args)] +pub struct IngReplayContent { + #[clap(value_parser = NonEmptyStringValueParser::new())] + pub content: String, + + #[arg(long, value_parser = validate_non_zero_id, required = true)] + pub id: u64, + // #[arg(long = "to", short = 't', value_parser = NonEmptyStringValueParser::new())] + // pub to: Vec, +} + +/// 根据ID展示闪存 +#[derive(Debug, Args)] +pub struct IngShowDetail { + /// 闪存ID + #[clap(value_parser =validate_non_zero_id)] + pub id: u64, + + /// 是否显示评论 + #[arg(long, short = 'c', default_value_t = false)] + pub comments: bool, +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..da736d5 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,78 @@ +pub mod fav; +pub mod ing; +pub mod news; +pub mod post; +pub mod user; + +use std::fmt; + +use clap::{Parser, Subcommand, ValueEnum}; +#[derive(Debug, Parser)] +#[command(name = "cnblogs", about = "博客园CLI工具", version)] +pub struct Cli { + #[arg(long, short = 'j', default_value = "false", global = true)] + pub json: bool, + #[arg(long, short = 'v', global = true, help = "显示详细信息")] + pub verbose: bool, + #[arg(long, short = 's', global = true, default_value_t = Style::Pretty, help = "输出样式")] + pub style: Style, + #[command(subcommand)] + pub commands: Commands, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + User(user::UserCommand), + Ing { + #[command(subcommand)] + action: ing::IngAction, + }, + Post(post::PostCommand), + News(news::NewsCommand), + Fav(fav::FaverateCommand), +} + +#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)] +pub enum Style { + #[clap(name = "json", help = "JSON格式输出")] + Json, + #[clap(name = "pretty", help = "美化输出格式输出")] + Pretty, + #[clap(name = "quiet", help = "禁用输出")] + Quiet, +} + +impl fmt::Display for Style { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Json => write!(f, "json"), + Self::Pretty => write!(f, "pretty"), + Self::Quiet => write!(f, "quiet"), + } + } +} + +// 验证不能为零 +fn validate_non_zero_id(s: &str) -> Result { + let id = s + .parse::() + .map_err(|_| format!("'{}' 不是有效的数字", s))?; + + if id == 0 { + Err("ID不能为0".to_string()) + } else { + Ok(id) + } +} + +pub fn validate_non_string(s: &str) -> Result { + let res = s + .parse::() + .map_err(|_| format!("'{}' 不是有效的字符串", s))?; + + if res.is_empty() { + Err("ID不能为0".to_string()) + } else { + Ok(res) + } +} diff --git a/src/commands/news.rs b/src/commands/news.rs new file mode 100644 index 0000000..d973698 --- /dev/null +++ b/src/commands/news.rs @@ -0,0 +1,25 @@ +use clap::{Args, Subcommand}; +use serde::Serialize; + +#[derive(Debug, Args)] +pub struct NewsCommand { + #[clap(subcommand)] + pub commands: NewsAction, +} + +#[derive(Debug, Subcommand)] +pub enum NewsAction { + List(ListArgs), +} + +#[derive(Debug, Args, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListArgs { + /// 分页页码(从1开始) + #[arg(long = "page-index", default_value_t = 1)] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg(long = "page-size", default_value_t = 10)] + pub page_size: u64, +} diff --git a/src/commands/post.rs b/src/commands/post.rs new file mode 100644 index 0000000..d453031 --- /dev/null +++ b/src/commands/post.rs @@ -0,0 +1,102 @@ +use clap::{Args, Subcommand, builder::NonEmptyStringValueParser}; +use serde::Serialize; + +use crate::commands::validate_non_zero_id; + +#[derive(Debug, Args)] +pub struct PostCommand { + #[clap(subcommand)] + pub commands: PostAction, +} + +#[derive(Debug, Subcommand)] +pub enum PostAction { + // Create, + List(ListArgs), + Replay(ReplayArgs), + Show(ShowArgs), + // Update, +} + +/// 随笔列表,可根据博客名称获取。 +#[derive(Debug, Args, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListArgs { + /// 博客名称,api接口的blog_app,默认当前用户,也可以指定。 + #[arg( + value_name = "Blog name", + help = "博客名称,api接口的blog_app,默认当前用户,也可以指定。", + value_parser = NonEmptyStringValueParser::new() + )] + #[serde(skip)] + pub name: Option, + + /// 分页页码(从1开始) + #[arg(long = "page-index", default_value_t = 1)] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg(long = "page-size", default_value_t = 10)] + pub page_size: u64, +} + +/// 展示随笔内容 +#[derive(Debug, Args, Serialize)] +pub struct ShowArgs { + /// 随笔ID,必传 + #[serde(skip)] + #[clap(value_parser = validate_non_zero_id, required = true)] + pub id: u64, + + /// 展示评论,默认关闭评论,选择显示评论,则不展示随笔内容 + /// + /// 目前API调用存在问题,未实现此功能,占位。 + #[serde(skip)] + #[arg(long, short = 'c', default_value_t = false)] + pub comments: bool, + + /// 分页页码(从1开始) + #[arg( + default_value_t = 1, + long = "page-index", + required_if_eq("comments", "true"), + value_parser = validate_non_zero_id)] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg( + long = "page-size", + default_value_t = 20, + required_if_eq("comments", "true"), + value_parser = validate_non_zero_id + )] + pub page_size: u64, +} + +/// 博文评论回复 +#[derive(Debug, Args, Serialize)] +pub struct ReplayArgs { + /// 评论内容,必传 + #[serde(rename = "body")] + #[clap(value_parser = NonEmptyStringValueParser::new(), required = true)] + pub content: String, + + /// 随笔ID,必传 + #[serde(skip)] + #[arg(long, value_parser = validate_non_zero_id, required = true)] + pub id: u64, + + #[serde(skip)] + #[arg( + long, + value_name = "Blog name", + help = "博客名称,api接口的blog_app,默认当前用户,也可以指定。", + value_parser = NonEmptyStringValueParser::new() + )] + /// 回复博客园名称,不传为当前登录账号的博客名称。 + pub name: Option, +} + +/// 博文评论回复 +#[derive(Debug, Args, Serialize)] +pub struct CreateArgs {} diff --git a/src/commands/user.rs b/src/commands/user.rs new file mode 100644 index 0000000..8b0f81f --- /dev/null +++ b/src/commands/user.rs @@ -0,0 +1,27 @@ +//! +//! 认证子命令 +//! + +use clap::{Args, Subcommand, builder::NonEmptyStringValueParser}; + +#[derive(Debug, Args)] +pub struct UserCommand { + #[clap(subcommand)] + pub commands: UserAction, +} + +/// 提供通过access token登录,状态查询,退出,显示当前token功能 +#[derive(Debug, Subcommand)] +pub enum UserAction { + /// 用户登录,需提供access token。 + Login { + #[clap(value_parser = NonEmptyStringValueParser::new())] + token: String, + }, + /// 用户退出 + Logout, + /// 查看登录状态,登录后会显示用户信息 + Status, + /// 显示当前登录token + Token, +} diff --git a/src/context/config.rs b/src/context/config.rs new file mode 100644 index 0000000..f632695 --- /dev/null +++ b/src/context/config.rs @@ -0,0 +1,128 @@ +use std::fs::File; +use std::{ + fs, + io::{Read, Write}, + path::PathBuf, +}; + +use anyhow::{Result, anyhow}; +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; + +use crate::models::user::UserInfo; + +const CACHE_DIR: &str = ".cnblogs"; +const CACHE: &str = "token"; + +#[derive(Debug, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct Cache { + pub id: u64, + pub blog_id: u64, + pub blog_app: String, + pub username: String, + pub token: String, +} + +impl Cache { + pub fn from_bytes(buf: &[u8]) -> Result { + Ok(serde_json::from_slice(buf)?) + } + + pub fn to_bytes(&self) -> Result> { + Ok(serde_json::to_vec(self)?) + } + + /// 检查 token 是否为空 + pub fn is_token_empty(&self) -> bool { + self.token.trim().is_empty() + } + + /// 验证缓存数据的有效性 + pub fn is_valid(&self) -> bool { + !self.is_token_empty() && self.id > 0 + } +} + +impl From for Cache { + fn from(value: UserInfo) -> Self { + Self { + id: value.account_id, + blog_id: value.blog_id, + blog_app: value.blog_app, + username: value.display_name, + token: "".to_string(), + } + } +} + +#[derive(Debug)] +pub struct CacheDir { + pub cache_dir: PathBuf, + pub cache_file: PathBuf, + pub home_dir: PathBuf, +} + +impl CacheDir { + pub fn new() -> Result { + let home_dir = + home::home_dir().ok_or_else(|| anyhow!("无法获取用户家目录,退出。".red()))?; + let cache_dir = PathBuf::from(CACHE_DIR); + let cache_file = PathBuf::from(CACHE); + + Ok(Self { + cache_dir, + cache_file, + home_dir, + }) + } + + /// 初始化,检查文件夹和目录是否存在,如果不存在则创建 + pub fn init(&self) -> Result<()> { + self.ensure_dir()?; + self.ensure_file() + } + + /// 获取缓存目录的完整路径 + pub fn full_cache_dir(&self) -> PathBuf { + self.home_dir.join(&self.cache_dir) + } + + /// 获取缓存文件的完整路径 + pub fn full_cache_file(&self) -> PathBuf { + self.full_cache_dir().join(&self.cache_file) + } + + /// 检查缓存目录是否存在,不存在创建。 + pub fn ensure_dir(&self) -> Result<()> { + let p = self.full_cache_dir(); + if !p.exists() { + fs::create_dir_all(p)?; + } + Ok(()) + } + + /// 检查缓存文件是否存在,不存在创建。 + pub fn ensure_file(&self) -> Result<()> { + let p = self.full_cache_file(); + if !p.exists() { + fs::File::create(p)?; + } + Ok(()) + } + + /// 写入缓存文件 + pub fn write(&self, buf: &[u8]) -> Result<()> { + let mut f = File::create(self.full_cache_file())?; + f.write_all(buf)?; + Ok(()) + } + + /// 读取缓存文件 + pub fn read(&self) -> Result> { + let mut buf = vec![]; + let mut f = File::open(self.full_cache_file())?; + f.read_to_end(&mut buf)?; + Ok(buf) + } +} diff --git a/src/context/mod.rs b/src/context/mod.rs new file mode 100644 index 0000000..22dc400 --- /dev/null +++ b/src/context/mod.rs @@ -0,0 +1,78 @@ +pub mod config; +pub mod output; + +use core::time; +use std::fmt; + +use anyhow::{Ok, Result}; +use reqwest::{ + Client, ClientBuilder, + header::{self, HeaderMap}, +}; + +use crate::context::{ + config::{Cache, CacheDir}, + output::Terminal, +}; + +// const FILENAME: &str = ".cnblogs/token"; + +#[derive(Debug)] +pub struct Context { + pub terminal: Terminal, + pub client: Client, + pub json: bool, + pub cache: Cache, + pub path: CacheDir, +} + +impl Context { + pub fn new() -> Result { + let path = CacheDir::new()?; + path.init()?; + let buf = path.read()?; + let cache = Cache::from_bytes(&buf).unwrap_or_default(); + let terminal = Terminal::new(); + let mut headers = HeaderMap::new(); + + if !cache.token.is_empty() { + let header_value = format!("Bearer {}", cache.token); + headers.append(header::AUTHORIZATION, header_value.parse()?); + headers.append("authorization-type", "pat".parse()?); + } + + let client = ClientBuilder::new() + .default_headers(headers) + .connect_timeout(time::Duration::from_secs(10)) + .https_only(true) + .build()?; + + Ok(Self { + terminal, + client, + json: false, + cache, + path, + }) + } + + pub const fn set_json(&mut self, json: bool) { + self.json = json; + } + + pub fn print_message(&mut self, msg: T) -> Result<()> { + self.terminal.writeln(msg) + } + + /// 清空缓存信息 + pub fn clean(&self) -> Result<()> { + let c = Cache::default(); + let buf = c.to_bytes()?; + self.path.write(&buf) + } + + /// 保存之缓存文件 + pub fn save_cache(&self, cache: Cache) -> Result<()> { + self.path.write(&cache.to_bytes()?) + } +} diff --git a/src/context/output.rs b/src/context/output.rs new file mode 100644 index 0000000..aa5f3ca --- /dev/null +++ b/src/context/output.rs @@ -0,0 +1,78 @@ +//! +//! 输出统一管理 +//! + +use std::fmt; +use std::fmt::Display; +use std::io::IsTerminal; +use std::io::Write; + +use anstream::{AutoStream, ColorChoice}; +use anstyle::{AnsiColor, Color, Style}; +use anyhow::Ok; +use anyhow::Result; +use serde::Serialize; + +#[derive(Debug)] +pub struct Terminal { + pub stdout: AutoStream, + pub stderr: AutoStream, + pub color_choice: ColorChoice, +} + +impl Terminal { + pub fn new() -> Self { + let use_color = std::io::stdout().is_terminal(); + + let color_choice = if use_color { + ColorChoice::Auto + } else { + ColorChoice::Never + }; + let stdout = AutoStream::new(std::io::stdout(), color_choice); + let stderr = AutoStream::new(std::io::stderr(), color_choice); + Self { + stdout, + stderr, + color_choice, + } + } + + pub fn write(&mut self, msg: impl Display) -> Result<()> { + Ok(write!(self.stdout, "{}", msg)?) + } + + pub fn json(&mut self, msg: impl fmt::Display + Serialize) -> Result<()> { + Ok(writeln!(self.stdout, "{}", serde_json::to_string(&msg)?)?) + } + + /// 打印信息(普通输出) + pub fn writeln(&mut self, message: impl fmt::Display) -> Result<()> { + Ok(writeln!(self.stdout, "{}", message)?) + } + + /// 清除当前行 + pub fn clear_line(&mut self) -> Result<()> { + Ok(write!(self.stdout, "\r\x1b[K")?) + } +} + +impl Default for Terminal { + fn default() -> Self { + Self::new() + } +} +pub const fn blue_style() -> Style { + Style::new().fg_color(Some(Color::Ansi(AnsiColor::Blue))) +} + +pub const fn green_style() -> Style { + Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))) +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Verbosity { + Verbose, + Normal, + Qiuet, +} diff --git a/src/display/colorful/fav.rs b/src/display/colorful/fav.rs deleted file mode 100644 index 37803d1..0000000 --- a/src/display/colorful/fav.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::api::fav::get_list::FavEntry; -use crate::args::TimeStyle; -use crate::display::colorful::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use colored::Colorize; -use std::fmt::Write; -use std::ops::Not; - -pub fn list_fav( - time_style: &TimeStyle, - fav_iter: Result>, -) -> Result { - let fav_iter = match fav_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - fav_iter - .map(|fav| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&fav.create_time, time_style); - writeln!(buf, "{} {}", create_time.dimmed(), fav.url.dimmed())?; - writeln!(buf, " {}", fav.title)?; - - let summary = { - fav.summary.width_split(get_term_width() - 4).map_or_else( - || fav.summary.clone(), - |vec| { - vec.into_iter() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n") - }, - ) - }; - if summary.is_empty().not() { - writeln!(buf, "{}", summary.dimmed())?; - } - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/colorful/ing.rs b/src/display/colorful/ing.rs deleted file mode 100644 index dc63d96..0000000 --- a/src/display/colorful/ing.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::api::ing::get_comment_list::IngCommentEntry; -use crate::api::ing::get_list::IngEntry; -use crate::api::ing::{ - fmt_content, get_ing_at_user_tag_text, ing_star_tag_to_text, rm_ing_at_user_tag, IngSendFrom, -}; -use crate::args::TimeStyle; -use crate::display::colorful::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use colored::Colorize; -use std::fmt::Write; -use std::ops::Not; -use unicode_width::UnicodeWidthStr; - -// TODO: rm unnecessary line divider -pub fn list_ing( - time_style: &TimeStyle, - ing_with_comment_iter: Result)>>, - align: bool, -) -> Result { - let mut ing_with_comment_iter = match ing_with_comment_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - ing_with_comment_iter.try_fold(String::new(), |mut buf, (ing, comment_list)| try { - { - let buf = &mut buf; - let create_time = display_cnb_time(&ing.create_time, time_style); - write!(buf, "{}", create_time.dimmed())?; - - let send_from_mark = match ing.send_from { - IngSendFrom::Cli => Some("CLI"), - IngSendFrom::CellPhone => Some("Mobile"), - IngSendFrom::VsCode => Some("VSCode"), - IngSendFrom::Web => Some("Web"), - _ => None, - }; - if let Some(mark) = send_from_mark { - write!(buf, " {}", mark.dimmed())?; - } - if ing.is_lucky { - let star_text = ing_star_tag_to_text(&ing.icons); - write!(buf, " {}⭐", star_text.yellow())?; - } - writeln!(buf, " {} {}", "#".dimmed(), ing.id.to_string().dimmed())?; - let content = if align { - let user_name_width = ing.user_name.width_cjk(); - let left_width = get_term_width().saturating_sub(user_name_width + 3); - fmt_content(&ing.content) - .width_split(left_width) - .map_or_else( - || ing.content.clone(), - |lines| { - if comment_list.is_empty().not() { - lines.join("\n").replace( - '\n', - &format!("\n │{}", " ".repeat(user_name_width - 2)), - ) - } else { - lines.join("\n").replace( - '\n', - &format!("\n{}", " ".repeat(user_name_width + 3)), - ) - } - }, - ) - } else { - fmt_content(&ing.content) - }; - writeln!(buf, " {} {}", ing.user_name.cyan(), content)?; - - let len = comment_list.len(); - if len != 0 { - let max_i = len - 1; - let comment_list_buf: Result = comment_list.iter().enumerate().try_fold( - String::new(), - |mut buf, (i, entry)| try { - { - let buf = &mut buf; - if i != max_i { - write!(buf, " │ {}", entry.user_name.blue())?; - } else { - write!(buf, " └ {}", entry.user_name.blue())?; - } - let at_user = get_ing_at_user_tag_text(&entry.content); - if at_user.is_empty().not() { - write!(buf, " {}{}", "@".bright_black(), at_user.bright_black())?; - } - let content = { - let content = rm_ing_at_user_tag(&entry.content); - fmt_content(&content) - }; - writeln!(buf, " {}", content.dimmed())?; - } - buf - }, - ); - write!(buf, "{}", comment_list_buf?)?; - } - - writeln!(buf)?; - }; - buf - }) -} diff --git a/src/display/colorful/mod.rs b/src/display/colorful/mod.rs deleted file mode 100644 index ca1e682..0000000 --- a/src/display/colorful/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub mod fav; -pub mod ing; -pub mod news; -pub mod post; -pub mod user; - -use anyhow::Result; -use colored::Colorize; -use std::fmt::Display; - -#[inline] -pub fn fmt_err(e: &anyhow::Error) -> String { - format!("{}: {}", "Err".red(), e) -} - -#[inline] -pub fn fmt_result(result: &Result) -> String { - match result { - Ok(t) => format!("{}: {}", "Ok".green(), t), - Err(e) => fmt_err(e), - } -} diff --git a/src/display/colorful/news.rs b/src/display/colorful/news.rs deleted file mode 100644 index 93d3a6e..0000000 --- a/src/display/colorful/news.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::api::news::get_list::NewsEntry; -use crate::args::TimeStyle; -use crate::display::colorful::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use colored::Colorize; -use std::fmt::Write; - -pub fn list_news( - time_style: &TimeStyle, - news_iter: Result>, -) -> Result { - let news_iter = match news_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - news_iter - .map(|news| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&news.create_time, time_style); - let url = format!("https://news.cnblogs.com/n/{}", news.id); - writeln!(buf, "{} {}", create_time.dimmed(), url.dimmed())?; - writeln!(buf, " {}", news.title)?; - - let summary = { - let summary = format!("{}...", news.summary); - summary.width_split(get_term_width() - 4).map_or_else( - || summary.clone(), - |vec| { - vec.into_iter() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n") - }, - ) - }; - writeln!(buf, "{}", summary.dimmed())?; - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/colorful/post.rs b/src/display/colorful/post.rs deleted file mode 100644 index 095f462..0000000 --- a/src/display/colorful/post.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::api::post::get_comment_list::PostCommentEntry; -use crate::api::post::get_one::PostEntry; -use crate::api::post::search_site::SearchResultEntry; -use crate::args::TimeStyle; -use crate::display::colorful::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use colored::Colorize; -use std::fmt::Write; - -pub fn list_post( - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - let (mut entry_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - entry_iter.try_fold( - format!("{}/{}\n", entry_iter.len(), total_count), - |mut buf, entry| try { - { - let buf = &mut buf; - write!(buf, "{} {}", "#".dimmed(), entry.id.to_string().dimmed())?; - if entry.is_published { - write!(buf, " {}", "Pub".green())?; - } else { - write!(buf, " {}", "Dft".yellow())?; - } - if entry.is_pinned { - write!(buf, " {}", "Pin".magenta())?; - } - write!(buf, " {}", entry.title.cyan().bold())?; - writeln!(buf)?; - } - buf - }, - ) -} - -pub fn show_post(entry: &Result) -> Result { - let entry = match entry { - Ok(entry) => entry, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - writeln!(buf, "{}\n", entry.title.cyan().bold())?; - if let Some(body) = &entry.body { - writeln!(buf, "{}", body)?; - } - } - buf.wrap_ok() -} - -pub fn show_post_meta(time_style: &TimeStyle, entry: &Result) -> Result { - let entry = match entry { - Ok(entry) => entry, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - writeln!(buf, "Title {}", entry.title.cyan().bold())?; - { - write!(buf, "Status")?; - if entry.is_published { - write!(buf, " {}", "Published".green())?; - } else { - write!(buf, " {}", "Draft".yellow())?; - } - if entry.is_pinned { - write!(buf, " {}", "Pinned".magenta())?; - } - writeln!(buf)?; - }; - if let Some(body) = &entry.body { - let words_count = words_count::count(body).words; - writeln!(buf, "Words {}", words_count)?; - } - if let Some(tags) = &entry.tags { - if let Some(tags_text) = tags - .clone() - .into_iter() - .reduce(|acc, tag| format!("{}, {}", acc, tag)) - { - writeln!(buf, "Tags {}", tags_text)?; - } - } - let create_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "Create {}", create_time)?; - let modify_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "Modify {}", modify_time)?; - writeln!(buf, "Link https:{}", entry.url)?; - } - buf.wrap_ok() -} - -pub fn show_post_comment( - time_style: &TimeStyle, - comment_iter: Result>, -) -> Result { - let mut comment_iter = match comment_iter { - Ok(entry) => entry, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - comment_iter.try_fold(String::new(), |mut buf, comment| try { - { - let buf = &mut buf; - let create_time = display_cnb_time(&comment.create_time, time_style); - let floor_text = format!("{}F", comment.floor); - writeln!(buf, "{} {}", create_time.dimmed(), floor_text.dimmed())?; - writeln!(buf, " {} {}", comment.user_name.cyan(), comment.content)?; - } - buf - }) -} - -pub fn search_self_post( - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - let (mut id_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - id_iter.try_fold( - format!("{}/{}\n", id_iter.len(), total_count), - |mut buf, id| try { - writeln!(&mut buf, "# {}", id)?; - buf - }, - ) -} - -pub fn search_site_post( - time_style: &TimeStyle, - entry_iter: Result>, -) -> Result { - let entry_iter = match entry_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - entry_iter - .map(|entry| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "{} {}", create_time.dimmed(), entry.url.dimmed())?; - writeln!(buf, " {}", entry.title)?; - let view_vote_comment_count = format!( - "View {} Vote {} Comment {}", - entry.view_count, entry.vote_count, entry.comment_count - ); - writeln!(buf, " {}", view_vote_comment_count.dimmed())?; - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/colorful/user.rs b/src/display/colorful/user.rs deleted file mode 100644 index 9793aec..0000000 --- a/src/display/colorful/user.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::api::user::info::UserInfo; -use crate::display::colorful::fmt_err; -use crate::infra::result::WrapResult; -use anyhow::Result; -use colored::Colorize; -use std::fmt::Write; -use std::path::PathBuf; - -pub fn login(cfg_path: &Result) -> String { - match cfg_path { - Ok(pb) => format!("PAT was saved in {:?}", pb), - Err(e) => fmt_err(e), - } -} - -pub fn logout(cfg_path: &Result) -> String { - match cfg_path { - Ok(pb) => format!("{:?} was successfully removed", pb), - Err(e) => fmt_err(e), - } -} - -pub fn user_info(info: &Result) -> Result { - let info = match info { - Ok(info) => info, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - write!(buf, "{}", info.display_name.cyan())?; - if info.is_vip { - write!(buf, " {}", " VIP ".on_blue())?; - } - writeln!(buf)?; - writeln!( - buf, - "{} Following {} Followers", - info.following_count, info.followers_count - )?; - writeln!(buf, "ID {}", info.blog_id)?; - writeln!(buf, "Joined {}", info.joined)?; - writeln!(buf, "Blog https://www.cnblogs.com/{}", info.blog_app)?; - } - buf.wrap_ok() -} diff --git a/src/display/ing.rs b/src/display/ing.rs new file mode 100644 index 0000000..a4e5749 --- /dev/null +++ b/src/display/ing.rs @@ -0,0 +1,41 @@ +// use colored::Colorize; +use owo_colors::OwoColorize; + +pub fn tag_lucky(is_lucky: bool) -> String { + if is_lucky { + "[幸运星]".yellow().to_string() + } else { + "".to_string() + } +} + +pub fn tag_public(is_private: bool) -> String { + if is_private { + "[未公开]".yellow().to_string() + } else { + "[公开]".blue().to_string() + } +} + +pub fn tag_sender(i: u8) -> String { + match i { + 0 => "[none]".blue().to_string(), + 1 => "[ms]".blue().to_string(), + 2 => "[gtalk]".blue().to_string(), + 3 => "[qq]".blue().to_string(), + 5 => "[sms]".blue().to_string(), + 6 => "[mobile]".blue().to_string(), + 8 => "[web]".blue().to_string(), + 9 => "[vscode]".blue().to_string(), + 13 => "[cnb]".blue().to_string(), + _ => "[unknow]".blue().to_string(), + } +} + +pub fn tag_comment_count(c: u64) -> String { + if c > 0 { + format!("[{}评论]", c) + } else { + "".to_string() + } +} diff --git a/src/display/json/fav.rs b/src/display/json/fav.rs deleted file mode 100644 index 65509ad..0000000 --- a/src/display/json/fav.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::api::fav::get_list::FavEntry; -use crate::display::json::{fmt_err, fmt_ok}; -use anyhow::Result; - -pub fn list_fav(fav_iter: Result>) -> String { - let fav_iter = match fav_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let vec = fav_iter.collect::>(); - - fmt_ok(vec) -} diff --git a/src/display/json/ing.rs b/src/display/json/ing.rs deleted file mode 100644 index 5128195..0000000 --- a/src/display/json/ing.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::api::ing::get_comment_list::IngCommentEntry; -use crate::api::ing::get_list::IngEntry; -use crate::display::json::{fmt_err, fmt_ok}; -use anyhow::Result; -use serde_json::json; - -pub fn list_ing( - ing_with_comment_list: Result)>>, -) -> String { - let ing_with_comment_list = match ing_with_comment_list { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let json_vec = ing_with_comment_list - .map(|(entry, comment_list)| { - json!({ - "entry": entry, - "comment_list": comment_list - }) - }) - .collect::>(); - - fmt_ok(json_vec) -} diff --git a/src/display/json/mod.rs b/src/display/json/mod.rs deleted file mode 100644 index 4a441f4..0000000 --- a/src/display/json/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub mod fav; -pub mod ing; -pub mod news; -pub mod post; -pub mod user; - -use anyhow::Result; -use serde::Serialize; -use serde_json::json; -use std::fmt::Display; - -#[inline] -pub fn fmt_ok(t: impl Serialize) -> String { - let json = json!({ - "is_ok": true, - "msg": t - }); - json.to_string() -} - -#[inline] -pub fn fmt_err(e: impl ToString) -> String { - let json = json!({ - "is_ok": false, - "msg": e.to_string() - }); - json.to_string() -} - -pub fn fmt_result(result: &Result) -> String { - match result { - Ok(t) => fmt_ok(t), - Err(e) => fmt_err(e), - } -} diff --git a/src/display/json/news.rs b/src/display/json/news.rs deleted file mode 100644 index cd5687f..0000000 --- a/src/display/json/news.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::api::news::get_list::NewsEntry; -use crate::display::json::{fmt_err, fmt_ok}; -use anyhow::Result; - -pub fn list_news(news_iter: Result>) -> String { - let news_iter = match news_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let vec = news_iter.collect::>(); - - fmt_ok(vec) -} diff --git a/src/display/json/post.rs b/src/display/json/post.rs deleted file mode 100644 index 5e118c9..0000000 --- a/src/display/json/post.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::api::post::get_comment_list::PostCommentEntry; -use crate::api::post::get_one::PostEntry; -use crate::api::post::search_site::SearchResultEntry; -use crate::display::json::{fmt_err, fmt_ok, fmt_result}; -use anyhow::Result; -use serde_json::json; - -pub fn list_post(result: Result<(impl ExactSizeIterator, usize)>) -> String { - let (entry_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let vec = entry_iter.collect::>(); - let json = json!({ - "listed_count": vec.len(), - "total_count": total_count, - "entry_list": vec, - }); - fmt_ok(json) -} - -pub fn show_post(entry: &Result) -> String { - let json = entry.as_ref().map(|entry| { - json!({ - "title": entry.title, - "body": entry.body - }) - }); - fmt_result(&json) -} - -pub fn show_post_meta(entry: &Result) -> String { - fmt_result(entry) -} - -pub fn show_post_comment( - comment_iter: Result>, -) -> String { - let comment_iter = match comment_iter { - Ok(entry) => entry, - Err(e) => return fmt_err(&e), - }; - - let comment_vec = comment_iter.collect::>(); - fmt_ok(comment_vec) -} - -pub fn search_self_post(result: Result<(impl ExactSizeIterator, usize)>) -> String { - let (id_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let id_list = id_iter.collect::>(); - let json = json!({ - "listed_count": id_list.len(), - "total_count": total_count, - "id_list": id_list, - }); - fmt_ok(json) -} - -pub fn search_site_post( - entry_iter: Result>, -) -> String { - let entry_iter = match entry_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e), - }; - - let entry_vec = entry_iter.collect::>(); - fmt_ok(entry_vec) -} diff --git a/src/display/json/user.rs b/src/display/json/user.rs deleted file mode 100644 index 9552003..0000000 --- a/src/display/json/user.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::api::user::info::UserInfo; -use crate::display::json::fmt_result; -use anyhow::Result; -use serde_json::json; -use std::path::PathBuf; - -pub fn login(cfg_path: &Result) -> String { - let json = cfg_path.as_ref().map(|pb| json!({"cfg_path":pb})); - fmt_result(&json) -} - -pub fn logout(cfg_path: &Result) -> String { - let json = cfg_path.as_ref().map(|pb| json!({"cfg_path":pb})); - fmt_result(&json) -} - -pub fn user_info(info: &Result) -> String { - fmt_result(info) -} diff --git a/src/display/mod.rs b/src/display/mod.rs index c7f29ae..fab8107 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,183 +1 @@ -use crate::api::fav::get_list::FavEntry; -use crate::api::ing::get_comment_list::IngCommentEntry; -use crate::api::ing::get_list::IngEntry; -use crate::api::news::get_list::NewsEntry; -use crate::api::post::get_comment_list::PostCommentEntry; -use crate::api::post::get_one::PostEntry; -use crate::api::post::search_site::SearchResultEntry; -use crate::api::user::info::UserInfo; -use crate::args::{Style, TimeStyle}; -use crate::infra::result::WrapResult; -use anyhow::Result; -use std::path::PathBuf; - -mod colorful; -mod json; -mod normal; - -pub fn login(style: &Style, cfg_path: &Result) -> String { - match style { - Style::Colorful => colorful::user::login(cfg_path), - Style::Normal => normal::user::login(cfg_path), - Style::Json => json::user::login(cfg_path), - } -} - -pub fn logout(style: &Style, cfg_path: &Result) -> String { - match style { - Style::Colorful => colorful::user::logout(cfg_path), - Style::Normal => normal::user::logout(cfg_path), - Style::Json => json::user::logout(cfg_path), - } -} - -pub fn user_info(style: &Style, user_info: &Result) -> Result { - match style { - Style::Colorful => colorful::user::user_info(user_info), - Style::Normal => normal::user::user_info(user_info), - Style::Json => json::user::user_info(user_info).wrap_ok(), - } -} - -pub fn list_ing( - style: &Style, - time_style: &TimeStyle, - ing_with_comment_iter: Result)>>, - align: bool, -) -> Result { - match style { - Style::Colorful => colorful::ing::list_ing(time_style, ing_with_comment_iter, align), - Style::Normal => normal::ing::list_ing(time_style, ing_with_comment_iter, align), - Style::Json => json::ing::list_ing(ing_with_comment_iter).wrap_ok(), - } -} - -pub fn publish_ing(style: &Style, result: &Result<&String>) -> String { - match style { - Style::Colorful => colorful::fmt_result(result), - Style::Normal => normal::fmt_result(result), - Style::Json => json::fmt_result(result), - } -} - -pub fn comment_ing(style: &Style, result: &Result<&String>) -> String { - match style { - Style::Colorful => colorful::fmt_result(result), - Style::Normal => normal::fmt_result(result), - Style::Json => json::fmt_result(result), - } -} - -pub fn show_post(style: &Style, entry: &Result) -> Result { - match style { - Style::Colorful => colorful::post::show_post(entry), - Style::Normal => normal::post::show_post(entry), - Style::Json => json::post::show_post(entry).wrap_ok(), - } -} - -pub fn list_post( - style: &Style, - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - match style { - Style::Colorful => colorful::post::list_post(result), - Style::Normal => normal::post::list_post(result), - Style::Json => json::post::list_post(result).wrap_ok(), - } -} - -pub fn show_post_meta( - style: &Style, - time_style: &TimeStyle, - entry: &Result, -) -> Result { - match style { - Style::Colorful => colorful::post::show_post_meta(time_style, entry), - Style::Normal => normal::post::show_post_meta(time_style, entry), - Style::Json => json::post::show_post_meta(entry).wrap_ok(), - } -} - -pub fn show_post_comment( - style: &Style, - time_style: &TimeStyle, - comment_iter: Result>, -) -> Result { - match style { - Style::Colorful => colorful::post::show_post_comment(time_style, comment_iter), - Style::Normal => normal::post::show_post_comment(time_style, comment_iter), - Style::Json => json::post::show_post_comment(comment_iter).wrap_ok(), - } -} - -pub fn delete_post(style: &Style, result: &Result) -> String { - match style { - Style::Colorful => colorful::fmt_result(result), - Style::Normal => normal::fmt_result(result), - Style::Json => json::fmt_result(result), - } -} - -pub fn search_self_post( - style: &Style, - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - match style { - Style::Colorful => colorful::post::search_self_post(result), - Style::Normal => normal::post::search_self_post(result), - Style::Json => json::post::search_self_post(result).wrap_ok(), - } -} - -pub fn search_site_post( - style: &Style, - time_style: &TimeStyle, - entry_iter: Result>, -) -> Result { - match style { - Style::Colorful => colorful::post::search_site_post(time_style, entry_iter), - Style::Normal => normal::post::search_site_post(time_style, entry_iter), - Style::Json => json::post::search_site_post(entry_iter).wrap_ok(), - } -} - -pub fn create_post(style: &Style, result: &Result) -> String { - match style { - Style::Colorful => colorful::fmt_result(result), - Style::Normal => normal::fmt_result(result), - Style::Json => json::fmt_result(result), - } -} - -pub fn update_post(style: &Style, result: &Result) -> String { - match style { - Style::Colorful => colorful::fmt_result(result), - Style::Normal => normal::fmt_result(result), - Style::Json => json::fmt_result(result), - } -} - -pub fn list_news( - style: &Style, - time_style: &TimeStyle, - news_iter: Result>, -) -> Result { - match style { - Style::Colorful => colorful::news::list_news(time_style, news_iter), - Style::Normal => normal::news::list_news(time_style, news_iter), - Style::Json => json::news::list_news(news_iter).wrap_ok(), - } -} - -pub fn list_fav( - style: &Style, - time_style: &TimeStyle, - fav_iter: Result>, -) -> Result { - match style { - Style::Colorful => colorful::fav::list_fav(time_style, fav_iter), - Style::Normal => normal::fav::list_fav(time_style, fav_iter), - Style::Json => json::fav::list_fav(fav_iter).wrap_ok(), - } -} +pub mod ing; diff --git a/src/display/normal/fav.rs b/src/display/normal/fav.rs deleted file mode 100644 index 6affe0c..0000000 --- a/src/display/normal/fav.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::api::fav::get_list::FavEntry; -use crate::args::TimeStyle; -use crate::display::normal::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use std::fmt::Write; -use std::ops::Not; - -pub fn list_fav( - time_style: &TimeStyle, - fav_iter: Result>, -) -> Result { - let fav_iter = match fav_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - fav_iter - .map(|fav| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&fav.create_time, time_style); - writeln!(buf, "{} {}", create_time, fav.url)?; - writeln!(buf, " {}", fav.title)?; - - let summary = { - fav.summary.width_split(get_term_width() - 4).map_or_else( - || fav.summary.clone(), - |vec| { - vec.into_iter() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n") - }, - ) - }; - if summary.is_empty().not() { - writeln!(buf, "{}", summary)?; - } - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/normal/ing.rs b/src/display/normal/ing.rs deleted file mode 100644 index 62c75d0..0000000 --- a/src/display/normal/ing.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::api::ing::get_comment_list::IngCommentEntry; -use crate::api::ing::get_list::IngEntry; -use crate::api::ing::{ - fmt_content, get_ing_at_user_tag_text, ing_star_tag_to_text, rm_ing_at_user_tag, IngSendFrom, -}; -use crate::args::TimeStyle; -use crate::display::normal::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use std::fmt::Write; -use std::ops::Not; -use unicode_width::UnicodeWidthStr; - -// TODO: rm unnecessary line divider -pub fn list_ing( - time_style: &TimeStyle, - ing_with_comment_list: Result)>>, - align: bool, -) -> Result { - let mut ing_with_comment_list = match ing_with_comment_list { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - ing_with_comment_list.try_fold(String::new(), |mut buf, (ing, comment_list)| try { - { - let buf = &mut buf; - let create_time = display_cnb_time(&ing.create_time, time_style); - write!(buf, "{}", create_time)?; - - let send_from_mark = match ing.send_from { - IngSendFrom::Cli => Some("CLI"), - IngSendFrom::CellPhone => Some("Mobile"), - IngSendFrom::VsCode => Some("VSCode"), - IngSendFrom::Web => Some("Web"), - _ => None, - }; - if let Some(mark) = send_from_mark { - write!(buf, " {}", mark)?; - } - if ing.is_lucky { - let star_text = ing_star_tag_to_text(&ing.icons); - write!(buf, " {}★", star_text)?; - } - writeln!(buf, " # {}", ing.id)?; - let content = if align { - let user_name_width = ing.user_name.width_cjk(); - let left_width = get_term_width().saturating_sub(user_name_width + 3); - fmt_content(&ing.content) - .width_split(left_width) - .map_or_else( - || ing.content.clone(), - |lines| { - if comment_list.is_empty().not() { - lines.join("\n").replace( - '\n', - &format!("\n │{}", " ".repeat(user_name_width - 2)), - ) - } else { - lines.join("\n").replace( - '\n', - &format!("\n{}", " ".repeat(user_name_width + 3)), - ) - } - }, - ) - } else { - fmt_content(&ing.content) - }; - writeln!(buf, " {}: {}", ing.user_name, content)?; - - let len = comment_list.len(); - if len != 0 { - let max_i = len - 1; - let comment_list_buf: Result = comment_list.iter().enumerate().try_fold( - String::new(), - |mut buf, (i, entry)| try { - { - let buf = &mut buf; - if i != max_i { - write!(buf, " │ {}", entry.user_name)?; - } else { - write!(buf, " └ {}", entry.user_name)?; - } - let at_user = get_ing_at_user_tag_text(&entry.content); - if at_user.is_empty().not() { - write!(buf, " @{}", at_user)?; - } - let content = { - let content = rm_ing_at_user_tag(&entry.content); - fmt_content(&content) - }; - writeln!(buf, ": {}", content)?; - } - buf - }, - ); - write!(buf, "{}", comment_list_buf?)?; - } - - writeln!(buf)?; - }; - buf - }) -} diff --git a/src/display/normal/mod.rs b/src/display/normal/mod.rs deleted file mode 100644 index 2c941b9..0000000 --- a/src/display/normal/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod fav; -pub mod ing; -pub mod news; -pub mod post; -pub mod user; - -use anyhow::Result; -use std::fmt::Display; - -#[inline] -pub fn fmt_err(e: &anyhow::Error) -> String { - format!("Err: {}", e) -} - -#[inline] -pub fn fmt_result(result: &Result) -> String { - match result { - Ok(t) => format!("Ok: {}", t), - Err(e) => fmt_err(e), - } -} diff --git a/src/display/normal/news.rs b/src/display/normal/news.rs deleted file mode 100644 index 1cb47b1..0000000 --- a/src/display/normal/news.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::api::news::get_list::NewsEntry; -use crate::args::TimeStyle; -use crate::display::normal::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::str::StrExt; -use crate::infra::terminal::get_term_width; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use std::fmt::Write; - -pub fn list_news( - time_style: &TimeStyle, - news_iter: Result>, -) -> Result { - let news_iter = match news_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - news_iter - .map(|news| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&news.create_time, time_style); - let url = format!("https://news.cnblogs.com/n/{}", news.id); - writeln!(buf, "{} {}", create_time, url)?; - writeln!(buf, " {}", news.title)?; - - let summary = { - let summary = format!("{}...", news.summary); - summary.width_split(get_term_width() - 4).map_or_else( - || summary.clone(), - |vec| { - vec.into_iter() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n") - }, - ) - }; - writeln!(buf, "{}", summary)?; - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/normal/post.rs b/src/display/normal/post.rs deleted file mode 100644 index 3a9bd9a..0000000 --- a/src/display/normal/post.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::api::post::get_comment_list::PostCommentEntry; -use crate::api::post::get_one::PostEntry; -use crate::api::post::search_site::SearchResultEntry; -use crate::args::TimeStyle; -use crate::display::normal::fmt_err; -use crate::infra::result::WrapResult; -use crate::infra::time::display_cnb_time; -use anyhow::Result; -use std::fmt::Write; - -pub fn list_post( - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - let (mut entry_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - entry_iter.try_fold( - format!("{}/{}\n", entry_iter.len(), total_count), - |mut buf, entry| try { - { - let buf = &mut buf; - write!(buf, "# {}", entry.id)?; - if entry.is_published { - write!(buf, " Pub")?; - } else { - write!(buf, " Dft")?; - } - if entry.is_pinned { - write!(buf, " Pin")?; - } - write!(buf, " {}", entry.title)?; - writeln!(buf)?; - } - buf - }, - ) -} - -pub fn show_post(entry: &Result) -> Result { - let entry = match entry { - Ok(entry) => entry, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - writeln!(buf, "{}\n", entry.title)?; - if let Some(body) = &entry.body { - writeln!(buf, "{}", body)?; - } - } - buf.wrap_ok() -} - -pub fn show_post_meta(time_style: &TimeStyle, entry: &Result) -> Result { - let entry = match entry { - Ok(entry) => entry, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - writeln!(buf, "Title {}", entry.title)?; - { - write!(buf, "Status")?; - if entry.is_published { - write!(buf, " Published")?; - } else { - write!(buf, " Draft")?; - } - if entry.is_pinned { - write!(buf, " Pinned")?; - } - writeln!(buf)?; - }; - if let Some(body) = &entry.body { - let words_count = words_count::count(body).words; - writeln!(buf, "Words {}", words_count)?; - } - if let Some(tags) = &entry.tags { - if let Some(tags_text) = tags - .clone() - .into_iter() - .reduce(|acc, tag| format!("{}, {}", acc, tag)) - { - writeln!(buf, "Tags {}", tags_text)?; - } - } - let create_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "Create {}", create_time)?; - let modify_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "Modify {}", modify_time)?; - writeln!(buf, "Link https:{}", entry.url)?; - } - buf.wrap_ok() -} - -pub fn show_post_comment( - time_style: &TimeStyle, - comment_iter: Result>, -) -> Result { - let mut comment_iter = match comment_iter { - Ok(entry) => entry, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - comment_iter.try_fold(String::new(), |mut buf, comment| try { - { - let buf = &mut buf; - let create_time = display_cnb_time(&comment.create_time, time_style); - writeln!(buf, "{} {}F", create_time, comment.floor)?; - writeln!(buf, " {} {}", comment.user_name, comment.content)?; - } - buf - }) -} - -pub fn search_self_post( - result: Result<(impl ExactSizeIterator, usize)>, -) -> Result { - let (mut id_iter, total_count) = match result { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - id_iter.try_fold( - format!("{}/{}\n", id_iter.len(), total_count), - |mut buf, id| try { - writeln!(&mut buf, "# {}", id)?; - buf - }, - ) -} - -pub fn search_site_post( - time_style: &TimeStyle, - entry_iter: Result>, -) -> Result { - let entry_iter = match entry_iter { - Ok(o) => o, - Err(e) => return fmt_err(&e).wrap_ok(), - }; - - entry_iter - .map(|entry| try { - let mut buf = String::new(); - { - let buf = &mut buf; - let create_time = display_cnb_time(&entry.create_time, time_style); - writeln!(buf, "{} {}", create_time, entry.url)?; - writeln!(buf, " {}", entry.title)?; - let view_vote_comment_count = format!( - "View {} Vote {} Comment {}", - entry.view_count, entry.vote_count, entry.comment_count - ); - writeln!(buf, " {}", view_vote_comment_count)?; - } - buf - }) - .try_fold(String::new(), |mut acc, buf: Result| try { - writeln!(&mut acc, "{}", buf?)?; - acc - }) -} diff --git a/src/display/normal/user.rs b/src/display/normal/user.rs deleted file mode 100644 index 95971be..0000000 --- a/src/display/normal/user.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::api::user::info::UserInfo; -use crate::display::normal::fmt_err; -use crate::infra::result::WrapResult; -use anyhow::Result; -use std::fmt::Write; -use std::path::PathBuf; - -pub fn login(cfg_path: &Result) -> String { - match cfg_path { - Ok(pb) => format!("PAT was saved in {:?}", pb), - Err(e) => fmt_err(e), - } -} - -pub fn logout(cfg_path: &Result) -> String { - match cfg_path { - Ok(pb) => format!("{:?} was successfully removed", pb), - Err(e) => fmt_err(e), - } -} - -pub fn user_info(info: &Result) -> Result { - let info = match info { - Ok(info) => info, - Err(e) => return fmt_err(e).wrap_ok(), - }; - - let mut buf = String::new(); - { - let buf = &mut buf; - write!(buf, "{}", info.display_name)?; - if info.is_vip { - write!(buf, " VIP")?; - } - writeln!(buf)?; - writeln!( - buf, - "{} Following {} Followers", - info.following_count, info.followers_count - )?; - writeln!(buf, "ID {}", info.blog_id)?; - writeln!(buf, "Joined {}", info.joined)?; - writeln!(buf, "Blog https://www.cnblogs.com/{}", info.blog_app)?; - } - buf.wrap_ok() -} diff --git a/src/infra/fp.rs b/src/infra/fp.rs deleted file mode 100644 index e421b12..0000000 --- a/src/infra/fp.rs +++ /dev/null @@ -1,33 +0,0 @@ -pub mod currying { - #[inline] - pub fn id(x: X) -> impl Fn(X) -> X - where - X: Clone, - { - move |_| x.clone() - } - - #[inline] - pub fn eq(a: T) -> impl Fn(T) -> bool - where - T: PartialEq, - { - move |b| a == b - } - - #[inline] - pub fn lt(a: T) -> impl Fn(T) -> bool - where - T: PartialOrd, - { - move |b| a < b - } - - #[inline] - pub fn gt(a: T) -> impl Fn(T) -> bool - where - T: PartialOrd, - { - move |b| a > b - } -} diff --git a/src/infra/http.rs b/src/infra/http.rs deleted file mode 100644 index 5f34a4f..0000000 --- a/src/infra/http.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::infra::result::WrapResult; -use anyhow::bail; -use anyhow::Result; -use reqwest::header::AUTHORIZATION; -use reqwest::{RequestBuilder, Response}; -use std::ops::Not; - -pub const AUTHORIZATION_TYPE: &str = "Authorization-Type"; -pub const PAT: &str = "pat"; - -#[macro_export] -macro_rules! bearer { - ($token:expr) => {{ - format!("Bearer {}", $token) - }}; -} - -#[macro_export] -macro_rules! basic { - ($token:expr) => {{ - format!("Basic {}", $token) - }}; -} - -pub trait RequestBuilderExt { - fn pat_auth(self, pat: &str) -> RequestBuilder; -} - -impl RequestBuilderExt for RequestBuilder { - fn pat_auth(self, pat: &str) -> RequestBuilder { - let builder = self.header(AUTHORIZATION, bearer!(pat)); - builder.header(AUTHORIZATION_TYPE, PAT) - } -} - -pub trait VecExt { - fn into_query_string(self) -> String; -} - -impl VecExt for Vec<(K, V)> { - fn into_query_string(self) -> String { - self.into_iter() - .map(|(k, v)| { - let s_k = k.to_string(); - let s_v = v.to_string(); - format!("{}={}", s_k, s_v) - }) - .fold(String::new(), |acc, q| format!("{acc}&{q}")) - } -} - -pub async fn unit_or_err(resp: Response) -> Result<()> { - let code = resp.status(); - let body = resp.text().await?; - - if code.is_success().not() { - bail!("{}: {}", code, body); - } - - ().wrap_ok() -} - -pub async fn body_or_err(resp: Response) -> Result { - let code = resp.status(); - let body = resp.text().await?; - - if code.is_success() { - body.wrap_ok() - } else { - bail!("{}: {}", code, body) - } -} diff --git a/src/infra/infer.rs b/src/infra/infer.rs deleted file mode 100644 index 2110108..0000000 --- a/src/infra/infer.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Use this to infer type for val, as same as type ascription -pub const fn infer(val: T) -> T { - val -} diff --git a/src/infra/iter.rs b/src/infra/iter.rs deleted file mode 100644 index 1a7aafa..0000000 --- a/src/infra/iter.rs +++ /dev/null @@ -1,47 +0,0 @@ -use futures::future::{join_all, JoinAll}; -use std::future::Future; - -pub trait IteratorExt: Iterator { - #[inline] - fn dyn_rev<'t>(self, rev: bool) -> Box + 't> - where - Self: DoubleEndedIterator + Sized + 't, - { - if rev { - Box::new(self.rev()) - } else { - Box::new(self) - } - } -} - -impl IteratorExt for I where I: Iterator {} - -pub trait ExactSizeIteratorExt: ExactSizeIterator { - #[inline] - fn dyn_rev<'t>(self, rev: bool) -> Box + 't> - where - Self: DoubleEndedIterator + Sized + 't, - { - if rev { - Box::new(self.rev()) - } else { - Box::new(self) - } - } -} - -impl ExactSizeIteratorExt for I where I: ExactSizeIterator {} - -pub trait IntoIteratorExt: IntoIterator { - #[inline] - fn join_all(self) -> JoinAll - where - Self::Item: Future, - Self: Sized, - { - join_all(self) - } -} - -impl IntoIteratorExt for I where I: IntoIterator {} diff --git a/src/infra/json.rs b/src/infra/json.rs deleted file mode 100644 index d17fec7..0000000 --- a/src/infra/json.rs +++ /dev/null @@ -1,21 +0,0 @@ -use anyhow::{anyhow, Result}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json::Value; - -pub fn serialize(val: T) -> Result -where - T: Serialize, -{ - serde_json::to_value::(val) - .map_err(|e| anyhow!(e)) - .map(|v| v.to_string()) -} - -pub fn deserialize(json: &str) -> Result -where - T: DeserializeOwned, -{ - let val: Value = serde_json::from_str(json)?; - serde_json::from_value::(val).map_err(|e| anyhow!(e)) -} diff --git a/src/infra/mod.rs b/src/infra/mod.rs deleted file mode 100644 index 3c483e1..0000000 --- a/src/infra/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod fp; -pub mod http; -pub mod infer; -pub mod iter; -pub mod json; -pub mod option; -pub mod result; -pub mod str; -pub mod terminal; -pub mod time; -pub mod vec; diff --git a/src/infra/option.rs b/src/infra/option.rs deleted file mode 100644 index e93f9d8..0000000 --- a/src/infra/option.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub trait WrapOption -where - Self: Sized, -{ - #[inline] - fn wrap_some(self) -> Option { - Some(self) - } -} - -impl WrapOption for T {} - -pub trait OptionExt { - fn or_eval_result(self, f: F) -> Result - where - F: FnOnce() -> Result; -} - -impl OptionExt for Option { - #[inline] - fn or_eval_result(self, f: F) -> Result - where - F: FnOnce() -> Result, - { - self.map_or_else(f, |val| Ok(val)) - } -} diff --git a/src/infra/result.rs b/src/infra/result.rs deleted file mode 100644 index 0aad5f3..0000000 --- a/src/infra/result.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::Result; - -pub trait WrapResult -where - Self: Sized, -{ - #[inline] - fn wrap_ok(self) -> Result { - Ok(self) - } - #[inline] - fn wrap_err(self) -> Result { - Err(self) - } -} - -impl WrapResult for T {} - -pub type HomoResult = Result; - -pub trait ResultExt { - fn err_to_string(self) -> Result - where - E: ToString; - - fn homo_string(self) -> HomoResult - where - O: ToString, - E: ToString; -} - -impl ResultExt for Result { - #[inline] - fn err_to_string(self) -> Result - where - E: ToString, - { - self.map_err(|e| e.to_string()) - } - - #[inline] - fn homo_string(self) -> HomoResult - where - O: ToString, - E: ToString, - { - match self { - Ok(o) => Ok(o.to_string()), - Err(e) => Err(e.to_string()), - } - } -} diff --git a/src/infra/str.rs b/src/infra/str.rs deleted file mode 100644 index 8f2e0b2..0000000 --- a/src/infra/str.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::ops::ControlFlow; -use unicode_width::UnicodeWidthChar; - -pub trait StrExt { - fn width_split_head(&self, head_width: usize) -> (&str, &str); - fn width_split(&self, width: usize) -> Option>; -} - -impl StrExt for str { - fn width_split_head(&self, head_width: usize) -> (&str, &str) { - let mut left_take = head_width; - let mut take_bytes = 0; - self.chars().try_for_each(|c| { - let current_width = c.width_cjk().unwrap_or(0); - if left_take > 0 { - if left_take >= current_width { - left_take -= current_width; - take_bytes += c.len_utf8(); - ControlFlow::Continue(()) - } else { - left_take = 0; - ControlFlow::Break(()) - } - } else { - ControlFlow::Break(()) - } - }); - self.split_at(take_bytes) - } - - fn width_split(&self, width: usize) -> Option> { - let mut vec = vec![]; - let mut str = self; - loop { - let (head, tail) = str.width_split_head(width); - // No split strategy exist, return None - if head.is_empty() { - return None; - } - vec.push(head); - if tail.is_empty() { - break; - } - str = tail; - } - Some(vec) - } -} - -#[test] -fn test_width_split_head() { - let text = "测试test⭐"; - assert_eq!(text.width_split_head(0), ("", "测试test⭐")); - assert_eq!(text.width_split_head(1), ("", "测试test⭐")); - assert_eq!(text.width_split_head(2), ("测", "试test⭐")); - assert_eq!(text.width_split_head(3), ("测", "试test⭐")); - assert_eq!(text.width_split_head(4), ("测试", "test⭐")); - assert_eq!(text.width_split_head(5), ("测试t", "est⭐")); - assert_eq!(text.width_split_head(9), ("测试test", "⭐")); - assert_eq!(text.width_split_head(10), ("测试test⭐", "")); - assert_eq!(text.width_split_head(11), ("测试test⭐", "")); -} - -#[test] -fn test_width_split() { - use crate::infra::option::WrapOption; - let text = "测试test⭐测试test⭐"; - assert_eq!(text.width_split(0), None); - assert_eq!(text.width_split(1), None); - assert_eq!( - text.width_split(2), - vec!["测", "试", "te", "st", "⭐", "测", "试", "te", "st", "⭐"].wrap_some() - ); - assert_eq!( - text.width_split(3), - vec!["测", "试t", "est", "⭐", "测", "试t", "est", "⭐"].wrap_some() - ); - assert_eq!( - text.width_split(4), - vec!["测试", "test", "⭐测", "试te", "st⭐"].wrap_some() - ); - assert_eq!( - text.width_split(19), - vec!["测试test⭐测试test", "⭐"].wrap_some() - ); - assert_eq!( - text.width_split(20), - vec!["测试test⭐测试test⭐"].wrap_some() - ); - assert_eq!( - text.width_split(21), - vec!["测试test⭐测试test⭐"].wrap_some() - ); -} diff --git a/src/infra/terminal.rs b/src/infra/terminal.rs deleted file mode 100644 index 7312952..0000000 --- a/src/infra/terminal.rs +++ /dev/null @@ -1,6 +0,0 @@ -use terminal_size::{terminal_size, Width}; - -pub fn get_term_width() -> usize { - let (Width(width), _) = terminal_size().expect("Can not get terminal size"); - width as usize -} diff --git a/src/infra/time.rs b/src/infra/time.rs deleted file mode 100644 index aeb5faf..0000000 --- a/src/infra/time.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::args::TimeStyle; -use chrono::{DateTime, Datelike, Local, TimeZone, Timelike, Utc}; -use std::fmt::Display; - -pub fn display_cnb_time(time_str: &str, time_style: &TimeStyle) -> String { - let rfc3339 = patch_rfc3339(time_str); - let dt = DateTime::parse_from_rfc3339(&rfc3339) - .unwrap_or_else(|_| panic!("Invalid RFC3339: {}", rfc3339)) - .with_timezone(&Utc); - - match time_style { - TimeStyle::Friendly => fmt_time_to_string_friendly(dt.into(), Local::now()), - TimeStyle::Normal => dt.format("%y-%-m-%-d %-H:%M").to_string(), - } -} - -// HACK: -// Sometimes cnblogs' web API returns time string like: "2023-09-12T14:07:00" or "2019-02-06T08:45:53.94" -// This will patch it to standard RFC3339 format -fn patch_rfc3339(time_str: &str) -> String { - if time_str.len() != 25 { - let u8vec: Vec<_> = time_str.bytes().take(19).collect(); - format!( - "{}+08:00", - String::from_utf8(u8vec) - .unwrap_or_else(|_| panic!("Can not patch time string: {}", time_str)) - ) - } else { - time_str.to_owned() - } -} - -fn fmt_time_to_string_friendly(time_to_fmt: DateTime, current_time: DateTime) -> String -where - T: TimeZone, - ::Offset: Display, -{ - let diff = current_time.clone() - time_to_fmt.clone(); - match diff { - // In the future - _ if diff.num_milliseconds() < 0 => time_to_fmt.format("%y-%-m-%-d %-H:%M").to_string(), - // Same year... - _ if time_to_fmt.year() != current_time.year() => { - time_to_fmt.format("%Y-%m-%d").to_string() - } - _ if time_to_fmt.month() != current_time.month() => { - time_to_fmt.format("%m-%d %H:%M").to_string() - } - _ if time_to_fmt.day() != current_time.day() => { - let postfix = match time_to_fmt.day() { - 1 => "st", - 2 => "nd", - 3 => "rd", - _ => "th", - }; - time_to_fmt - .format(&format!("%d{} %H:%M", postfix)) - .to_string() - } - _ if time_to_fmt.hour() != current_time.hour() => time_to_fmt.format("%H:%M").to_string(), - // Within an hour - _ if diff.num_seconds() < 30 => "Now".to_owned(), - _ if diff.num_minutes() < 3 => "Recently".to_owned(), - _ if diff.num_minutes() < 30 => format!("{}m", diff.num_minutes()), - _ => time_to_fmt.format("%H:%M").to_string(), - } -} diff --git a/src/infra/vec.rs b/src/infra/vec.rs deleted file mode 100644 index 06d70c1..0000000 --- a/src/infra/vec.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub trait VecExt { - fn chain_push(self, item: T) -> Vec; -} - -impl VecExt for Vec { - #[inline] - fn chain_push(mut self, item: T) -> Self { - self.push(item); - self - } -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..776a72f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +//! +//! 博客园 API和cli的封装 +//! + +pub mod api; +pub mod commands; +pub mod context; +pub mod display; +pub mod logic; +pub mod models; +pub mod tools; diff --git a/src/logic/fav.rs b/src/logic/fav.rs new file mode 100644 index 0000000..cf71a15 --- /dev/null +++ b/src/logic/fav.rs @@ -0,0 +1,20 @@ +use anyhow::Result; + +use crate::{ + api, + commands::fav::{FaverateAction, FaverateCommand, ListArgs}, + context::Context, +}; + +pub async fn endpoint(cmd: FaverateCommand, ctx: &mut Context) -> Result<()> { + match cmd.commands { + FaverateAction::List(arg) => handle_list(arg, ctx).await, + } +} +async fn handle_list(arg: ListArgs, ctx: &mut Context) -> Result<()> { + api::fav::list_bookmarks(&ctx.client, arg) + .await? + .into_iter() + .for_each(|x| ctx.terminal.writeln(x.into_format()).unwrap()); + Ok(()) +} diff --git a/src/logic/ing.rs b/src/logic/ing.rs new file mode 100644 index 0000000..8c0b65b --- /dev/null +++ b/src/logic/ing.rs @@ -0,0 +1,111 @@ +//! +//! 闪存相关逻辑 +//! + +use anyhow::{Ok, Result}; + +use crate::context::Context; +use crate::tools::http::IntoNoParseResult; +use crate::{ + api, + commands::ing::{ + IngAction, IngContent, IngDelete, IngListArg, IngReplayContent, IngShowDetail, + }, +}; + +pub async fn endpoint(cmd: IngAction, ctx: &mut Context) -> Result<()> { + match cmd { + IngAction::List(action) => handle_list_action(action, ctx).await?, + IngAction::Create(action) => handle_create_action(action, ctx).await?, + IngAction::Delete(action) => handle_delete_action(action, ctx).await?, + IngAction::Replay(action) => handle_replay_action(action, ctx).await?, + IngAction::Show(action) => handle_show_action(action, ctx).await?, + }; + Ok(()) +} + +/// 查看实现 +async fn handle_list_action(action: IngListArg, ctx: &mut Context) -> Result<()> { + let resp = api::ing::list_statuses(&ctx.client, action.r#type.as_str(), &action).await?; + + match (action.no_comment, ctx.json) { + // 不显示评论,json格式输出。 + (true, true) => { + ctx.terminal.writeln(serde_json::to_string_pretty(&resp)?)?; + } + // 不显示评论,pretty输出。 + (true, false) => { + resp.into_iter() + .for_each(|x| ctx.terminal.writeln(x.format()).unwrap()); + } + // 显示评论,json格式输出。 + (false, true) => { + for i in resp { + let ing = api::ing::get_status_with_comment(&ctx.client, i).await?; + ctx.terminal.writeln(serde_json::to_string_pretty(&ing)?)?; + } + } + // 显示评论,pretty输出。 + (false, false) => { + for v in resp { + ctx.terminal.writeln( + api::ing::get_status_with_comment(&ctx.client, v) + .await? + .format(), + )?; + } + } + } + + Ok(()) +} + +/// 删除实现 +async fn handle_delete_action(action: IngDelete, ctx: &mut Context) -> Result<()> { + let a = if let Some(cid) = action.comment_id { + api::ing::raw_delete_status_comment(&ctx.client, action.id, cid) + .await? + .into_no_parse_result() + .await? + } else { + api::ing::raw_delete_status(&ctx.client, action.id) + .await? + .into_no_parse_result() + .await? + }; + ctx.terminal.writeln(a.into_format()) +} + +/// 发布闪存实现 +async fn handle_create_action(action: IngContent, ctx: &mut Context) -> Result<()> { + action.validate()?; + let a = api::ing::raw_create_status(&ctx.client, action.to_json()) + .await? + .into_no_parse_result() + .await?; + + ctx.terminal.writeln(a.into_format()) +} + +/// 回复功能实现 +async fn handle_replay_action(action: IngReplayContent, ctx: &mut Context) -> Result<()> { + let a = api::ing::raw_create_comment(&ctx.client, action.id, action.content) + .await? + .into_no_parse_result() + .await?; + + ctx.terminal.writeln(a.into_format()) +} + +/// 处理单条展示信息 +async fn handle_show_action(action: IngShowDetail, ctx: &mut Context) -> Result<()> { + let res = api::ing::get_status(&ctx.client, action.id).await?; + + // 显示评论 + if action.comments { + let r = api::ing::get_status_with_comment(&ctx.client, res).await?; + ctx.terminal.writeln(r.format()) + } else { + ctx.terminal.writeln(res.format()) + } +} diff --git a/src/logic/mod.rs b/src/logic/mod.rs new file mode 100644 index 0000000..290ee2d --- /dev/null +++ b/src/logic/mod.rs @@ -0,0 +1,30 @@ +//! +//! cli操作逻辑 +//! +//! 此模块暂定封装操作逻辑,比如是闪存的curd,闪存评论的curd。 +//! + +pub mod fav; +pub mod ing; +pub mod news; +pub mod post; +pub mod user; + +use anyhow::Result; + +use crate::commands::{Cli, Commands}; +use crate::context::Context; + +pub async fn run(cli: Cli, ctx: &mut Context) -> Result<()> { + match cli.commands { + Commands::User(action) => { + user::endpoint(action, ctx).await?; + } + Commands::Fav(action) => fav::endpoint(action, ctx).await?, + Commands::Ing { action } => ing::endpoint(action, ctx).await?, + Commands::News(action) => news::endpoint(action, ctx).await?, + Commands::Post(action) => post::endpoint(action, ctx).await?, + } + + Ok(()) +} diff --git a/src/logic/news.rs b/src/logic/news.rs new file mode 100644 index 0000000..c1e7c97 --- /dev/null +++ b/src/logic/news.rs @@ -0,0 +1,21 @@ +use anyhow::Result; + +use crate::{ + api, + commands::news::{ListArgs, NewsAction, NewsCommand}, + context::Context, +}; + +pub async fn endpoint(cmd: NewsCommand, ctx: &mut Context) -> Result<()> { + match cmd.commands { + NewsAction::List(arg) => handle_list(arg, ctx).await, + } +} + +async fn handle_list(arg: ListArgs, ctx: &mut Context) -> Result<()> { + api::news::list_news(&ctx.client, arg) + .await? + .into_iter() + .for_each(|x| ctx.terminal.writeln(x.into_format()).unwrap()); + Ok(()) +} diff --git a/src/logic/post.rs b/src/logic/post.rs new file mode 100644 index 0000000..dafe551 --- /dev/null +++ b/src/logic/post.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use owo_colors::OwoColorize; +use termimad::MadSkin; + +use crate::{ + api, + commands::post::{ListArgs, PostAction, PostCommand, ReplayArgs, ShowArgs}, + context::Context, +}; + +pub async fn endpoint(cmd: PostCommand, ctx: &mut Context) -> anyhow::Result<()> { + match cmd.commands { + // PostAction::Create => handle(ctx).await, + PostAction::List(arg) => handle_list(arg, ctx).await, + PostAction::Replay(arg) => handle_replay(arg, ctx).await, + PostAction::Show(arg) => handle_show(arg, ctx).await, + // PostAction::Update => handle(ctx).await, + } +} + +async fn handle_list(arg: ListArgs, ctx: &mut Context) -> Result<()> { + let name = if let Some(n) = &arg.name { + n + } else { + &ctx.cache.blog_app + }; + + let post = api::post::list_someone_post(&ctx.client, name, &arg).await?; + for i in post { + ctx.terminal.writeln(i.into_format())?; + } + Ok(()) +} + +async fn handle_replay(arg: ReplayArgs, ctx: &mut Context) -> Result<()> { + let name = if let Some(n) = &arg.name { + n + } else { + &ctx.cache.blog_app + }; + let resp = api::post::raw_create_comment(&ctx.client, name, arg.id, &arg).await?; + + if resp.status().is_success() { + ctx.terminal + .writeln(format!("[Ok] {}", resp.status().bright_green())) + } else { + ctx.terminal.writeln(format!( + "[Err] {}: {}", + resp.status().red(), + resp.text().await?.red() + )) + } + // Ok(()) +} + +async fn handle_show(arg: ShowArgs, ctx: &mut Context) -> Result<()> { + let resp = api::post::raw_show_post(&ctx.client, arg.id) + .await? + .text() + .await?; + + // TODO: 优化 + // 处理快平台 + let resp: String = serde_json::from_str(&resp)?; + let md = html2md::parse_html(&resp); + let mds = MadSkin::default_light(); + mds.write_text_on(&mut ctx.terminal.stdout, &md)?; + Ok(()) +} + +/// API 调用401,目前无法使用。 +#[allow(unused)] +async fn handle_comments(arg: ShowArgs, ctx: &mut Context) -> Result<()> { + let resp = api::post::raw_list_comments(&ctx.client, &ctx.cache.blog_app, arg.id, arg).await?; + ctx.terminal.writeln(resp.status())?; + ctx.terminal.writeln(resp.text().await?) +} diff --git a/src/logic/user.rs b/src/logic/user.rs new file mode 100644 index 0000000..f01bdab --- /dev/null +++ b/src/logic/user.rs @@ -0,0 +1,70 @@ +//! +//! 认证模块 +//! + +use anyhow::Result; +use owo_colors::OwoColorize; +use reqwest::header::{AUTHORIZATION, HeaderMap}; +use reqwest::{ClientBuilder, StatusCode}; + +use crate::commands::user::{UserAction, UserCommand}; +use crate::context::Context; +use crate::context::config::Cache; +use crate::tools::http::IntoNoParseResult; +use crate::{api, models}; + +pub async fn endpoint(cmd: UserCommand, ctx: &mut Context) -> anyhow::Result<()> { + match cmd.commands { + UserAction::Login { token } => handle_login(token, ctx).await, + UserAction::Logout => handle_logout(ctx), + UserAction::Status => user_info(ctx).await, + UserAction::Token => handle_print_token(ctx), + } +} + +async fn handle_login(token: String, ctx: &mut Context) -> Result<()> { + let header_value = format!("Bearer {}", token); + let mut header = HeaderMap::new(); + header.insert(AUTHORIZATION, header_value.parse()?); + + let client = ClientBuilder::new().default_headers(header).build()?; + let resp = api::user::raw_user_info(&client).await?; + + if resp.status().eq(&StatusCode::UNAUTHORIZED) { + let _ = ctx + .terminal + .writeln(format!("Token `{}`错误。请输入正确的token。", token).red()); + } + + if resp.status().is_success() { + let p = resp.json::().await?; + let name = p.display_name.clone(); + + let mut c: Cache = p.into(); + c.token = token; + ctx.save_cache(c)?; + + ctx.terminal + .writeln(format!("🎉 欢迎,{}!", name.bright_green()))?; + } else { + let r = resp.into_no_parse_result().await?; + ctx.terminal.writeln(r.into_format())?; + } + Ok(()) +} + +fn handle_print_token(ctx: &mut Context) -> Result<()> { + ctx.terminal + .writeln(format!("[Token]: {}", ctx.cache.token.bright_green())) +} + +async fn user_info(ctx: &mut Context) -> Result<()> { + let user = api::user::user_info(&ctx.client).await?; + let c: Cache = user.clone().into(); + ctx.save_cache(c)?; + ctx.terminal.writeln(user.format_user_info()) +} + +fn handle_logout(ctx: &Context) -> Result<()> { + ctx.clean() +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7b361dc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,227 +0,0 @@ -#![feature(try_blocks)] -#![feature(if_let_guard)] -#![feature(let_chains)] -#![feature(type_name_of_val)] -#![feature(iterator_try_collect)] -#![feature(iterator_try_reduce)] -#![warn(clippy::all, clippy::nursery, clippy::cargo_common_metadata)] - -use crate::api::auth::session; -use crate::api::fav::Fav; -use crate::api::ing::Ing; -use crate::api::news::News; -use crate::api::post::Post; -use crate::api::user::User; -use crate::args::cmd::post::{CreateCmd, UpdateCmd}; -use crate::args::parser::no_operation; -use crate::args::{parser, Args}; -use crate::infra::fp::currying::eq; -use crate::infra::infer::infer; -use crate::infra::iter::{ExactSizeIteratorExt, IntoIteratorExt}; -use crate::infra::option::OptionExt; -use crate::infra::result::WrapResult; -use anyhow::Result; -use clap::Parser; -use clap::{Command, CommandFactory}; -use colored::Colorize; -use std::env; - -pub mod api; -pub mod args; -pub mod display; -pub mod infra; - -fn show_non_printable_chars(text: String) -> String { - #[inline] - fn make_red(str: &str) -> String { - format!("{}", str.red()) - } - - text.replace(' ', &make_red("·")) - .replace('\0', &make_red("␀\0")) - .replace('\t', &make_red("␉\t")) - .replace('\n', &make_red("␊\n")) - .replace('\r', &make_red("␍\r")) - .replace("\r\n", &make_red("␍␊\r\n")) -} - -fn panic_if_err(result: &Result) { - if let Err(e) = result { - panic!("{}", e) - } -} - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<()> { - let args_vec = env::args().collect::>(); - if args_vec.iter().any(eq(&"--debug".to_owned())) { - dbg!(args_vec); - } - - let args: Args = Args::parse(); - let global_opt = &args.global_opt; - if global_opt.debug { - dbg!(&args); - } - - let pat = global_opt.with_pat.clone().or_eval_result(session::get_pat); - let style = &global_opt.style; - let time_style = &global_opt.time_style; - let rev = args.rev; - let foe = global_opt.fail_on_error; - - let output = match args { - _ if let Some(pat) = parser::user::login(&args) => { - let cfg_path = session::login(pat); - foe.then(|| panic_if_err(&cfg_path)); - display::login(style, &cfg_path) - } - _ if parser::user::logout(&args) => { - let cfg_path = session::logout(); - foe.then(|| panic_if_err(&cfg_path)); - display::logout(style, &cfg_path) - } - _ if parser::user::user_info(&args) => { - let user_info = User::new(pat?).get_info().await; - foe.then(|| panic_if_err(&user_info)); - display::user_info(style, &user_info)? - } - _ if let Some((skip, take, r#type, align)) = parser::ing::list_ing(&args) => { - let ing_with_comment_iter = infer::>(try { - let ing_api = Ing::new(pat?); - let ing_vec = ing_api.get_list(skip, take, &r#type).await?; - ing_vec.into_iter() - .map(|ing| async { - let result = ing_api.get_comment_list(ing.id).await; - result.map(|comment_vec| (ing, comment_vec)) - }) - .join_all() - .await - .into_iter() - .collect::>>()? - }).map(|vec| vec.into_iter().dyn_rev(rev)); - foe.then(|| panic_if_err(&ing_with_comment_iter)); - display::list_ing(style, time_style, ing_with_comment_iter, align)? - } - _ if let Some(content) = parser::ing::publish_ing(&args) => { - let content = try { - Ing::new(pat?).publish(content).await?; - content - }; - foe.then(|| panic_if_err(&content)); - display::publish_ing(style, &content) - } - _ if let Some((content, id)) = parser::ing::comment_ing(&args) => { - let content = try { - Ing::new(pat?).comment(id, content.clone(), None, None).await?; - content - }; - foe.then(|| panic_if_err(&content)); - display::comment_ing(style, &content) - } - _ if let Some(id) = parser::post::show_post(&args) => { - let entry = Post::new(pat?).get_one(id).await; - foe.then(|| panic_if_err(&entry)); - display::show_post(style, &entry)? - } - _ if let Some(id) = parser::post::show_post_meta(&args) => { - let entry = Post::new(pat?).get_one(id).await; - foe.then(|| panic_if_err(&entry)); - display::show_post_meta(style, time_style, &entry)? - } - _ if let Some(id) = parser::post::show_post_comment(&args) => { - let comment_iter = Post::new(pat?) - .get_comment_list(id).await - .map(|vec| vec.into_iter().dyn_rev(rev)); - foe.then(|| panic_if_err(&comment_iter)); - display::show_post_comment(style, time_style, comment_iter)? - } - _ if let Some((skip, take)) = parser::post::list_post(&args) => { - let meta_iter = Post::new(pat?) - .get_meta_list(skip, take) - .await - .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); - foe.then(|| panic_if_err(&meta_iter)); - display::list_post(style, meta_iter)? - } - _ if let Some(id) = parser::post::delete_post(&args) => { - let id = try { - Post::new(pat?).del_one(id).await?; - id - }; - foe.then(|| panic_if_err(&id)); - display::delete_post(style, &id) - } - _ if let Some((kw, skip, take)) = parser::post::search_self_post(&args) => { - let result = Post::new(pat?) - .search_self(skip, take, kw) - .await - .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); - foe.then(|| panic_if_err(&result)); - display::search_self_post(style, result)? - } - _ if let Some((kw, skip, take)) = parser::post::search_site_post(&args) => { - let result = Post::new(pat?) - .search_site(skip, take, kw) - .await - .map(|vec | vec.into_iter().dyn_rev(rev)); - foe.then(|| panic_if_err(&result)); - display::search_site_post(style, time_style, result)? - } - _ if let Some(create_cmd) = parser::post::create_post(&args) => { - let CreateCmd { title, body, publish } = create_cmd; - let id = Post::new(pat?).create(title, body, *publish).await; - foe.then(|| panic_if_err(&id)); - display::create_post(style, &id) - } - _ if let Some((id, update_cmd)) = parser::post::update_post(&args) => { - let UpdateCmd { title, body, publish } = update_cmd; - let id = Post::new(pat?).update(id, title, body, publish).await; - foe.then(|| panic_if_err(&id)); - display::update_post(style, &id) - } - _ if let Some((skip, take)) = parser::news::list_news(&args) => { - let news_iter = News::new(pat?) - .get_list(skip, take) - .await - .map(|vec| vec.into_iter().dyn_rev(rev)); - foe.then(|| panic_if_err(&news_iter)); - display::list_news(style, time_style, news_iter)? - } - _ if let Some((skip, take)) = parser::fav::list_fav(&args) => { - let fav_iter = Fav::new(pat?) - .get_list(skip, take) - .await - .map(|vec| vec.into_iter().dyn_rev(rev)); - foe.then(|| panic_if_err(&fav_iter)); - display::list_fav(style, time_style, fav_iter)? - } - - _ if no_operation(&args) => - infer::(Args::command()).render_help().to_string(), - _ => "Invalid usage, follow '--help' for more information".to_owned() - }; - - if global_opt.quiet { - return ().wrap_ok(); - } - - let output = { - let output = if output.ends_with("\n\n") { - output[..output.len() - 1].to_owned() - } else if output.ends_with('\n') { - output - } else { - format!("{}\n", output) - }; - if global_opt.debug { - show_non_printable_chars(output) - } else { - output - } - }; - - print!("{}", output); - - ().wrap_ok() -} diff --git a/src/models/fav.rs b/src/models/fav.rs new file mode 100644 index 0000000..11f00ff --- /dev/null +++ b/src/models/fav.rs @@ -0,0 +1,38 @@ +// "WzLinkId": 5262184, +// "Title": "MongoDB数据库 - suoning - 博客园", +// "LinkUrl": "https://www.cnblogs.com/suoning/p/6759367.html#3682005", +// "Summary": "", +// "Tags": [], +// "DateAdded": "2019-04-05T23:33:05.263", +// "FromCNBlogs": true + +use chrono::NaiveDateTime; +use owo_colors::OwoColorize; +use serde::Deserialize; + +use crate::tools::timer::DateFormatExt; + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "PascalCase")] +#[serde(default)] +pub struct FavInfo { + pub wz_link_id: u64, + pub title: String, + pub link_url: String, + pub summary: Option, + pub tags: Vec, + pub date_added: NaiveDateTime, + pub from_c_n_blogs: bool, +} + +impl FavInfo { + pub fn into_format(self) -> String { + format!( + "{title} [#{id}][收藏@ {date}]\n{link}\n", + title = self.title.bold().cyan(), + id = self.wz_link_id.bright_green(), + date = self.date_added.as_time_age(), + link = self.link_url.blue(), + ) + } +} diff --git a/src/models/ing.rs b/src/models/ing.rs new file mode 100644 index 0000000..dc1eadd --- /dev/null +++ b/src/models/ing.rs @@ -0,0 +1,108 @@ +use chrono::NaiveDateTime; +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; + +use crate::{ + display::ing::{tag_comment_count, tag_lucky, tag_public, tag_sender}, + tools::strings::{ExtractAtPeople, ExtractIconAlt}, + tools::timer::DateFormatExt, +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct IngInfo { + pub id: u64, + pub content: String, + pub is_private: bool, + pub is_lucky: bool, + pub comment_count: u64, + pub date_added: NaiveDateTime, + pub user_alias: String, + pub user_display_name: String, + pub user_icon_url: String, + pub user_id: u64, + pub user_guid: String, + pub send_from: u8, + pub icons: String, +} + +impl IngInfo { + pub fn format(self) -> String { + let content = self.content.extract_name().unwrap(); + let icon_tag = self.icons.extract_tag().unwrap(); + format!( + "{name}:{content}{icon_tag}{emoji:>4}{time}[#{id}]{lucky}{public}{sender}{comments}", + name = self.user_display_name.blue(), + content = content, + icon_tag = icon_tag, + emoji = "📎", + time = self.date_added.as_time_age().blue(), + id = self.id.bright_green(), + lucky = tag_lucky(self.is_lucky), + public = tag_public(self.is_private), + sender = tag_sender(self.send_from), + comments = tag_comment_count(self.comment_count) + ) + // + // format!( + // "[#{id}]{name}:{content}{icon_tag}{emoji:>4}{time}{lucky}{public}{sender}{comments}", + // name = self.user_display_name.blue(), + // content = content, + // icon_tag = icon_tag, + // emoji = "📎", + // time = self.date_added.as_time_ago().blue(), + // id = self.id.bright_green(), + // lucky = tag_lucky(self.is_lucky), + // public = tag_public(self.is_private), + // sender = tag_sender(self.send_from), + // comments = tag_comment_count(self.comment_count) + // ) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct IngComment { + pub id: u64, + pub content: String, + pub date_added: NaiveDateTime, + pub status_id: u64, + pub user_alias: String, + pub user_display_name: String, + pub user_icon_url: String, + pub user_id: u64, + pub user_guid: String, +} + +impl IngComment { + pub fn format(self) -> String { + let content = self.content.extract_name().unwrap(); + // let icon_tag = self.icons.extract_tag().unwrap(); + format!( + "{name}:{content}{emoji:>4}{time}[#{id}]", + name = self.user_display_name.blue(), + content = content, + emoji = "📎", + time = self.date_added.as_time_age(), + id = self.id.bright_green(), + ) + } +} + +#[derive(Debug, Serialize)] +pub struct IngDetail { + #[serde(flatten)] + pub status: IngInfo, + pub comments: Vec, +} + +impl IngDetail { + pub fn format(self) -> String { + let res = format!("{}\n", self.status.format()); + + self.comments.into_iter().fold(res, |mut acc, x| { + acc = format!("{}{:>4}{}\n", acc, "", x.format()); + acc + }) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..bb70b45 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,44 @@ +pub mod fav; +pub mod ing; +pub mod news; +pub mod post; +pub mod user; + +use owo_colors::OwoColorize; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "PascalCase")] +pub struct NoParseResult { + #[serde(skip)] + pub status_code: u16, + #[serde(skip)] + pub is_success: bool, + // pub target_site: String, + pub message: String, + // pub data: String, + // pub inner_exception: String, + // pub help_link: String, + // pub source: String, + // pub h_result: u64, + // pub stack_trace: String, +} + +impl NoParseResult { + pub fn into_format(self) -> String { + if self.is_success { + format!( + "[{info}] {status_code}", + info = "Ok".bright_green(), + status_code = self.status_code, + ) + } else { + format!( + "[{info}] {status_code}: {msg}", + info = "Error".red(), + status_code = self.status_code, + msg = self.message + ) + } + } +} diff --git a/src/models/news.rs b/src/models/news.rs new file mode 100644 index 0000000..b697306 --- /dev/null +++ b/src/models/news.rs @@ -0,0 +1,58 @@ +use chrono::{DateTime, Utc}; +use owo_colors::OwoColorize; +use serde::Deserialize; + +use crate::tools::timer::DateFormatExt; + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "PascalCase")] +pub struct NewsInfo { + pub id: u64, + pub title: String, + pub summary: String, + pub topic_id: u64, + pub topic_icon: Option, + pub view_count: u64, + pub comment_count: u64, + pub digg_count: u64, + #[serde(with = "crate::tools::timer::rfc3339_or_naive")] + pub date_added: DateTime, +} + +impl NewsInfo { + pub fn into_format(self) -> String { + format!( + "{title}\n{summary}\n[#{id}][posted@ {date}][浏览:{vc}][评论:{cc}][点赞:{dc}]\n", + title = self.title.bold().cyan(), + summary = self.summary, + id = self.id.bright_green(), + date = self.date_added.as_time_age(), + vc = self.view_count, + cc = self.comment_count, + dc = self.digg_count, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse() { + let a = r#"{ + "Id": 813264, + "Title": "马斯克兑现承诺,开源X推荐算法!100% AI驱动,0人工规则", + "Summary": "新智元报道 编辑:定慧 马斯克兑现承诺,X平台全新推荐算法正式开源!这套由 Grok 驱动的 AI 系统,完全取代了人工规则,通过 15 种行为预测精准计算每条帖子的命运。 1 月 11 日,马斯克在X平台上发了一条帖子,宣布将在 7 天内开源X平台全新的推荐算法。 他还承诺,此后每 4 周重复一次", + "TopicId": 570, + "TopicIcon": "https://images0.cnblogs.com/news_topic/20150702090016187.png", + "ViewCount": 37, + "CommentCount": 0, + "DiggCount": 2, + "DateAdded": "2026-01-20T15:40:00+08:00" + }"#; + + let aa: NewsInfo = serde_json::from_str(a).unwrap(); + assert_eq!(aa.id, 813264); + } +} diff --git a/src/models/post.rs b/src/models/post.rs new file mode 100644 index 0000000..2a30366 --- /dev/null +++ b/src/models/post.rs @@ -0,0 +1,121 @@ +use chrono::NaiveDateTime; +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +#[serde(default)] +#[serde(rename_all = "PascalCase")] +pub struct PostInfo { + pub id: u64, + pub title: String, + pub url: String, + pub description: String, + pub author: String, + pub blog_app: String, + pub avatar: String, + pub post_date: NaiveDateTime, + pub view_count: u32, + pub comment_count: u32, + pub digg_count: u32, +} + +impl PostInfo { + pub fn into_format(self) -> String { + let view_count = if self.view_count != 0 { + format!("[阅读数:{}]", self.view_count) + } else { + "".to_string() + }; + + let comment_count = if self.comment_count != 0 { + format!("[评论数:{}]", self.comment_count) + } else { + "".to_string() + }; + + let digg_count = if self.digg_count != 0 { + format!("[点赞数:{}]", self.digg_count) + } else { + "".to_string() + }; + format!( + "[#{id}] {title}{space:>4}[{author}]{view_count}{comment_count}{digg_count}", + id = self.id.bright_green(), + title = self.title.bold().bright_magenta(), + author = self.author, + space = "", + // date = self.post_date + ) + } + + pub fn as_markdown_header(&self) -> String { + format!("# {title}\n", title = self.title) + } + + pub fn as_markdown_foot(&self) -> String { + format!( + "\n[阅读数:{vc}] [评论数:{cc}] [点赞数:{dc}] [创建时间:{time}]", + vc = self.view_count, + cc = self.comment_count, + dc = self.digg_count, + time = self.post_date, + ) + } +} + +pub struct PostContent { + pub info: PostInfo, + pub content: String, +} + +impl PostContent { + pub fn into_format(self) -> String { + let body = html2md::parse_html(&self.content); + + format!( + "{header}{body}{footer}", + header = self.info.as_markdown_header(), + footer = self.info.as_markdown_foot() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const A: &str = r#"[ + { + "Id": 19276632, + "Title": "Android 模拟器root权限", + "Url": "https://www.cnblogs.com/linga/p/19276632", + "Description": "前置: adb,Adnroid Studio Emulator,在命令行可执行,或者通过绝对路径执行 创建模拟器 首先,启动Android Studio并创建一个模拟器AVD(Android虚拟设备)。在创建AVD时请务必注意服务类型(Google Play Store,Google APIs,An", + "Author": "咕咚!", + "BlogApp": "linga", + "Avatar": "https://pic.cnblogs.com/face/1284904/20180727165140.png", + "PostDate": "2025-11-27T11:32:00", + "ViewCount": 83, + "CommentCount": 0, + "DiggCount": 0 + }, + { + "Id": 18193317, + "Title": "Docker 编译安装Nginx正向代理", + "Url": "https://www.cnblogs.com/linga/p/18193317", + "Description": "先记录一波正向代理 目录: . ├── Dockerfile ├── Dockerfile.bak ├── nginx-1.24.0.tar.gz ├── nginx.conf ├── openssl-3.3.0.tar.gz ├── pcre2-10.43.tar.bz2 ├── pcre-8.4", + "Author": "咕咚!", + "BlogApp": "linga", + "Avatar": "https://pic.cnblogs.com/face/1284904/20180727165140.png", + "PostDate": "2024-05-15T10:28:00", + "ViewCount": 256, + "CommentCount": 0, + "DiggCount": 0 + } +]"#; + + #[test] + fn test_parse() { + let a: Vec = serde_json::from_str(A).unwrap(); + assert_eq!(a[0].id, 19276632) + } +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..bb8681d --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,45 @@ +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UserInfo { + pub user_id: String, + #[serde(rename = "SpaceUserID")] + pub space_user_id: u64, + pub account_id: u64, + pub blog_id: u64, + pub display_name: String, + pub face: String, + pub avatar: String, + pub seniority: String, + pub blog_app: String, + pub following_count: u64, + pub follower_count: u64, + pub is_vip: bool, + pub joined: String, +} + +impl UserInfo { + /// 提取公共的用户信息格式化逻辑 + pub fn format_user_info(&self) -> String { + let mut info = Vec::new(); + + info.push(if self.is_vip { + format!("{}[VIP]", self.display_name).red().to_string() + } else { + self.display_name.to_string().blue().to_string() + }); + + info.push(format!("ID:{}", self.account_id)); + info.push(format!("加入时间:{}", self.joined)); + info.push(format!("博客:https://www.cnblogs.com/{}", self.blog_app)); + info.push("📊 数据统计".into()); + info.push(format!("├─ 关注:{} 人 ", self.following_count)); + info.push(format!("├─ 粉丝:{} 人 ", self.follower_count)); + if !self.seniority.is_empty() { + info.push(format!("└─ 园龄:{}", self.seniority)); + } + info.join("\n") + } +} diff --git a/src/tools/http.rs b/src/tools/http.rs new file mode 100644 index 0000000..6c28e90 --- /dev/null +++ b/src/tools/http.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +// use owo_colors::OwoColorize; +use reqwest::Response; + +use crate::models::NoParseResult; + +// #[allow(async_fn_in_trait)] +// pub trait ResponseExt { +// async fn ensure_success(self) -> Result; +// } + +// impl ResponseExt for Response { +// async fn ensure_success(self) -> Result { +// let status = self.status(); + +// if status.is_success() { +// Ok(self) +// } else { +// let body = self.text().await?; +// anyhow::bail!( +// "HTTP Error: status={} {}, body: {}", +// status.as_u16(), +// status.canonical_reason().unwrap_or("Unknown"), +// body +// ) +// } +// } +// } + +// #[allow(async_fn_in_trait)] +// pub trait ResponseWrite { +// async fn write_to_stdout(self) -> Result<()>; +// } + +// impl ResponseWrite for Response { +// async fn write_to_stdout(self) -> Result<()> { +// let status = self.status(); +// if status.is_success() { +// println!("{}", format!("Ok: {}", status).bright_green()); +// } else { +// let body = self.text().await?; +// println!( +// "{}", +// format!( +// "HTTP Error: status={} {}, body: {}", +// status.as_u16(), +// status.canonical_reason().unwrap_or("Unknown"), +// body +// ) +// .red() +// ); +// } +// Ok(()) +// } +// } + +#[allow(async_fn_in_trait)] +pub trait IntoNoParseResult { + async fn into_no_parse_result(self) -> Result; +} + +impl IntoNoParseResult for Response { + async fn into_no_parse_result(self) -> Result { + let mut res = NoParseResult::default(); + if self.status().is_success() { + res.status_code = self.status().as_u16(); + res.is_success = self.status().is_success(); + } else { + let status = self.status(); + res = self.json().await?; + res.status_code = status.as_u16(); + res.is_success = status.is_success(); + } + + Ok(res) + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs new file mode 100644 index 0000000..dbdf916 --- /dev/null +++ b/src/tools/mod.rs @@ -0,0 +1,15 @@ +pub mod http; +pub mod strings; +pub mod timer; + +use anyhow::Result; + +pub trait IntoAnyhowResult { + fn into_anyhow_result(self) -> Result; +} + +impl IntoAnyhowResult for reqwest::Result { + fn into_anyhow_result(self) -> Result { + Ok(self?) + } +} diff --git a/src/tools/strings.rs b/src/tools/strings.rs new file mode 100644 index 0000000..1240162 --- /dev/null +++ b/src/tools/strings.rs @@ -0,0 +1,80 @@ +use anyhow::{Result, anyhow}; +use owo_colors::OwoColorize; + +// 预编译正则表达式以提升性能 +lazy_static::lazy_static! { + pub static ref AT_PEOPLE_REGEX: regex::Regex = + regex::Regex::new(r"]*>([^<]*)") + .expect("无效的 @ 人员正则表达式"); + + pub static ref ICON_ALT_REGEX: regex::Regex = + regex::Regex::new(r#"]*alt\s*=\s*["']([^"']*)["'][^>]*>"#) + .expect("无效的图标正则表达式"); +} +/// 如果文本中还有a标签链接,则进一步处理,将超链接替换成链接内容, +/// 主要是针对@xxx情况。 +pub trait ExtractAtPeople { + fn extract_name(self) -> Result; +} + +impl ExtractAtPeople for String { + fn extract_name(self) -> Result { + // 使用预编译的正则表达式 + let re = &AT_PEOPLE_REGEX; + + if !re.is_match(&self) { + return Ok(self); + } + + let mut last_end = 0; + let mut output = Self::with_capacity(self.len()); + + // 更高效的单次遍历替换 + for caps in re.captures_iter(&self) { + let m = caps.get(0).ok_or_else(|| anyhow!("未找到匹配项"))?; + let link_text = caps.get(1).ok_or_else(|| anyhow!("未找到链接文本"))?; + + // 添加匹配前的文本 + output.push_str(&self[last_end..m.start()]); + + // 添加着色后的链接文本 + output.push_str(&link_text.as_str().blue().to_string()); + + last_end = m.end(); + } + + // 添加最后一段文本 + if last_end < self.len() { + output.push_str(&self[last_end..]); + } + + Ok(output) + } +} + +pub trait ExtractIconAlt { + fn extract_tag(self) -> Result; +} + +impl ExtractIconAlt for String { + fn extract_tag(self) -> Result { + // 使用预编译的正则表达式 + let re = &ICON_ALT_REGEX; + + if let Some(caps) = re.captures(&self) { + let alt_text = caps + .get(1) + .ok_or_else(|| anyhow!("未找到 alt 文本"))? + .as_str(); + + // 根据内容决定颜色 + if alt_text.contains("VIP") { + Ok(format!("{}", alt_text.red())) + } else { + Ok(format!("{}", alt_text.bright_yellow())) + } + } else { + Ok(Self::new()) + } + } +} diff --git a/src/tools/timer.rs b/src/tools/timer.rs new file mode 100644 index 0000000..bd1372d --- /dev/null +++ b/src/tools/timer.rs @@ -0,0 +1,90 @@ +use chrono::{DateTime, Datelike, Local, NaiveDateTime, TimeZone, Utc}; + +pub trait DateFormatExt { + fn as_time_age(&self) -> String; +} + +impl DateFormatExt for NaiveDateTime { + fn as_time_age(&self) -> String { + let now = chrono::Local::now().naive_local(); + let year = now.year() - self.year(); + if year.ge(&1) { + return self.format("[%y年%m月%d日%H:%M]").to_string(); + }; + + let seconds = (now - *self).num_seconds(); + match seconds { + secs if secs < 60 => format!("[{}秒前]", secs), + secs if secs < 3600 => format!("[{}分钟前]", secs / 60), + secs if secs < 86400 => format!("[{}小时前]", secs / 3600), + secs if secs < 604800 => format!("[{}天前]", secs / 86400), + _ => self.format("[%y年%m月%d日 %H:%M]").to_string(), + } + } +} + +impl DateFormatExt for DateTime { + fn as_time_age(&self) -> String { + // 将UTC时间转换为本地时间 + let local_time = self.with_timezone(&Local); + let now = Local::now(); + + // 计算年份差 + let year = now.year() - local_time.year(); + if year >= 1 { + return local_time.format("[%y年%m月%d日 %H:%M]").to_string(); + } + + // 计算秒数差 + let seconds = (now - local_time).num_seconds(); + match seconds { + secs if secs < 60 => format!("[{}秒前]", secs), + secs if secs < 3600 => format!("[{}分钟前]", secs / 60), + secs if secs < 86400 => format!("[{}小时前]", secs / 3600), + secs if secs < 604800 => format!("[{}天前]", secs / 86400), + _ => local_time.format("[%y年%m月%d日 %H:%M]").to_string(), + } + } +} + +/// 自定义反序列化模块 +pub mod rfc3339_or_naive { + use std::time::Duration; + + use super::*; + + use serde::{Deserialize, Deserializer, Serializer, de}; + + pub fn serialize(dt: &DateTime, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&dt.to_rfc3339()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + // 首先尝试解析RFC3339格式(带时区) + if let Ok(dt) = DateTime::parse_from_rfc3339(&s) { + return Ok(dt.with_timezone(&Utc)); + } + + // 2026-01-22T18:35:49.593 + if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.3f") { + let ndt = ndt - Duration::from_hours(8); + return Ok(Utc.from_utc_datetime(&ndt)); + } + + // 2026-01-22T18:35:49 + if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") { + let ndt = ndt - Duration::from_hours(8); + return Ok(Utc.from_local_datetime(&ndt).unwrap()); + } + + Err(de::Error::custom(format!("无效的时间格式: {}", s))) + } +}