├── .github └── workflows │ ├── cicd.yml │ ├── coverage.yml │ └── scorecard.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bourso-launchd.sh └── src ├── bourso_api ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── account.rs │ ├── client │ ├── account.rs │ ├── config.rs │ ├── error.rs │ ├── mod.rs │ ├── trade │ │ ├── error.rs │ │ ├── feed.rs │ │ ├── mod.rs │ │ ├── order.rs │ │ └── tick.rs │ └── virtual_pad.rs │ ├── constants.rs │ └── lib.rs ├── lib.rs ├── main.rs ├── settings.rs └── validate.rs /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | # Inspired by https://github.com/Swatinem/rust-cache/blob/master/.github/workflows/simple.yml 2 | name: CI/CD 3 | 4 | on: 5 | push: 6 | tags: 7 | - v*.*.* 8 | 9 | permissions: 10 | contents: write 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | build: 17 | outputs: 18 | hash-ubuntu-latest: ${{ steps.hash.outputs.hash-ubuntu-latest }} 19 | hash-macos-latest: ${{ steps.hash.outputs.hash-macos-latest }} 20 | hash-windows-latest: ${{ steps.hash-windows.outputs.hash-windows-latest }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-latest, macos-latest, windows-latest] 25 | 26 | name: Bourso CLI ${{ matrix.os }} 27 | runs-on: ${{ matrix.os }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | # When rustup is updated, it tries to replace its binary, which on Windows is somehow locked. 33 | # This can result in the CI failure, see: https://github.com/rust-lang/rustup/issues/3029 34 | - run: | 35 | rustup set auto-self-update disable 36 | rustup toolchain install stable --profile minimal 37 | 38 | - name: Install Rust 39 | uses: dtolnay/rust-toolchain@v1 40 | with: 41 | toolchain: stable 42 | components: rustfmt 43 | 44 | - name: Cache 45 | uses: Swatinem/rust-cache@v2 46 | 47 | - name: Check and test 48 | run: | 49 | cargo check 50 | cargo test 51 | cargo test -p bourso_api 52 | 53 | - name: Build release 54 | run: cargo build --release 55 | 56 | - name: Rename and make executabe Mac Release 57 | if: matrix.os == 'macos-latest' 58 | run: | 59 | chmod +x target/release/bourso-cli 60 | tar -czvf target/release/bourso-cli-darwin.tar.gz -C target/release bourso-cli 61 | 62 | - name: Build Linux Release 63 | if: matrix.os == 'ubuntu-latest' 64 | run: tar -czvf target/release/bourso-cli-linux.tar.gz -C target/release bourso-cli 65 | 66 | - name: Generate subject for Unix-like 67 | if: matrix.os != 'windows-latest' 68 | id: hash 69 | run: | 70 | set -euo pipefail 71 | if [ ${{ matrix.os }} = "ubuntu-latest" ]; then 72 | echo "hash-${{ matrix.os }}=$(sha256sum target/release/bourso-cli | base64 -w0)" >> "$GITHUB_OUTPUT" 73 | elif [ ${{ matrix.os }} = "macos-latest" ]; then 74 | echo "hash-${{ matrix.os }}=$(shasum -a 256 target/release/bourso-cli | base64)" >> "$GITHUB_OUTPUT" 75 | fi 76 | 77 | - name: Generate subject for Windows 78 | if: matrix.os == 'windows-latest' 79 | id: hash-windows 80 | run: | 81 | # Stop script execution on errors (similar to set -e in Unix) 82 | $ErrorActionPreference = "Stop" 83 | $hash = Get-FileHash -Path "target/release/bourso-cli.exe" -Algorithm SHA256 84 | $hashString = $hash.Hash.ToLower() + " target/release/bourso-cli.exe" 85 | $base64Hash = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($hashString)) 86 | echo "hash-windows-latest=$base64Hash" | Out-File -FilePath $env:GITHUB_OUTPUT -Append 87 | 88 | - name: Create GitHub Release 89 | uses: softprops/action-gh-release@v2.0.4 90 | if: startsWith(github.ref, 'refs/tags/') 91 | with: 92 | files: | 93 | target/release/bourso-cli-linux.tar.gz 94 | target/release/bourso-cli-darwin.tar.gz 95 | target/release/bourso-cli.exe 96 | 97 | provenance: 98 | needs: [build] 99 | strategy: 100 | matrix: 101 | os: [ubuntu-latest, macos-latest, windows-latest] 102 | permissions: 103 | actions: read # To read the workflow path. 104 | id-token: write # To sign the provenance. 105 | contents: write # To add assets to a release. 106 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 107 | with: 108 | provenance-name: provenance-id-${{ matrix.os }}.intoto.jsonl 109 | base64-subjects: "${{ needs.build.outputs[format('hash-{0}', matrix.os)] }}" 110 | upload-assets: true # Optional: Upload to a new release 111 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: [push] 4 | jobs: 5 | test: 6 | name: coverage 7 | runs-on: ubuntu-latest 8 | container: 9 | image: xd009642/tarpaulin 10 | options: --security-opt seccomp=unconfined 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Generate code coverage 16 | run: | 17 | cargo tarpaulin --verbose --packages bourso_api --timeout 120 --out xml 18 | 19 | - name: Upload to codecov.io 20 | uses: codecov/codecov-action@v4.3.0 21 | with: 22 | fail_ci_if_error: false 23 | token: ${{ secrets.CODECOV_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '21 0 * * 1' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.chls 2 | 3 | 4 | # Added by cargo 5 | 6 | /target 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bourso-cli" 3 | version = "0.2.0" 4 | edition = "2021" 5 | repository = "https://github.com/azerpas/bourso-api" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bourso_api = { path = "./src/bourso_api" } 11 | tokio = { version = "1.33.0", features = ["full"] } 12 | anyhow = { version = "1.0.75" } 13 | clap = { version = "4.4.6" } 14 | rpassword = { version = "7.2.0" } 15 | directories = { version = "5.0.1" } 16 | serde = { version = "1.0.189", features = ["derive"] } 17 | serde_json = { version = "1.0.107" } 18 | log = { version = "0.4.20" } 19 | log4rs = { version = "1.3.0" } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Azerpas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bourso CLI 2 | 3 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/azerpas/bourso-api/badge)](https://securityscorecards.dev/viewer/?uri=github.com/azerpas/bourso-api) 4 | [![codecov](https://codecov.io/gh/azerpas/bourso-api/graph/badge.svg?token=I47J55VCB3)](https://codecov.io/gh/azerpas/bourso-api) 5 | 6 | Screenshot of Bourso CLI 7 | 8 | 9 | This app aims to be a simple CLI powered by *[Bourso API](./src/bourso_api/)* to log in to your [BoursoBank/Boursorama](https://www.boursorama.com) account and achieve some basic tasks. 10 | 11 | The first goal of this project was creating an automated [DCA (Dollar Cost Average)](https://www.investopedia.com/terms/d/dollarcostaveraging.asp) solution to buy [ETFs (Exchange Traded Funds)](https://www.investopedia.com/terms/e/etf.asp) on a regular basis with your Bourso account. 12 | 13 | [Follow these instructions](#dca-dollar-cost-averaging-investing) to setup your own automated DCA. 14 | 15 | - [Installation](#installation) 16 | - [From releases](#from-releases) 17 | - [Approve the app (MacOS)](#approve-the-app) 18 | - [Verify your installation](#verify-your-installation) 19 | - [From source](#from-source) 20 | - [Usage](#usage) 21 | - [Get your accounts](#get-your-accounts) 22 | - [Place an order](#place-an-order) 23 | - [Quote 🥷](#quote) 24 | - [DCA](#dca-dollar-cost-averaging-investing) 25 | - [Security](#security) 26 | - [Disclaimer](#disclaimer) 27 | 28 | (🥷 annoted commands require no login) 29 | 30 | ## Installation 31 | ### From releases 32 | You can download the latest release [here](https://github.com/azerpas/bourso-api/releases). 33 | 34 | Choose the right binary for your OS between: 35 | - `bourso-cli-darwin.tar.gz` for MacOS 36 | - `bourso-cli-linux.tar.gz` for Linux 37 | - `bourso-cli.exe` for Windows 38 | 39 | #### Approve the app (MacOS) 40 | 41 | If you then get a `"bourso-cli" cannot be opened because the developer cannot be verified` error, go to `System Preferences > Security & Privacy > General` and click `Open Anyway` 42 | 43 | If the above doesn't help you, make sure the file is executable: 44 | ```sh 45 | chmod +x bourso-cli 46 | # if it still says `Permission denied`, try 47 | chown 777 bourso-cli 48 | ``` 49 | 50 | ⚠️ Signing in with a different IP address than the ones you usually use will trigger a security check from Bourso. You'll have to validate the connection from your phone. A [GitHub pull request](https://github.com/azerpas/bourso-api/pull/10) is open to handle this case. 51 | 52 | #### Verify your installation 53 | Bourso CLI embeds [SLSA](https://slsa.dev/) standard to verify the integrity of the binary. You can verify the signature of the binary by: 54 | - Downloading the provenance generated by the release pipeline for your OS: https://github.com/azerpas/bourso-api/releases/latest 55 | - Installing [slsa-verifier](https://github.com/slsa-framework/slsa-verifier?tab=readme-ov-file#installation) 56 | - Running `slsa-verifier`. MacOS example: 57 | ```sh 58 | slsa-verifier-darwin-arm64 verify-artifact --provenance-path provenance-id-macos-latest.intoto.jsonl --source-uri "github.com/azerpas/bourso-api" bourso-cli 59 | ``` 60 | Which should output: 61 | ``` 62 | Verifying artifact ~/bourso-cli: PASSED 63 | ``` 64 | 65 | ### From source 66 | Requires [>=Rust 1.77.2](https://www.rust-lang.org) 67 | ```sh 68 | git clone git@github.com:azerpas/bourso-api.git 69 | cd bourso-api 70 | cargo build --release 71 | # You can run any command from the built application, e.g: 72 | ./target/release/bourso-cli config 73 | ``` 74 | 75 | ## Usage 76 | 77 | ### Configuration 78 | Save your client ID with this config command: 79 | ``` 80 | ./bourso-cli config 81 | ``` 82 | The password will be asked each time you run the app to avoid storing it in a file. 83 | 84 | ### Get your accounts 85 | ``` 86 | ./bourso-cli accounts 87 | ``` 88 | You'll get something like this: 89 | ``` 90 | [ 91 | Account { 92 | id: "1a2953bd1a28a37bd3fe89d32986e613", 93 | name: "BoursoBank", 94 | balance: 100, 95 | bank_name: "BoursoBank", 96 | kind: Banking, 97 | }, 98 | Account { 99 | id: "a583f3c5842c34fb00b408486ef493e0", 100 | name: "PEA DOE", 101 | balance: 1000000, 102 | bank_name: "BoursoBank", 103 | kind: Trading, 104 | }, 105 | ] 106 | ``` 107 | 108 | ### Place an order 109 | **Make sure to have a trading account with enough balance to place the order.** Check the previous section to see how to get your account ID. 110 | 111 | 🛍️ Place a buy order for 4 shares of the ETF "1rTCW8" (AMUNDI MSCI WORLD UCITS ETF - EUR) on your account "a583f3c5842c34fb00b408486ef493e0": 112 | ``` 113 | ./bourso-cli trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4 114 | ``` 115 | 116 | *Tip: You can get the ETF ID from the tracker URL, e.g. "AMUNDI MSCI WORLD UCITS ETF - EUR" url is https://www.boursorama.com/bourse/trackers/cours/1rTCW8/ (1rTCW8)* 117 | 118 | ### Quote 119 | Quote an asset to retrieve its value over time, e.g: 120 | ``` 121 | ➜ ~ ./bourso-cli quote --symbol 1rTCW8 average 122 | INFO bourso_cli > Welcome to BoursoBank CLI 👋 123 | INFO bourso_cli > ℹ️ - Version 0.1.6. Make sure you're running the latest version: https://github.com/azerpas/bourso-api 124 | 125 | INFO bourso_cli > Fetching quotes... 126 | INFO bourso_cli > Average quote: 494.5348136363637 127 | ``` 128 | Subcommands available: `highest`, `lowest`, `average`, `volume`, `last` 129 | 130 | ### DCA (Dollar Cost Averaging) investing 131 | 132 | You can use this script to do DCA investing. For example, if you want to buy 1 share of the ETF "1rTCW8" (AMUNDI MSCI WORLD UCITS ETF - EUR) every month, you can use a cron job to run the script every month. 133 | 134 | #### With MacOS 135 | You can use the `launchd` daemon to run the script every week. Create a file named `com.bourso-cli.plist` in `~/Library/LaunchAgents/` with the following content: 136 | ```xml 137 | 138 | 139 | 140 | 141 | Label 142 | com.azerpas.bourso-cli 143 | ProgramArguments 144 | 145 | /Users/YOUR_USER/bourso-launchd.sh 146 | 147 | StartCalendarInterval 148 | 149 | Weekday 150 | 1 151 | Hour 152 | 20 153 | Minute 154 | 00 155 | 156 | RunAtLoad 157 | 158 | StandardOutPath 159 | /Users/YOUR_USER/bourso-launchd-cli-stdout.log 160 | StandardErrorPath 161 | /Users/YOUR_USER/bourso-launchd-cli-stderr.log 162 | 163 | 164 | ``` 165 | Replace `YOUR_USER` with your username. 166 | 167 | Then copy the `bourso-launchd.sh` script in your home directory, modify the variables and make it executable. 168 | ```sh 169 | chmod +x bourso-launchd.sh 170 | chown 777 bourso-launchd.sh 171 | ``` 172 | Finally, load the agent with the following command: 173 | ```sh 174 | launchctl load ~/Library/LaunchAgents/com.bourso-cli.plist 175 | ``` 176 | The script will now run every week at 08:00 PM on Monday. You can check the logs in `~/bourso-launchd-cli-stdout.log` and `~/bourso-launchd-cli-stderr.log`. 177 | #### With Linux 178 | TODO 179 | #### With Windows 180 | Copy/paste the following commands and replace the path with the actual location of `bourso-cli.exe`. Then paste the commands to Powershell. 181 | ```ps1 182 | # Create a new task trigger that will run weekly on Sunday at 1:00pm 183 | $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 1:00PM 184 | # Create a new task action that will execute the trade based on the trigger defined above 185 | $action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoExit -Command `"& { `$exePath = 'C:\Path\To\bourso-cli.exe'; `$arguments = 'trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4'; Start-Process -FilePath `$exePath -ArgumentList `$arguments -NoNewWindow -Wait }`"" 186 | # Create a task named "Weekly Bourso CLI Task" 187 | Register-ScheduledTask -TaskName "Weekly Bourso CLI Task" -Trigger $trigger -Action $action 188 | ``` 189 | 190 | ## Security 191 | This app runs locally. All outbound/inbound data is sent/received to/from BoursoBank servers **only**. Your password will not be saved locally and will be asked each time you run the app. Your client ID has to be configurated and will be saved into the app data for next usages. 192 | 193 | ## Disclaimer 194 | 195 | This script is provided as is, without any warranty. I am not responsible for any loss of funds. Use at your own risk. I am not affiliated with BoursoBank or any other project mentioned in this repository. This is not financial advice. 196 | -------------------------------------------------------------------------------- /bourso-launchd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define your arguments here 4 | SYMBOL="1rTCW8" 5 | ACCOUNT="a583f3c5842c34fb00b408486ef493e0" 6 | QUANTITY="1" 7 | # ./bourso-cli trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4 8 | 9 | # Path to the bourso-cli script 10 | SCRIPT_PATH="/path/to/your/bourso-cli" 11 | # e.g SCRIPT_PATH="$HOME/bourso-cli-darwin" 12 | 13 | # Define the timestamp file path 14 | TIMESTAMP_FILE="$HOME/.bourso-cli/last_run" 15 | 16 | # Get the current timestamp 17 | CURRENT_TIME=$(date +%s) 18 | 19 | # Check if the timestamp file exists and read the last execution timestamp 20 | if [ -f "$TIMESTAMP_FILE" ]; then 21 | LAST_RUN=$(cat "$TIMESTAMP_FILE") 22 | else 23 | LAST_RUN=0 24 | fi 25 | 26 | # Time interval in seconds (1 week) 27 | INTERVAL=$((7 * 24 * 60 * 60)) 28 | 29 | # If the time since the last run is greater than or equal to the interval, run the script 30 | if [ $((CURRENT_TIME - LAST_RUN)) -ge $INTERVAL ]; then 31 | # Launch the script with the terminal window "popping up" 32 | osascript < "$TIMESTAMP_FILE" 41 | fi -------------------------------------------------------------------------------- /src/bourso_api/.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /src/bourso_api/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anstream" 46 | version = "0.6.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 49 | dependencies = [ 50 | "anstyle", 51 | "anstyle-parse", 52 | "anstyle-query", 53 | "anstyle-wincon", 54 | "colorchoice", 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle" 60 | version = "1.0.4" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 63 | 64 | [[package]] 65 | name = "anstyle-parse" 66 | version = "0.2.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 69 | dependencies = [ 70 | "utf8parse", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-query" 75 | version = "1.0.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 78 | dependencies = [ 79 | "windows-sys", 80 | ] 81 | 82 | [[package]] 83 | name = "anstyle-wincon" 84 | version = "3.0.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 87 | dependencies = [ 88 | "anstyle", 89 | "windows-sys", 90 | ] 91 | 92 | [[package]] 93 | name = "anyhow" 94 | version = "1.0.75" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 97 | 98 | [[package]] 99 | name = "autocfg" 100 | version = "1.1.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 103 | 104 | [[package]] 105 | name = "backtrace" 106 | version = "0.3.69" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 109 | dependencies = [ 110 | "addr2line", 111 | "cc", 112 | "cfg-if", 113 | "libc", 114 | "miniz_oxide", 115 | "object", 116 | "rustc-demangle", 117 | ] 118 | 119 | [[package]] 120 | name = "base64" 121 | version = "0.21.5" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" 124 | 125 | [[package]] 126 | name = "bitflags" 127 | version = "1.3.2" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 130 | 131 | [[package]] 132 | name = "bitflags" 133 | version = "2.4.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 136 | 137 | [[package]] 138 | name = "bourso_api" 139 | version = "0.1.0" 140 | dependencies = [ 141 | "anyhow", 142 | "chrono", 143 | "clap", 144 | "cookie_store 0.20.0", 145 | "lazy_static", 146 | "log", 147 | "regex", 148 | "reqwest", 149 | "reqwest_cookie_store", 150 | "serde", 151 | "serde_json", 152 | ] 153 | 154 | [[package]] 155 | name = "bumpalo" 156 | version = "3.14.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 159 | 160 | [[package]] 161 | name = "bytes" 162 | version = "1.5.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 165 | 166 | [[package]] 167 | name = "cc" 168 | version = "1.0.83" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 171 | dependencies = [ 172 | "libc", 173 | ] 174 | 175 | [[package]] 176 | name = "cfg-if" 177 | version = "1.0.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 180 | 181 | [[package]] 182 | name = "chrono" 183 | version = "0.4.31" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 186 | dependencies = [ 187 | "android-tzdata", 188 | "iana-time-zone", 189 | "js-sys", 190 | "num-traits", 191 | "wasm-bindgen", 192 | "windows-targets", 193 | ] 194 | 195 | [[package]] 196 | name = "clap" 197 | version = "4.4.7" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" 200 | dependencies = [ 201 | "clap_builder", 202 | "clap_derive", 203 | ] 204 | 205 | [[package]] 206 | name = "clap_builder" 207 | version = "4.4.7" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" 210 | dependencies = [ 211 | "anstream", 212 | "anstyle", 213 | "clap_lex", 214 | "strsim", 215 | ] 216 | 217 | [[package]] 218 | name = "clap_derive" 219 | version = "4.4.7" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 222 | dependencies = [ 223 | "heck", 224 | "proc-macro2", 225 | "quote", 226 | "syn", 227 | ] 228 | 229 | [[package]] 230 | name = "clap_lex" 231 | version = "0.6.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 234 | 235 | [[package]] 236 | name = "colorchoice" 237 | version = "1.0.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 240 | 241 | [[package]] 242 | name = "cookie" 243 | version = "0.16.2" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 246 | dependencies = [ 247 | "percent-encoding", 248 | "time", 249 | "version_check", 250 | ] 251 | 252 | [[package]] 253 | name = "cookie" 254 | version = "0.17.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" 257 | dependencies = [ 258 | "percent-encoding", 259 | "time", 260 | "version_check", 261 | ] 262 | 263 | [[package]] 264 | name = "cookie_store" 265 | version = "0.16.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" 268 | dependencies = [ 269 | "cookie 0.16.2", 270 | "idna 0.2.3", 271 | "log", 272 | "publicsuffix", 273 | "serde", 274 | "serde_derive", 275 | "serde_json", 276 | "time", 277 | "url", 278 | ] 279 | 280 | [[package]] 281 | name = "cookie_store" 282 | version = "0.20.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" 285 | dependencies = [ 286 | "cookie 0.17.0", 287 | "idna 0.3.0", 288 | "log", 289 | "publicsuffix", 290 | "serde", 291 | "serde_derive", 292 | "serde_json", 293 | "time", 294 | "url", 295 | ] 296 | 297 | [[package]] 298 | name = "core-foundation" 299 | version = "0.9.3" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 302 | dependencies = [ 303 | "core-foundation-sys", 304 | "libc", 305 | ] 306 | 307 | [[package]] 308 | name = "core-foundation-sys" 309 | version = "0.8.4" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 312 | 313 | [[package]] 314 | name = "deranged" 315 | version = "0.3.9" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" 318 | dependencies = [ 319 | "powerfmt", 320 | ] 321 | 322 | [[package]] 323 | name = "encoding_rs" 324 | version = "0.8.33" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 327 | dependencies = [ 328 | "cfg-if", 329 | ] 330 | 331 | [[package]] 332 | name = "equivalent" 333 | version = "1.0.1" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 336 | 337 | [[package]] 338 | name = "errno" 339 | version = "0.3.5" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 342 | dependencies = [ 343 | "libc", 344 | "windows-sys", 345 | ] 346 | 347 | [[package]] 348 | name = "fastrand" 349 | version = "2.0.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 352 | 353 | [[package]] 354 | name = "fnv" 355 | version = "1.0.7" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 358 | 359 | [[package]] 360 | name = "foreign-types" 361 | version = "0.3.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 364 | dependencies = [ 365 | "foreign-types-shared", 366 | ] 367 | 368 | [[package]] 369 | name = "foreign-types-shared" 370 | version = "0.1.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 373 | 374 | [[package]] 375 | name = "form_urlencoded" 376 | version = "1.2.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 379 | dependencies = [ 380 | "percent-encoding", 381 | ] 382 | 383 | [[package]] 384 | name = "futures-channel" 385 | version = "0.3.29" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 388 | dependencies = [ 389 | "futures-core", 390 | ] 391 | 392 | [[package]] 393 | name = "futures-core" 394 | version = "0.3.29" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 397 | 398 | [[package]] 399 | name = "futures-sink" 400 | version = "0.3.29" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 403 | 404 | [[package]] 405 | name = "futures-task" 406 | version = "0.3.29" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 409 | 410 | [[package]] 411 | name = "futures-util" 412 | version = "0.3.29" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 415 | dependencies = [ 416 | "futures-core", 417 | "futures-task", 418 | "pin-project-lite", 419 | "pin-utils", 420 | ] 421 | 422 | [[package]] 423 | name = "gimli" 424 | version = "0.28.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 427 | 428 | [[package]] 429 | name = "h2" 430 | version = "0.3.24" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" 433 | dependencies = [ 434 | "bytes", 435 | "fnv", 436 | "futures-core", 437 | "futures-sink", 438 | "futures-util", 439 | "http", 440 | "indexmap", 441 | "slab", 442 | "tokio", 443 | "tokio-util", 444 | "tracing", 445 | ] 446 | 447 | [[package]] 448 | name = "hashbrown" 449 | version = "0.14.3" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 452 | 453 | [[package]] 454 | name = "heck" 455 | version = "0.4.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 458 | 459 | [[package]] 460 | name = "http" 461 | version = "0.2.9" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 464 | dependencies = [ 465 | "bytes", 466 | "fnv", 467 | "itoa", 468 | ] 469 | 470 | [[package]] 471 | name = "http-body" 472 | version = "0.4.5" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 475 | dependencies = [ 476 | "bytes", 477 | "http", 478 | "pin-project-lite", 479 | ] 480 | 481 | [[package]] 482 | name = "httparse" 483 | version = "1.8.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 486 | 487 | [[package]] 488 | name = "httpdate" 489 | version = "1.0.3" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 492 | 493 | [[package]] 494 | name = "hyper" 495 | version = "0.14.27" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" 498 | dependencies = [ 499 | "bytes", 500 | "futures-channel", 501 | "futures-core", 502 | "futures-util", 503 | "h2", 504 | "http", 505 | "http-body", 506 | "httparse", 507 | "httpdate", 508 | "itoa", 509 | "pin-project-lite", 510 | "socket2 0.4.10", 511 | "tokio", 512 | "tower-service", 513 | "tracing", 514 | "want", 515 | ] 516 | 517 | [[package]] 518 | name = "hyper-tls" 519 | version = "0.5.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 522 | dependencies = [ 523 | "bytes", 524 | "hyper", 525 | "native-tls", 526 | "tokio", 527 | "tokio-native-tls", 528 | ] 529 | 530 | [[package]] 531 | name = "iana-time-zone" 532 | version = "0.1.58" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 535 | dependencies = [ 536 | "android_system_properties", 537 | "core-foundation-sys", 538 | "iana-time-zone-haiku", 539 | "js-sys", 540 | "wasm-bindgen", 541 | "windows-core", 542 | ] 543 | 544 | [[package]] 545 | name = "iana-time-zone-haiku" 546 | version = "0.1.2" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 549 | dependencies = [ 550 | "cc", 551 | ] 552 | 553 | [[package]] 554 | name = "idna" 555 | version = "0.2.3" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 558 | dependencies = [ 559 | "matches", 560 | "unicode-bidi", 561 | "unicode-normalization", 562 | ] 563 | 564 | [[package]] 565 | name = "idna" 566 | version = "0.3.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 569 | dependencies = [ 570 | "unicode-bidi", 571 | "unicode-normalization", 572 | ] 573 | 574 | [[package]] 575 | name = "idna" 576 | version = "0.4.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 579 | dependencies = [ 580 | "unicode-bidi", 581 | "unicode-normalization", 582 | ] 583 | 584 | [[package]] 585 | name = "indexmap" 586 | version = "2.1.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 589 | dependencies = [ 590 | "equivalent", 591 | "hashbrown", 592 | ] 593 | 594 | [[package]] 595 | name = "ipnet" 596 | version = "2.9.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 599 | 600 | [[package]] 601 | name = "itoa" 602 | version = "1.0.9" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 605 | 606 | [[package]] 607 | name = "js-sys" 608 | version = "0.3.65" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 611 | dependencies = [ 612 | "wasm-bindgen", 613 | ] 614 | 615 | [[package]] 616 | name = "lazy_static" 617 | version = "1.4.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 620 | 621 | [[package]] 622 | name = "libc" 623 | version = "0.2.149" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" 626 | 627 | [[package]] 628 | name = "linux-raw-sys" 629 | version = "0.4.10" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 632 | 633 | [[package]] 634 | name = "log" 635 | version = "0.4.20" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 638 | 639 | [[package]] 640 | name = "matches" 641 | version = "0.1.10" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" 644 | 645 | [[package]] 646 | name = "memchr" 647 | version = "2.6.4" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 650 | 651 | [[package]] 652 | name = "mime" 653 | version = "0.3.17" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 656 | 657 | [[package]] 658 | name = "mime_guess" 659 | version = "2.0.4" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 662 | dependencies = [ 663 | "mime", 664 | "unicase", 665 | ] 666 | 667 | [[package]] 668 | name = "miniz_oxide" 669 | version = "0.7.1" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 672 | dependencies = [ 673 | "adler", 674 | ] 675 | 676 | [[package]] 677 | name = "mio" 678 | version = "0.8.11" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 681 | dependencies = [ 682 | "libc", 683 | "wasi", 684 | "windows-sys", 685 | ] 686 | 687 | [[package]] 688 | name = "native-tls" 689 | version = "0.2.11" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 692 | dependencies = [ 693 | "lazy_static", 694 | "libc", 695 | "log", 696 | "openssl", 697 | "openssl-probe", 698 | "openssl-sys", 699 | "schannel", 700 | "security-framework", 701 | "security-framework-sys", 702 | "tempfile", 703 | ] 704 | 705 | [[package]] 706 | name = "num-traits" 707 | version = "0.2.17" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 710 | dependencies = [ 711 | "autocfg", 712 | ] 713 | 714 | [[package]] 715 | name = "object" 716 | version = "0.32.1" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 719 | dependencies = [ 720 | "memchr", 721 | ] 722 | 723 | [[package]] 724 | name = "once_cell" 725 | version = "1.18.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 728 | 729 | [[package]] 730 | name = "openssl" 731 | version = "0.10.60" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" 734 | dependencies = [ 735 | "bitflags 2.4.1", 736 | "cfg-if", 737 | "foreign-types", 738 | "libc", 739 | "once_cell", 740 | "openssl-macros", 741 | "openssl-sys", 742 | ] 743 | 744 | [[package]] 745 | name = "openssl-macros" 746 | version = "0.1.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 749 | dependencies = [ 750 | "proc-macro2", 751 | "quote", 752 | "syn", 753 | ] 754 | 755 | [[package]] 756 | name = "openssl-probe" 757 | version = "0.1.5" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 760 | 761 | [[package]] 762 | name = "openssl-sys" 763 | version = "0.9.96" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" 766 | dependencies = [ 767 | "cc", 768 | "libc", 769 | "pkg-config", 770 | "vcpkg", 771 | ] 772 | 773 | [[package]] 774 | name = "percent-encoding" 775 | version = "2.3.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 778 | 779 | [[package]] 780 | name = "pin-project-lite" 781 | version = "0.2.13" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 784 | 785 | [[package]] 786 | name = "pin-utils" 787 | version = "0.1.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 790 | 791 | [[package]] 792 | name = "pkg-config" 793 | version = "0.3.27" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 796 | 797 | [[package]] 798 | name = "powerfmt" 799 | version = "0.2.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 802 | 803 | [[package]] 804 | name = "proc-macro2" 805 | version = "1.0.69" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 808 | dependencies = [ 809 | "unicode-ident", 810 | ] 811 | 812 | [[package]] 813 | name = "psl-types" 814 | version = "2.0.11" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" 817 | 818 | [[package]] 819 | name = "publicsuffix" 820 | version = "2.2.3" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" 823 | dependencies = [ 824 | "idna 0.3.0", 825 | "psl-types", 826 | ] 827 | 828 | [[package]] 829 | name = "quote" 830 | version = "1.0.33" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 833 | dependencies = [ 834 | "proc-macro2", 835 | ] 836 | 837 | [[package]] 838 | name = "redox_syscall" 839 | version = "0.4.1" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 842 | dependencies = [ 843 | "bitflags 1.3.2", 844 | ] 845 | 846 | [[package]] 847 | name = "regex" 848 | version = "1.10.2" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 851 | dependencies = [ 852 | "aho-corasick", 853 | "memchr", 854 | "regex-automata", 855 | "regex-syntax", 856 | ] 857 | 858 | [[package]] 859 | name = "regex-automata" 860 | version = "0.4.3" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 863 | dependencies = [ 864 | "aho-corasick", 865 | "memchr", 866 | "regex-syntax", 867 | ] 868 | 869 | [[package]] 870 | name = "regex-syntax" 871 | version = "0.8.2" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 874 | 875 | [[package]] 876 | name = "reqwest" 877 | version = "0.11.22" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" 880 | dependencies = [ 881 | "base64", 882 | "bytes", 883 | "cookie 0.16.2", 884 | "cookie_store 0.16.2", 885 | "encoding_rs", 886 | "futures-core", 887 | "futures-util", 888 | "h2", 889 | "http", 890 | "http-body", 891 | "hyper", 892 | "hyper-tls", 893 | "ipnet", 894 | "js-sys", 895 | "log", 896 | "mime", 897 | "mime_guess", 898 | "native-tls", 899 | "once_cell", 900 | "percent-encoding", 901 | "pin-project-lite", 902 | "serde", 903 | "serde_json", 904 | "serde_urlencoded", 905 | "system-configuration", 906 | "tokio", 907 | "tokio-native-tls", 908 | "tower-service", 909 | "url", 910 | "wasm-bindgen", 911 | "wasm-bindgen-futures", 912 | "web-sys", 913 | "winreg", 914 | ] 915 | 916 | [[package]] 917 | name = "reqwest_cookie_store" 918 | version = "0.6.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061" 921 | dependencies = [ 922 | "bytes", 923 | "cookie_store 0.20.0", 924 | "reqwest", 925 | "url", 926 | ] 927 | 928 | [[package]] 929 | name = "rustc-demangle" 930 | version = "0.1.23" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 933 | 934 | [[package]] 935 | name = "rustix" 936 | version = "0.38.21" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 939 | dependencies = [ 940 | "bitflags 2.4.1", 941 | "errno", 942 | "libc", 943 | "linux-raw-sys", 944 | "windows-sys", 945 | ] 946 | 947 | [[package]] 948 | name = "ryu" 949 | version = "1.0.15" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 952 | 953 | [[package]] 954 | name = "schannel" 955 | version = "0.1.22" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" 958 | dependencies = [ 959 | "windows-sys", 960 | ] 961 | 962 | [[package]] 963 | name = "security-framework" 964 | version = "2.9.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 967 | dependencies = [ 968 | "bitflags 1.3.2", 969 | "core-foundation", 970 | "core-foundation-sys", 971 | "libc", 972 | "security-framework-sys", 973 | ] 974 | 975 | [[package]] 976 | name = "security-framework-sys" 977 | version = "2.9.1" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 980 | dependencies = [ 981 | "core-foundation-sys", 982 | "libc", 983 | ] 984 | 985 | [[package]] 986 | name = "serde" 987 | version = "1.0.190" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" 990 | dependencies = [ 991 | "serde_derive", 992 | ] 993 | 994 | [[package]] 995 | name = "serde_derive" 996 | version = "1.0.190" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "syn", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "serde_json" 1007 | version = "1.0.108" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 1010 | dependencies = [ 1011 | "itoa", 1012 | "ryu", 1013 | "serde", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "serde_urlencoded" 1018 | version = "0.7.1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1021 | dependencies = [ 1022 | "form_urlencoded", 1023 | "itoa", 1024 | "ryu", 1025 | "serde", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "slab" 1030 | version = "0.4.9" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1033 | dependencies = [ 1034 | "autocfg", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "socket2" 1039 | version = "0.4.10" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" 1042 | dependencies = [ 1043 | "libc", 1044 | "winapi", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "socket2" 1049 | version = "0.5.5" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 1052 | dependencies = [ 1053 | "libc", 1054 | "windows-sys", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "strsim" 1059 | version = "0.10.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1062 | 1063 | [[package]] 1064 | name = "syn" 1065 | version = "2.0.38" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 1068 | dependencies = [ 1069 | "proc-macro2", 1070 | "quote", 1071 | "unicode-ident", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "system-configuration" 1076 | version = "0.5.1" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1079 | dependencies = [ 1080 | "bitflags 1.3.2", 1081 | "core-foundation", 1082 | "system-configuration-sys", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "system-configuration-sys" 1087 | version = "0.5.0" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1090 | dependencies = [ 1091 | "core-foundation-sys", 1092 | "libc", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "tempfile" 1097 | version = "3.8.1" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" 1100 | dependencies = [ 1101 | "cfg-if", 1102 | "fastrand", 1103 | "redox_syscall", 1104 | "rustix", 1105 | "windows-sys", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "time" 1110 | version = "0.3.30" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" 1113 | dependencies = [ 1114 | "deranged", 1115 | "itoa", 1116 | "powerfmt", 1117 | "serde", 1118 | "time-core", 1119 | "time-macros", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "time-core" 1124 | version = "0.1.2" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1127 | 1128 | [[package]] 1129 | name = "time-macros" 1130 | version = "0.2.15" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" 1133 | dependencies = [ 1134 | "time-core", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "tinyvec" 1139 | version = "1.6.0" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1142 | dependencies = [ 1143 | "tinyvec_macros", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "tinyvec_macros" 1148 | version = "0.1.1" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1151 | 1152 | [[package]] 1153 | name = "tokio" 1154 | version = "1.33.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" 1157 | dependencies = [ 1158 | "backtrace", 1159 | "bytes", 1160 | "libc", 1161 | "mio", 1162 | "pin-project-lite", 1163 | "socket2 0.5.5", 1164 | "windows-sys", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "tokio-native-tls" 1169 | version = "0.3.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1172 | dependencies = [ 1173 | "native-tls", 1174 | "tokio", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "tokio-util" 1179 | version = "0.7.10" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1182 | dependencies = [ 1183 | "bytes", 1184 | "futures-core", 1185 | "futures-sink", 1186 | "pin-project-lite", 1187 | "tokio", 1188 | "tracing", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "tower-service" 1193 | version = "0.3.2" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1196 | 1197 | [[package]] 1198 | name = "tracing" 1199 | version = "0.1.40" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1202 | dependencies = [ 1203 | "pin-project-lite", 1204 | "tracing-core", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "tracing-core" 1209 | version = "0.1.32" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1212 | dependencies = [ 1213 | "once_cell", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "try-lock" 1218 | version = "0.2.4" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1221 | 1222 | [[package]] 1223 | name = "unicase" 1224 | version = "2.7.0" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1227 | dependencies = [ 1228 | "version_check", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "unicode-bidi" 1233 | version = "0.3.13" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1236 | 1237 | [[package]] 1238 | name = "unicode-ident" 1239 | version = "1.0.12" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1242 | 1243 | [[package]] 1244 | name = "unicode-normalization" 1245 | version = "0.1.22" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1248 | dependencies = [ 1249 | "tinyvec", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "url" 1254 | version = "2.4.1" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" 1257 | dependencies = [ 1258 | "form_urlencoded", 1259 | "idna 0.4.0", 1260 | "percent-encoding", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "utf8parse" 1265 | version = "0.2.1" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1268 | 1269 | [[package]] 1270 | name = "vcpkg" 1271 | version = "0.2.15" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1274 | 1275 | [[package]] 1276 | name = "version_check" 1277 | version = "0.9.4" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1280 | 1281 | [[package]] 1282 | name = "want" 1283 | version = "0.3.1" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1286 | dependencies = [ 1287 | "try-lock", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "wasi" 1292 | version = "0.11.0+wasi-snapshot-preview1" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1295 | 1296 | [[package]] 1297 | name = "wasm-bindgen" 1298 | version = "0.2.88" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 1301 | dependencies = [ 1302 | "cfg-if", 1303 | "wasm-bindgen-macro", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "wasm-bindgen-backend" 1308 | version = "0.2.88" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 1311 | dependencies = [ 1312 | "bumpalo", 1313 | "log", 1314 | "once_cell", 1315 | "proc-macro2", 1316 | "quote", 1317 | "syn", 1318 | "wasm-bindgen-shared", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "wasm-bindgen-futures" 1323 | version = "0.4.38" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" 1326 | dependencies = [ 1327 | "cfg-if", 1328 | "js-sys", 1329 | "wasm-bindgen", 1330 | "web-sys", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "wasm-bindgen-macro" 1335 | version = "0.2.88" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 1338 | dependencies = [ 1339 | "quote", 1340 | "wasm-bindgen-macro-support", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "wasm-bindgen-macro-support" 1345 | version = "0.2.88" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 1348 | dependencies = [ 1349 | "proc-macro2", 1350 | "quote", 1351 | "syn", 1352 | "wasm-bindgen-backend", 1353 | "wasm-bindgen-shared", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "wasm-bindgen-shared" 1358 | version = "0.2.88" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 1361 | 1362 | [[package]] 1363 | name = "web-sys" 1364 | version = "0.3.65" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 1367 | dependencies = [ 1368 | "js-sys", 1369 | "wasm-bindgen", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "winapi" 1374 | version = "0.3.9" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1377 | dependencies = [ 1378 | "winapi-i686-pc-windows-gnu", 1379 | "winapi-x86_64-pc-windows-gnu", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "winapi-i686-pc-windows-gnu" 1384 | version = "0.4.0" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1387 | 1388 | [[package]] 1389 | name = "winapi-x86_64-pc-windows-gnu" 1390 | version = "0.4.0" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1393 | 1394 | [[package]] 1395 | name = "windows-core" 1396 | version = "0.51.1" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 1399 | dependencies = [ 1400 | "windows-targets", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "windows-sys" 1405 | version = "0.48.0" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1408 | dependencies = [ 1409 | "windows-targets", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "windows-targets" 1414 | version = "0.48.5" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1417 | dependencies = [ 1418 | "windows_aarch64_gnullvm", 1419 | "windows_aarch64_msvc", 1420 | "windows_i686_gnu", 1421 | "windows_i686_msvc", 1422 | "windows_x86_64_gnu", 1423 | "windows_x86_64_gnullvm", 1424 | "windows_x86_64_msvc", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "windows_aarch64_gnullvm" 1429 | version = "0.48.5" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1432 | 1433 | [[package]] 1434 | name = "windows_aarch64_msvc" 1435 | version = "0.48.5" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1438 | 1439 | [[package]] 1440 | name = "windows_i686_gnu" 1441 | version = "0.48.5" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1444 | 1445 | [[package]] 1446 | name = "windows_i686_msvc" 1447 | version = "0.48.5" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1450 | 1451 | [[package]] 1452 | name = "windows_x86_64_gnu" 1453 | version = "0.48.5" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1456 | 1457 | [[package]] 1458 | name = "windows_x86_64_gnullvm" 1459 | version = "0.48.5" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1462 | 1463 | [[package]] 1464 | name = "windows_x86_64_msvc" 1465 | version = "0.48.5" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1468 | 1469 | [[package]] 1470 | name = "winreg" 1471 | version = "0.50.0" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1474 | dependencies = [ 1475 | "cfg-if", 1476 | "windows-sys", 1477 | ] 1478 | -------------------------------------------------------------------------------- /src/bourso_api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bourso_api" 3 | version = "0.1.11" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { version = "0.12.12", features = ["multipart", "cookies"] } 10 | anyhow = { version = "1.0.75" } 11 | regex = { version = "1.10.2" } 12 | lazy_static = { version = "1.4.0" } 13 | clap = { version = "4.5.26", features = ["derive"] } 14 | serde = { version = "1.0.189", features = ["derive"] } 15 | serde_json = { version = "1.0.107" } 16 | reqwest_cookie_store = "0.8.0" 17 | cookie_store = "0.21.1" 18 | chrono = { version = "0.4.39" } 19 | log = { version = "0.4.20" } -------------------------------------------------------------------------------- /src/bourso_api/README.md: -------------------------------------------------------------------------------- 1 | # Bourso API 2 | 3 | Unofficial API for [BoursoBank/Boursorama](https://www.boursorama.com) written in Rust. -------------------------------------------------------------------------------- /src/bourso_api/src/account.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Type of account 4 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)] 5 | pub enum AccountKind { 6 | Banking, 7 | Savings, 8 | #[default] 9 | Trading, 10 | Loans, 11 | } 12 | 13 | /// A bank account 14 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 15 | pub struct Account { 16 | /// Account id as an hexadecimal string (32 characters) 17 | pub id: String, 18 | /// Account name 19 | pub name: String, 20 | /// Balance in cents 21 | pub balance: isize, 22 | /// Account bank name as you can connect accounts from other banks 23 | pub bank_name: String, 24 | /// The type of account 25 | pub kind: AccountKind, 26 | } 27 | -------------------------------------------------------------------------------- /src/bourso_api/src/client/account.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | account::{AccountKind, Account}, 3 | constants::{BASE_URL, SAVINGS_PATTERN, BANKING_PATTERN, TRADING_PATTERN, LOANS_PATTERN, ACCOUNT_PATTERN} 4 | }; 5 | 6 | use super::BoursoWebClient; 7 | 8 | use anyhow::{Context, Result}; 9 | use log::debug; 10 | use regex::Regex; 11 | 12 | impl BoursoWebClient { 13 | /// Get the accounts list. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `kind` - The type of accounts to retrieve. If `None`, all accounts are retrieved. 18 | /// 19 | /// # Returns 20 | /// 21 | /// The accounts list as a vector of `Account`. 22 | pub async fn get_accounts(&self, kind: Option) -> Result> { 23 | let res = self.client 24 | .get(format!("{BASE_URL}/dashboard/liste-comptes?rumroute=dashboard.new_accounts&_hinclude=1")) 25 | .headers(self.get_headers()) 26 | .send() 27 | .await? 28 | .text() 29 | .await?; 30 | 31 | let accounts = match kind { 32 | Some(AccountKind::Savings) => extract_accounts(&res, AccountKind::Savings)?, 33 | Some(AccountKind::Banking) => extract_accounts(&res, AccountKind::Banking)?, 34 | Some(AccountKind::Trading) => extract_accounts(&res, AccountKind::Trading)?, 35 | Some(AccountKind::Loans) => extract_accounts(&res, AccountKind::Loans)?, 36 | // all accounts 37 | _ => { 38 | [ 39 | extract_accounts(&res, AccountKind::Savings).unwrap_or(Vec::new()), 40 | extract_accounts(&res, AccountKind::Banking).unwrap_or(Vec::new()), 41 | extract_accounts(&res, AccountKind::Trading).unwrap_or(Vec::new()), 42 | extract_accounts(&res, AccountKind::Loans).unwrap_or(Vec::new()), 43 | ].concat() 44 | }, 45 | }; 46 | 47 | Ok(accounts) 48 | } 49 | } 50 | 51 | fn extract_accounts(res: &str, kind: AccountKind) -> Result> { 52 | let regex = Regex::new( 53 | match kind { 54 | AccountKind::Savings => SAVINGS_PATTERN, 55 | AccountKind::Banking => BANKING_PATTERN, 56 | AccountKind::Trading => TRADING_PATTERN, 57 | AccountKind::Loans => LOANS_PATTERN, 58 | } 59 | )?; 60 | let accounts_ul = regex 61 | .captures(&res) 62 | .with_context(|| { 63 | debug!("Response: {}", res); 64 | format!("Failed to extract {:?} accounts from the response", kind) 65 | })? 66 | .get(1) 67 | .context("Failed to extract accounts from regex match")? 68 | .as_str(); 69 | 70 | let account_regex = Regex::new(ACCOUNT_PATTERN)?; 71 | 72 | let accounts = account_regex 73 | .captures_iter(&accounts_ul) 74 | .map(|m| { 75 | Account { 76 | id: m.name("id") 77 | .unwrap() 78 | .as_str() 79 | .trim() 80 | .to_string(), 81 | name: m.name("name") 82 | .unwrap() 83 | .as_str() 84 | .trim() 85 | .to_string(), 86 | balance: m.name("balance") 87 | .unwrap() 88 | .as_str() 89 | .trim() 90 | .replace(" ", "") 91 | .replace(",", "") 92 | .replace("\u{a0}", "") 93 | .replace("−", "-") 94 | .parse::() 95 | .unwrap(), 96 | bank_name: m.name("bank_name") 97 | .unwrap() 98 | .as_str() 99 | .trim() 100 | .to_string(), 101 | kind: kind, 102 | } 103 | }) 104 | .collect::>(); 105 | 106 | Ok(accounts) 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use crate::{client::account::extract_accounts, account::AccountKind}; 112 | 113 | #[test] 114 | fn test_extract_accounts() { 115 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Savings).unwrap(); 116 | assert_eq!(accounts.len(), 2); 117 | assert_eq!(accounts[0].name, "LIVRET DEVELOPPEMENT DURABLE SOLIDAIRE"); 118 | assert_eq!(accounts[0].balance, 1101000); 119 | assert_eq!(accounts[0].bank_name, "BoursoBank"); 120 | assert_eq!(accounts[1].id, "d4e4fd4067b6d4d0b538a15e42238ef9"); 121 | assert_eq!(accounts[1].name, "Livret Jeune"); 122 | assert_eq!(accounts[1].balance, 159972); 123 | assert_eq!(accounts[1].bank_name, "Crédit Agricole"); 124 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Banking).unwrap(); 125 | assert_eq!(accounts.len(), 2); 126 | assert_eq!(accounts[0].id, "e2f509c466f5294f15abd873dbbf8a62"); 127 | assert_eq!(accounts[0].name, "BoursoBank"); 128 | assert_eq!(accounts[0].balance, 2081050); 129 | assert_eq!(accounts[0].bank_name, "BoursoBank"); 130 | assert_eq!(accounts[1].name, "Compte de chèques ****0102"); 131 | assert_eq!(accounts[1].balance, 50040); 132 | assert_eq!(accounts[1].bank_name, "CIC"); 133 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Trading).unwrap(); 134 | assert_eq!(accounts.len(), 1); 135 | assert_eq!(accounts[0].name, "PEA DOE"); 136 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Loans).unwrap(); 137 | assert_eq!(accounts.len(), 1); 138 | assert_eq!(accounts[0].name, "Prêt personnel"); 139 | assert_eq!(accounts[0].balance, -9495982); 140 | assert_eq!(accounts[0].bank_name, "Crédit Agricole"); 141 | } 142 | 143 | 144 | 145 | pub const ACCOUNTS_RES: &str = r#" 147 |
148 |
149 | 151 |
152 |
153 |
154 | 155 | 226 | 227 | 228 | 286 | 287 | 288 |
289 |
290 | 291 | Mes placements financiers 292 | 293 | 294 | 143 088,89 € 295 | 296 |
297 | 324 |
325 | 326 | 327 |
328 |
329 | 330 | Mes crédits 331 | 332 | 333 | − 94 959,82 € 334 | 335 |
336 | 363 |
364 | 365 | 366 | 367 | 368 | 369 | 370 | "#; 371 | } -------------------------------------------------------------------------------- /src/bourso_api/src/client/config.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use serde::{Serialize, Deserialize}; 3 | use anyhow::{Result, Context}; 4 | 5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct Config { 8 | #[serde(rename = "API_HOST")] 9 | pub api_host: String, 10 | #[serde(rename = "API_PATH")] 11 | pub api_path: String, 12 | #[serde(rename = "API_ENV")] 13 | pub api_env: String, 14 | #[serde(rename = "API_URL")] 15 | pub api_url: String, 16 | #[serde(rename = "API_REFERER_FEATURE_ID")] 17 | pub api_referer_feature_id: String, 18 | #[serde(rename = "LOCALE")] 19 | pub locale: String, 20 | #[serde(rename = "SUBSCRIPTION_HOST")] 21 | pub subscription_host: String, 22 | #[serde(rename = "CUSTOMER_SUBSCRIPTION_HOST")] 23 | pub customer_subscription_host: String, 24 | #[serde(rename = "PROSPECT_SUBSCRIPTION_HOST")] 25 | pub prospect_subscription_host: String, 26 | #[serde(rename = "DEBUG")] 27 | pub debug: bool, 28 | #[serde(rename = "ENABLE_PROFILER")] 29 | pub enable_profiler: bool, 30 | #[serde(rename = "AUTHENTICATION_ENDPOINT")] 31 | pub authentication_endpoint: String, 32 | #[serde(rename = "app_customer_website_host")] 33 | pub app_customer_website_host: String, 34 | #[serde(rename = "app_portal_website_host")] 35 | pub app_portal_website_host: String, 36 | #[serde(rename = "pjax_enabled")] 37 | pub pjax_enabled: bool, 38 | #[serde(rename = "pjax_timeout")] 39 | pub pjax_timeout: i64, 40 | #[serde(rename = "pjax_offset_duration")] 41 | pub pjax_offset_duration: i64, 42 | #[serde(rename = "select_bar_autoclose_tooltip_timeout")] 43 | pub select_bar_autoclose_tooltip_timeout: i64, 44 | #[serde(rename = "app_release_date")] 45 | pub app_release_date: String, 46 | #[serde(rename = "USER_HASH")] 47 | pub user_hash: Option, 48 | #[serde(rename = "JWT_TOKEN_ID")] 49 | pub jwt_token_id: String, 50 | #[serde(rename = "DEFAULT_API_BEARER")] 51 | pub default_api_bearer: String, 52 | #[serde(rename = "JAVASCRIPT_APPS_BEARER")] 53 | pub javascript_apps_bearer: JavascriptAppsBearer, 54 | #[serde(rename = "APPLICATION_NAME")] 55 | pub application_name: String, 56 | pub webauth: Webauth, 57 | #[serde(rename = "MARKETING_NAME")] 58 | pub marketing_name: String, 59 | } 60 | 61 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 62 | #[serde(rename_all = "camelCase")] 63 | pub struct JavascriptAppsBearer { 64 | #[serde(rename = "web_all_feedback_01")] 65 | pub web_all_feedback_01: String, 66 | } 67 | 68 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 69 | #[serde(rename_all = "camelCase")] 70 | pub struct Webauth { 71 | pub prepare_path: String, 72 | pub valid_path: String, 73 | } 74 | 75 | pub fn extract_brs_config(res: &str) -> Result { 76 | let regex = Regex::new(r#"(?ms)window\.BRS_CONFIG\s*=\s*(?P.*?);"#).unwrap(); 77 | let config = regex 78 | .captures(&res) 79 | .unwrap() 80 | .name("config") 81 | .unwrap(); 82 | 83 | let config: Config = serde_json::from_str(&config.as_str().trim()) 84 | .with_context(|| format!("Could not deserialize BRS_CONFIG: {}", config.as_str().trim()))?; 85 | 86 | Ok(config) 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | const SCRIPT_CONFIG: &str = r#""#; 98 | 99 | #[test] 100 | fn test_extract_brs_config() { 101 | let config = super::extract_brs_config(SCRIPT_CONFIG).unwrap(); 102 | assert_eq!(config.jwt_token_id, "brsxds_61d55b52615fbdfb898a3731bba89b35"); 103 | assert_eq!(config.api_path, "/services/api/v1.7"); 104 | assert_eq!(config.api_host, "api.boursobank.com"); 105 | assert_eq!(config.api_url, "https://api.boursobank.com/services/api/v1.7"); 106 | assert_eq!(config.api_env, "prod"); 107 | assert_eq!(config.api_referer_feature_id, "customer.dashboard_home.web_fr_front_20"); 108 | assert_eq!(config.locale, "fr-FR"); 109 | assert_eq!(config.subscription_host, "souscrire.boursobank.com"); 110 | assert_eq!(config.customer_subscription_host, "souscrire.boursobank.com"); 111 | assert_eq!(config.prospect_subscription_host, "ouvrir-un-compte.boursobank.com"); 112 | assert_eq!(config.debug, false); 113 | assert_eq!(config.enable_profiler, false); 114 | assert_eq!(config.authentication_endpoint, "/connexion/"); 115 | assert_eq!(config.app_customer_website_host, "clients.boursobank.com"); 116 | assert_eq!(config.app_portal_website_host, "www.boursorama.com"); 117 | assert_eq!(config.pjax_enabled, true); 118 | assert_eq!(config.pjax_timeout, 20000); 119 | assert_eq!(config.pjax_offset_duration, 350); 120 | assert_eq!(config.select_bar_autoclose_tooltip_timeout, 3000); 121 | assert_eq!(config.app_release_date, "2023-03-01T14:15:36+0100"); 122 | assert_eq!(config.user_hash.unwrap(), "61d55b52615fbdf"); 123 | assert_eq!(config.application_name, "web_fr_front_20"); 124 | assert_eq!(config.marketing_name, "BoursoBank"); 125 | assert_eq!(config.webauth.prepare_path, "/webauthn/authentification/preparation"); 126 | assert_eq!(config.webauth.valid_path, "/webauthn/authentification/validation"); 127 | assert_eq!(config.default_api_bearer, "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTgyNDU5MTksImV4cCI6MTcyOTc4MTkxOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.bvXls6bqw_xGqA6V8DQMsZK92dMrV8K6hebWpEu5IF8MlEd4qmwmcchJBUT7oeBnSIp5TJHH5112ho548Sw57A"); 128 | } 129 | } -------------------------------------------------------------------------------- /src/bourso_api/src/client/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub enum ClientError { 6 | InvalidCredentials, 7 | MfaRequired, 8 | InvalidMfa, 9 | } 10 | 11 | impl fmt::Display for ClientError { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | match self { 14 | ClientError::InvalidCredentials => write!(f, "Invalid credentials"), 15 | ClientError::MfaRequired => write!(f, "MFA required"), 16 | ClientError::InvalidMfa => write!(f, "Invalid MFA"), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/bourso_api/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod config; 3 | pub mod error; 4 | pub mod trade; 5 | pub mod virtual_pad; 6 | 7 | use core::fmt; 8 | use std::sync::Arc; 9 | 10 | use anyhow::{bail, Result}; 11 | use cookie_store::Cookie; 12 | use error::ClientError; 13 | use log::{debug, error, info}; 14 | use regex::Regex; 15 | use reqwest_cookie_store::{CookieStore, CookieStoreMutex}; 16 | use serde::{Deserialize, Serialize}; 17 | 18 | use self::config::{extract_brs_config, Config}; 19 | 20 | use super::constants::BASE_URL; 21 | 22 | pub struct BoursoWebClient { 23 | /// The client used to make requests to the Bourso website. 24 | client: reqwest::Client, 25 | /// __brs_mit cookie is a cookie that is necessary to access login page. 26 | /// Bourso website sets it when you access the login page for the first time before refreshing the page. 27 | brs_mit_cookie: String, 28 | /// Virtual pad IDs are the IDs of the virtual pad keys. They are used to translate the password 29 | virtual_pad_ids: Vec, 30 | /// Challenge ID is a token retrieved from the virtual pad page. It represents a random string 31 | /// that corresponds to the used virtual pad keys layout. 32 | challenge_id: String, 33 | /// Customer ID used to login. 34 | customer_id: String, 35 | /// Form token used to login. 36 | token: String, 37 | /// Password used to login. 38 | password: String, 39 | /// Cookie store used to store cookies between each request made by the client to the Bourso website. 40 | cookie_store: Arc, 41 | /// Bourso Web current configuration 42 | pub config: Config, 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 46 | pub enum MfaType { 47 | Email, 48 | Sms, 49 | WebToApp, 50 | } 51 | 52 | impl fmt::Display for MfaType { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | match self { 55 | MfaType::Email => write!(f, "email"), 56 | MfaType::Sms => write!(f, "sms"), 57 | MfaType::WebToApp => write!(f, "web to app"), 58 | } 59 | } 60 | } 61 | 62 | impl MfaType { 63 | pub fn start_path(&self) -> &'static str { 64 | match self { 65 | MfaType::Email => "startemail", 66 | MfaType::Sms => "startsms", 67 | MfaType::WebToApp => "startwebtoapp", 68 | } 69 | } 70 | 71 | pub fn check_path(&self) -> &'static str { 72 | match self { 73 | MfaType::Email => "checkemail", 74 | MfaType::Sms => "checksms", 75 | MfaType::WebToApp => "checkwebtoapp", 76 | } 77 | } 78 | } 79 | 80 | impl BoursoWebClient { 81 | pub fn new() -> BoursoWebClient { 82 | // create a new client 83 | let cookie_store = CookieStore::new(None); 84 | let cookie_store = CookieStoreMutex::new(cookie_store); 85 | let cookie_store = Arc::new(cookie_store); 86 | BoursoWebClient { 87 | client: reqwest::Client::builder() 88 | .redirect(reqwest::redirect::Policy::none()) 89 | .cookie_provider(Arc::clone(&cookie_store)) 90 | .build() 91 | .unwrap(), 92 | cookie_store: cookie_store, 93 | brs_mit_cookie: String::new(), 94 | virtual_pad_ids: Default::default(), 95 | challenge_id: String::new(), 96 | customer_id: String::new(), 97 | token: String::new(), 98 | password: String::new(), 99 | config: Config::default(), 100 | } 101 | } 102 | 103 | /// Get the headers needed to make requests to the Bourso website. 104 | /// 105 | /// # Returns 106 | /// 107 | /// The headers as a `reqwest::header::HeaderMap`. 108 | #[cfg(not(tarpaulin_include))] 109 | fn get_headers(&self) -> reqwest::header::HeaderMap { 110 | let mut headers = reqwest::header::HeaderMap::new(); 111 | headers.insert( 112 | "user-agent", 113 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36".parse().unwrap(), 114 | ); 115 | 116 | headers 117 | } 118 | 119 | /// Get the login page content as a string. 120 | /// 121 | /// We're forced to call this page at least two times to retrieve the `__brs_mit` cookie and the form token. 122 | /// 123 | /// # Returns 124 | /// 125 | /// The login page content as a string. 126 | #[cfg(not(tarpaulin_include))] 127 | async fn get_login_page(&self) -> Result { 128 | Ok(self 129 | .client 130 | .get(format!("{BASE_URL}/connexion/")) 131 | .headers(self.get_headers()) 132 | .send() 133 | .await? 134 | .text() 135 | .await?) 136 | } 137 | 138 | /// Initialize the session by retrieving the `__brs_mit` cookie, the form token, the challenge ID and the virtual pad keys. 139 | /// 140 | /// # Returns 141 | /// 142 | /// Nothing if the session was initialized successfully, an error otherwise. 143 | #[cfg(not(tarpaulin_include))] 144 | pub async fn init_session(&mut self) -> Result<()> { 145 | // This first call is necessary to get the __brs_mit cookie 146 | let init_res = self.get_login_page().await?; 147 | 148 | self.brs_mit_cookie = extract_brs_mit_cookie(&init_res)?; 149 | 150 | // Use a scope to drop the lock on the cookie store 151 | // once we've inserted the necessary cookies 152 | { 153 | let mut store = self.cookie_store.lock().unwrap(); 154 | store.insert( 155 | Cookie::parse( 156 | // Necessary cookie to remove the domain migration error 157 | "brsDomainMigration=migrated;", 158 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(), 159 | ) 160 | .unwrap(), 161 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(), 162 | )?; 163 | store.insert( 164 | Cookie::parse( 165 | // Necessary cookie to access the virtual pad 166 | format!("__brs_mit={};", self.brs_mit_cookie), 167 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(), 168 | ) 169 | .unwrap(), 170 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(), 171 | )?; 172 | } 173 | 174 | // We call the login page again to a form token 175 | let res = self.get_login_page().await?; 176 | 177 | self.token = extract_token(&res)?; 178 | self.config = extract_brs_config(&res)?; 179 | debug!("Using version from {}", self.config.app_release_date); 180 | 181 | let res = self 182 | .client 183 | .get(format!("{BASE_URL}/connexion/clavier-virtuel?_hinclude=1")) 184 | .headers(self.get_headers()) 185 | .send() 186 | .await? 187 | .text() 188 | .await?; 189 | 190 | self.challenge_id = virtual_pad::extract_challenge_token(&res)?; 191 | 192 | self.virtual_pad_ids = virtual_pad::extract_data_matrix_keys(&res)? 193 | .map(|key| key.to_string()) 194 | .to_vec(); 195 | 196 | Ok(()) 197 | } 198 | 199 | /// Login to the Bourso website. 200 | /// 201 | /// # Arguments 202 | /// 203 | /// * `customer_id` - The customer ID used to login. 204 | /// * `password` - The password used to login in plaintext. 205 | /// 206 | /// # Returns 207 | /// 208 | /// Nothing if the login was successful, an error otherwise. 209 | #[cfg(not(tarpaulin_include))] 210 | pub async fn login(&mut self, customer_id: &str, password: &str) -> Result<()> { 211 | use error::ClientError; 212 | 213 | self.customer_id = customer_id.to_string(); 214 | self.password = 215 | virtual_pad::password_to_virtual_pad_keys(self.virtual_pad_ids.clone(), password)? 216 | .join("|"); 217 | let data = reqwest::multipart::Form::new() 218 | .text("form[fakePassword]", "••••••••") 219 | .text("form[ajx]", "1") 220 | .text("form[password]", self.password.clone()) 221 | // passwordAck is a JSON object that indicates the different times the user pressed on the virtual pad keys, 222 | // the click coordinates and the screen size. It seems like it's not necessary to fill the values to login. 223 | .text("form[passwordAck]", r#"{"ry":[],"pt":[],"js":true}"#) 224 | .text("form[platformAuthenticatorAvailable]", "1") 225 | .text("form[matrixRandomChallenge]", self.challenge_id.to_string()) 226 | .text("form[_token]", self.token.to_string()) 227 | .text("form[clientNumber]", self.customer_id.to_string()); 228 | 229 | let res = self 230 | .client 231 | .post(format!("{BASE_URL}/connexion/saisie-mot-de-passe")) 232 | .multipart(data) 233 | .headers(self.get_headers()) 234 | .send() 235 | .await?; 236 | 237 | if res.status() != 302 { 238 | let status = res.status(); 239 | let text = res.text().await?; 240 | if text.contains("Identifiant ou mot de passe invalide") 241 | || text.contains("Erreur d'authentification") 242 | { 243 | bail!(ClientError::InvalidCredentials); 244 | } 245 | error!("{}", text); 246 | bail!("Could not login to Bourso website, status code: {}", status); 247 | } 248 | 249 | let res = self 250 | .client 251 | .get(format!("{BASE_URL}/")) 252 | .headers(self.get_headers()) 253 | .send() 254 | .await? 255 | .text() 256 | .await?; 257 | 258 | if res.contains(r#"href="/se-deconnecter""#) { 259 | // Update the config with user hash 260 | self.config = extract_brs_config(&res)?; 261 | info!( 262 | "🔓 You are now logged in with user: {}", 263 | self.config.user_hash.as_ref().unwrap() 264 | ); 265 | } else { 266 | if res.contains("/securisation") { 267 | bail!(ClientError::MfaRequired) 268 | /* 269 | bail!(r#"Boursobank has flagged this connection as suspicious. 270 | You're likely trying to login from a new device or location. 271 | Password authentication is not allowed in this case. 272 | We're aware of this issue and are working on a fix (https://github.com/azerpas/bourso-api/pull/10). 273 | 274 | In the meantime, you can try to login to the website manually from your current location (ip address) to unblock the connection, and then retry here."#); 275 | */ 276 | } 277 | debug!("{}", res); 278 | 279 | bail!(ClientError::InvalidCredentials) 280 | } 281 | 282 | Ok(()) 283 | } 284 | 285 | /// Request the MFA code to be sent to the user. 286 | /// 287 | /// # Returns 288 | /// * `otp_id` - The OTP ID tied to the MFA request. 289 | /// * `token_form` - The token form to use to submit the MFA code. 290 | /// * `mfa_type` - The type of MFA requested. 291 | pub async fn request_mfa(&mut self) -> Result<(String, String, MfaType)> { 292 | let _ = self 293 | .client 294 | .get(format!("{BASE_URL}/securisation")) 295 | .headers(self.get_headers()) 296 | .send() 297 | .await?; 298 | 299 | let _ = self 300 | .client 301 | .get(format!("{BASE_URL}/x-domain-authentification/set-cookie")) 302 | .headers(self.get_headers()) 303 | .send() 304 | .await?; 305 | 306 | let _ = self 307 | .client 308 | .get(format!("{BASE_URL}/")) 309 | .headers(self.get_headers()) 310 | .send() 311 | .await?; 312 | 313 | let _ = self 314 | .client 315 | .get(format!("{BASE_URL}/securisation/authentification/")) 316 | .headers(self.get_headers()) 317 | .send() 318 | .await?; 319 | 320 | let res = self 321 | .client 322 | .get(format!( 323 | "{BASE_URL}/securisation/authentification/validation" 324 | )) 325 | .headers(self.get_headers()) 326 | .send() 327 | .await?; 328 | 329 | let res = res.text().await?; 330 | 331 | let mfa_type = if res.contains("brs-otp-email") { 332 | MfaType::Email 333 | } else if res.contains("brs-otp-sms") { 334 | MfaType::Sms 335 | } else if res.contains("brs-otp-webtoapp") { 336 | MfaType::WebToApp 337 | } else { 338 | debug!("{}", res); 339 | bail!("Could not request MFA, MFA type not found"); 340 | }; 341 | 342 | self.config = extract_brs_config(&res)?; 343 | let start_otp_url = match extract_start_otp_url(&res) { 344 | Ok(url) => url, 345 | Err(_) => { 346 | let res = self 347 | .client 348 | .get(format!("{BASE_URL}/securisation/validation")) 349 | .headers(self.get_headers()) 350 | .send() 351 | .await? 352 | .text() 353 | .await?; 354 | 355 | println!("securisation/validation response: {}", res); 356 | 357 | bail!("Could not request MFA, start sms otp url not found"); 358 | } 359 | }; 360 | 361 | let otp_id = start_otp_url.split("/").last().unwrap(); 362 | let contact_number = match mfa_type { 363 | MfaType::WebToApp => "your phone app".to_string(), 364 | _ => extract_user_contact(&res)?, 365 | }; 366 | let token_form = extract_token(&res)?; 367 | 368 | let url = format!( 369 | "{}/_user_/_{}_/session/otp/{}/{}", 370 | self.config.api_url, 371 | self.config.user_hash.as_ref().unwrap(), 372 | mfa_type.start_path(), 373 | otp_id 374 | ); 375 | debug!("Requesting MFA to {} with url {}", contact_number, url); 376 | 377 | let res = self 378 | .client 379 | .post(url) 380 | .body("{}") 381 | .headers(self.get_headers()) 382 | .send() 383 | .await?; 384 | 385 | if res.status() != 200 { 386 | bail!("Could not request MFA, status code: {}", res.status()); 387 | } 388 | 389 | let body = res.text().await?; 390 | let json_body: serde_json::Value = serde_json::from_str(&body)?; 391 | if json_body["success"].as_bool().unwrap() { 392 | info!("{} MFA request sent to {}", mfa_type, contact_number); 393 | } else { 394 | error!("{}", json_body); 395 | bail!("Could not request MFA, response: {}", json_body); 396 | } 397 | 398 | Ok((otp_id.to_string(), token_form, mfa_type)) 399 | } 400 | 401 | /// Submit the MFA code to the Bourso website. 402 | /// 403 | /// # Arguments 404 | /// * `mfa_type` - The type of MFA to submit. 405 | /// * `otp_id` - The OTP ID tied to the MFA request. 406 | /// * `code` - The MFA code to submit. 407 | /// * `token_form` - The token form to use to submit the MFA code. 408 | pub async fn submit_mfa( 409 | &mut self, 410 | mfa_type: MfaType, 411 | otp_id: String, 412 | code: String, 413 | token_form: String, 414 | ) -> Result<()> { 415 | let url = format!( 416 | "{}/_user_/_{}_/session/otp/{}/{}", 417 | self.config.api_url, 418 | self.config.user_hash.as_ref().unwrap(), 419 | mfa_type.check_path(), 420 | otp_id 421 | ); 422 | debug!("Submitting MFA code to {}", url); 423 | 424 | let payload = serde_json::json!({ 425 | "token": code 426 | }); 427 | let res = self 428 | .client 429 | .post(url) 430 | .body(payload.to_string()) 431 | .header("Content-Type", "application/json; charset=utf-8") 432 | .send() 433 | .await?; 434 | 435 | if res.status() != 200 { 436 | bail!("Could not submit MFA code, status code: {}", res.status()); 437 | } 438 | 439 | let body = res.text().await?; 440 | let json_body: serde_json::Value = serde_json::from_str(&body)?; 441 | 442 | if json_body["success"].as_bool().unwrap() { 443 | debug!("Submitting form with token: {}", token_form); 444 | 445 | let params = [("form[_token]", token_form)]; 446 | 447 | let res = self 448 | .client 449 | .post(format!( 450 | "{BASE_URL}/securisation/authentification/validation" 451 | )) 452 | .form(¶ms) 453 | .header("Host", "clients.boursobank.com") 454 | .header( 455 | "accept", 456 | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 457 | ) 458 | .header("origin", "https://clients.boursobank.com") 459 | .header("sec-fetch-site", "same-origin") 460 | .header("sec-fetch-mode", "navigate") 461 | .headers(self.get_headers()) 462 | .header( 463 | "referer", 464 | "https://clients.boursobank.com/securisation/authentification/validation", 465 | ) 466 | .header("sec-fetch-dest", "document") 467 | .header("accept-language", "fr-FR,fr;q=0.9") 468 | .header("priority", "u=0, i") 469 | .send() 470 | .await?; 471 | 472 | if res.status() != 302 { 473 | // bail!("Could not submit MFA validation, status code: {}", res.status()); 474 | println!( 475 | "Could not submit MFA validation, status code: {}", 476 | res.status() 477 | ); 478 | } 479 | 480 | let res = self 481 | .client 482 | .get(format!("{BASE_URL}/")) 483 | .headers(self.get_headers()) 484 | .header( 485 | "referer", 486 | format!("{BASE_URL}/securisation/authentification/validation"), 487 | ) 488 | .header("accept-language", "fr-FR,fr;q=0.9") 489 | .send() 490 | .await? 491 | .text() 492 | .await?; 493 | 494 | if res.contains(r#"href="/se-deconnecter""#) { 495 | // Update the config with user hash 496 | self.config = extract_brs_config(&res)?; 497 | info!( 498 | "🔓 You are now logged in with user: {}", 499 | self.config.user_hash.as_ref().unwrap() 500 | ); 501 | } else { 502 | if res.contains("/securisation") { 503 | bail!(ClientError::MfaRequired); 504 | } 505 | 506 | bail!("Could not submit MFA, response: {}", res); 507 | } 508 | 509 | info!("🔓 MFA successfully submitted"); 510 | } else { 511 | error!("{}", json_body); 512 | bail!("Could not submit MFA, response: {}", json_body); 513 | } 514 | 515 | Ok(()) 516 | } 517 | } 518 | 519 | /// Extract the __brs_mit cookie from a string, usually the response of the `/connexion/` page. 520 | /// 521 | /// # Arguments 522 | /// 523 | /// * `res` - The response of the `/connexion/` page as a string. 524 | /// 525 | /// # Returns 526 | /// 527 | /// The __brs_mit cookie as a string. 528 | fn extract_brs_mit_cookie(res: &str) -> Result { 529 | let regex = Regex::new(r"(?m)__brs_mit=(?P.*?);").unwrap(); 530 | let captures = regex.captures(&res); 531 | 532 | if captures.is_none() { 533 | error!("{}", res); 534 | bail!("Could not extract brs mit cookie"); 535 | } 536 | 537 | let brs_mit_cookie = captures.unwrap().name("brs_mit_cookie").unwrap(); 538 | 539 | Ok(brs_mit_cookie.as_str().to_string()) 540 | } 541 | 542 | fn extract_token(res: &str) -> Result { 543 | let regex = Regex::new(r#"(?ms)form\[_token\]"(.*?)value="(?P.*?)"\s*>"#).unwrap(); 544 | let token = regex.captures(&res).unwrap().name("token").unwrap(); 545 | 546 | Ok(token.as_str().trim().to_string()) 547 | } 548 | 549 | fn extract_start_otp_url(res: &str) -> Result { 550 | let regex = Regex::new(r"(?m)\\/services\\/api\\/v[\d.]*?\\/_user_\\/_\{userHash\}_\\/session\\/otp\\/start.*?\\/\d+").unwrap(); 551 | 552 | let captures = regex.captures(&res); 553 | 554 | if captures.is_none() { 555 | error!("{}", res); 556 | bail!("Could not extract start sms otp url"); 557 | } 558 | 559 | let start_sms_otp_url = captures.unwrap().get(0).unwrap(); 560 | 561 | Ok(start_sms_otp_url.as_str().replace("\\", "").to_string()) 562 | } 563 | 564 | fn extract_user_contact(res: &str) -> Result { 565 | let regex = Regex::new(r"(?m)userContact":"(?P.*?)"").unwrap(); 566 | let contact_user = regex.captures(&res).unwrap().name("contact_user").unwrap(); 567 | 568 | Ok(contact_user.as_str().trim().to_string()) 569 | } 570 | 571 | #[cfg(test)] 572 | mod tests { 573 | use super::*; 574 | 575 | #[test] 576 | fn test_get_headers() { 577 | let client = BoursoWebClient::new(); 578 | let headers = client.get_headers(); 579 | assert_eq!(headers.get("user-agent").unwrap().to_str().unwrap(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"); 580 | } 581 | 582 | #[test] 583 | fn test_extract_brs_mit_cookie() { 584 | let res = r#" \n\n\n \n\n\n\n\n\n"#; 585 | let brs_mit_cookie = extract_brs_mit_cookie(&res).unwrap(); 586 | assert_eq!(brs_mit_cookie, "8e6912eb6a0268f0a2411668b8bf289f"); 587 | } 588 | 589 | #[test] 590 | fn test_extract_token() { 591 | let res = r#"data-backspace>